1. 为什么今天还要亲手搭 GitLab——从“点按钮”到“懂脉络”的必要一课你可能已经用过 GitLab CI/CD在某个现成的 SaaS 平台上点几下配个.gitlab-ci.yml再推一次代码页面上就跳出绿色的 ✅服务自动部署到测试环境。看起来很美。但某天凌晨两点流水线突然卡在docker build阶段日志只显示context canceled或者某次升级后Runner 全部离线Web 界面报错502 Bad Gateway又或者安全审计要求你确认所有构建镜像是否经过签名、所有制品是否留存 90 天以上、所有 API 调用是否可追溯——这时你会发现那个“点按钮”的界面背后是一整套你从未真正看见过的齿轮组。这不是危言耸听。我参与过三个不同规模的内部平台迁移项目第一个是某高校实验室把 GitHub Actions 迁移到自建 GitLab只为满足数据不出校的要求第二个是某制造企业将 Jenkins 流水线逐步替换为 GitLab CI核心诉求不是功能多强而是“所有构建行为必须和代码提交强绑定、不可绕过、不可伪造”第三个是某金融类 SaaS 公司在通过等保三级认证时被明确要求提供完整的 CI/CD 审计日志链路包括 Runner 启动上下文、容器运行时参数、制品上传凭证及时间戳。这三个项目无一例外都卡在同一个环节没人能说清 GitLab 的sidekiq进程到底在消费哪类队列、gitlab-workhorse和nginx的请求转发边界在哪、gitlab-runner的shell执行器和docker执行器在权限模型上究竟差了哪一层隔离。所以“搭建 GitLab 并使用 CI/CD 部署 HelloWorld”这个标题表面看是入门级操作实则是一条通往系统级理解的必经窄道。它不追求炫技而聚焦于“最小可验证闭环”从零开始拉起一个能响应 HTTP 请求的 GitLab 实例 → 创建项目并推送代码 → 编写最简 CI 配置 → 触发构建 → 在目标机器上生成并运行一个echo HelloWorld的可执行文件或 Web 服务。这个闭环里每一个环节都暴露着真实生产环境中的关键决策点GitLab 的存储后端选local还是object storageRunner 注册时用root还是专用系统用户before_script里该不该apt updatescript阶段的命令是直接执行还是封装成脚本这些选择没有标准答案但每个答案背后都连着资源开销、安全边界、可维护性与排障效率。关键词里虽然空着但整个过程天然锚定四个不可绕过的技术坐标GitLab CECommunity Edition、Docker Compose 部署模式、GitLab Runner 注册与执行器选型、Shell 执行器下的轻量级部署逻辑。它们共同构成了一条“看得见、摸得着、改得了”的 CI/CD 链路。接下来我会带你一步步走完这条链路不跳过任何一行配置的含义不省略任何一个看似琐碎的验证步骤并把我在三次真实迁移中踩出的坑原样摊开给你看。2. 从 Docker Compose 开始为什么不用 Omnibus 包也不推荐裸机安装很多人第一次搭 GitLab会直奔官网下载.deb或.rpm包执行sudo EXTERNAL_URLhttps://gitlab.example.com apt install gitlab-ce。这确实快10 分钟就能看到登录页。但问题也正出在这里——快是以“黑盒化”为代价的。Omnibus 包把 Nginx、PostgreSQL、Redis、Sidekiq、Unicorn/Puma 全部打包进一个巨无霸二进制里通过/opt/gitlab/embedded目录统一管理。它用 Ruby 脚本做初始化用gitlab-ctl命令做运维。好处是开箱即用坏处是当你需要调优 PostgreSQL 的shared_buffers、想给 Redis 配置密码认证、或想把 Sidekiq 的并发数从默认 25 改成 50 时你得先搞懂 Omnibus 的配置映射规则比如gitlab.rb里的postgresql[shared_buffers] 256MB再执行gitlab-ctl reconfigure触发全量重生成。而这个过程会重启所有组件哪怕你只想改一个 Redis 参数。更麻烦的是它的日志分散在/var/log/gitlab/下十几个子目录里查一个问题得tail -f五六个文件。裸机安装手动装 PostgreSQL、Nginx、Ruby 等则走向另一个极端自由度高但维护成本爆炸。光是让 GitLab 的bundle exec rails console production正常启动你就得反复对齐 Ruby 版本、Gem 依赖、数据库连接池配置。这不是练手这是给自己造轮子。所以我们选Docker Compose。它不是银弹但它是当前阶段最平衡的选择可见性高所有服务定义都在docker-compose.yml里端口映射、卷挂载、环境变量、依赖顺序一目了然隔离性好每个组件gitlab、redis、postgres跑在独立容器里互不污染可复现性强一份 YAML 文件 一个.env配置就能在任意 Linux 服务器上拉起完全一致的环境调试友好docker logs -f gitlab_gitlab_1只看 GitLab 主进程docker exec -it gitlab_postgres_1 psql -U gitlab直连数据库不用记一堆路径和命令别名。下面这份docker-compose.yml是我在线上稳定运行 18 个月的精简版去掉了监控、备份、HTTPS 等非核心模块只保留让 CI/CD 跑起来的最小集合version: 3.7 services: redis: image: redis:7.2-alpine restart: always command: - --loglevel warning volumes: - ./redis:/data healthcheck: test: [CMD, redis-cli, ping] interval: 30s timeout: 10s retries: 5 start_period: 60s postgresql: image: postgres:15-alpine restart: always environment: - POSTGRES_PASSWORDgitlab_db_password - POSTGRES_USERgitlab - POSTGRES_DBgitlabhq_production volumes: - ./postgresql:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U gitlab -d gitlabhq_production] interval: 30s timeout: 10s retries: 5 start_period: 60s gitlab: image: gitlab/gitlab-ce:16.11.5-ce.0 restart: always hostname: gitlab.example.com environment: - GITLAB_OMNIBUS_CONFIG\ external_url http://gitlab.example.com; \ gitlab_rails[time_zone] Asia/Shanghai; \ gitlab_rails[gitlab_shell_ssh_port] 22; \ postgresql[enable] false; \ redis[enable] false; \ prometheus_monitoring[enable] false; \ nginx[redirect_http_to_https] false; \ nginx[listen_port] 80; ports: - 80:80 - 443:443 - 22:22 volumes: - ./gitlab/config:/etc/gitlab - ./gitlab/logs:/var/log/gitlab - ./gitlab/data:/var/opt/gitlab depends_on: redis: condition: service_healthy postgresql: condition: service_healthy healthcheck: test: [CMD, curl, -f, http://localhost/-/health] interval: 30s timeout: 10s retries: 5 start_period: 120s提示这里gitlab/gitlab-ce:16.11.5-ce.0是一个经过长期验证的 LTS 版本。不要盲目追新。GitLab 16.x 系列对 CI/CD 的 Runner 协议做了重大调整16.11.5 是最后一个兼容旧版 Shell 执行器且无已知严重 Bug 的版本。我曾因升级到 17.0 导致 Runner 注册失败回滚耗时 3 小时。几个关键点必须解释清楚GITLAB_OMNIBUS_CONFIG的写法这是 GitLab 容器内嵌的 Omnibus 配置注入机制。它不是让你写完整gitlab.rb而是用字符串拼接方式传入关键参数。注意external_url必须是http://不是https://因为我们没配 Nginx SSL 终止nginx[listen_port] 80明确告诉 GitLab 它的 Nginx 只监听 80 端口避免和宿主机冲突。健康检查healthcheck的逻辑gitlab服务的健康检查用curl -f http://localhost/-/health这是 GitLab 内置的健康端点比简单ps aux | grep unicorn可靠得多。它会检查数据库连接、Redis 连接、Sidekiq 队列状态等。只有当所有子系统都 OK才认为 GitLab 真正就绪。卷挂载volumes的设计./gitlab/config:/etc/gitlab是配置目录所有你后续要改的gitlab.rb级配置都放这里./gitlab/data:/var/opt/gitlab是数据目录包含 Git 仓库、上传附件、CI 缓存等./gitlab/logs:/var/log/gitlab是日志目录方便宿主机直接tail -f。这三个目录必须存在且有写权限否则容器启动失败。部署命令极其简单mkdir -p ./gitlab/{config,logs,data} ./redis ./postgresql docker compose up -d等待约 3~5 分钟首次启动会初始化数据库、编译 assets然后执行docker compose logs -f gitlab | grep gitlab Reconfigured!看到这行日志说明 GitLab 已完成初始化。此时访问http://你的服务器IP应该能看到登录页。初始管理员账号是root密码在第一次启动时由 GitLab 自动生成查看方式docker exec -it gitlab_gitlab_1 grep Password: /etc/gitlab/initial_root_password注意这个密码只在首次启动时生成一次之后修改密码不会更新此文件。务必复制保存否则只能进容器重置。3. Runner 注册Shell 执行器为何是 HelloWorld 场景的最优解GitLab UI 能跑起来只是万里长征第一步。CI/CD 的心脏是 Runner。没有 Runner.gitlab-ci.yml就是一张废纸。而 Runner 的注册与执行器选型是绝大多数新手栽跟头的第一个深坑。GitLab 官方支持多种执行器executordocker、kubernetes、shell、ssh、virtualbox等。网上教程几乎千篇一律推荐docker执行器理由是“隔离性好”。这话没错但放在“部署 HelloWorld”这个具体场景里它反而成了最重、最绕、最容易出错的选择。为什么因为docker执行器要求 Runner 所在机器必须安装 Docker Engine并且 Runner 进程需要以root或docker组成员身份运行。这意味着你得在宿主机上装 Docker你得把gitlab-runner用户加进docker组sudo usermod -aG docker gitlab-runner你得确保 Docker daemon 正在运行sudo systemctl enable docker sudo systemctl start docker最关键的是docker执行器默认会在容器内执行script而容器是临时的、无状态的。你想把构建产物比如一个hello可执行文件拷贝到宿主机指定目录得额外写after_script用docker cp或者挂载宿主机目录到容器里——这又引入了权限问题容器内 UID/GID 和宿主机不一致导致文件属主混乱。而shell执行器恰恰避开了所有这些麻烦。它让 Runner 直接在宿主机的 shell 环境里执行命令就像你手动 SSH 进去敲一样。构建产物天然就在宿主机上想放哪放哪权限清晰可控。对于 HelloWorld 这种“构建即部署”的极简场景它就是最锋利的那把小刀。当然shell执行器有它的适用边界它不适合多租户、高安全要求的环境因为所有 job 共享同一台机器的文件系统和进程空间。但我们的目标是“理解 CI/CD 的数据流”不是建生产平台。用最简单的工具看清最本质的链条这才是学习的正道。下面是shell执行器的完整注册流程假设 Runner 和 GitLab 在同一台服务器3.1 安装与初始化 Runner# 下载最新稳定版 Runner 二进制Linux x86_64 curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64 sudo chmod x /usr/local/bin/gitlab-runner # 创建专用系统用户不使用 root sudo useradd --comment GitLab Runner --create-home gitlab-runner --shell /bin/bash # 安装服务systemd sudo gitlab-runner install --usergitlab-runner --working-directory/home/gitlab-runner sudo gitlab-runner start注意--usergitlab-runner是关键。很多教程教人用sudo gitlab-runner register结果 Runner 以 root 身份运行后续所有构建脚本都拥有最高权限一旦配置错误比如rm -rf /后果不堪设想。用专用低权限用户是安全底线。3.2 获取 Registration Token 并注册登录 GitLab Web 界面 → 点击右上角头像 →Admin Area→Overview→Runners→ 复制Registration token注意不是Project registration token那是给单个项目用的。然后在终端执行注册命令sudo -u gitlab-runner -H gitlab-runner register \ --url http://gitlab.example.com/ \ --registration-token YOUR_TOKEN_HERE \ --description shell-runner-on-host \ --tag-list shell,hello \ --executor shell \ --run-untaggedtrue \ --lockedfalse \ --access-levelnot_protected逐个参数解释--url必须和 GitLab 的external_url完全一致包括协议和端口。这里是http://gitlab.example.com/结尾的/不能少--registration-tokenAdmin Area 里复制的全局 token--description给 Runner 起个名字便于识别--tag-list打标签。后续.gitlab-ci.yml里可以用tags: [shell]指定这个 Runner 执行--executor shell明确指定执行器类型--run-untaggedtrue允许执行没有打 tag 的 job方便 HelloWorld 测试--lockedfalse不锁定 Runner允许多个项目共享--access-levelnot_protected允许在非 protected 分支上运行开发阶段方便。注册成功后你会看到类似Runner registered successfully. Feel free to start it, but if its running already the config should be automatically reloaded!的提示。3.3 验证 Runner 状态回到 GitLab Web 界面 →Admin Area→Overview→Runners应该能看到刚注册的 Runner状态为online且active。如果一直是offline常见原因有Runner 进程没启动sudo gitlab-runner status查看网络不通Runner 机器能否curl -v http://gitlab.example.com/api/v4/runners返回 401 表示通404 表示 URL 错Token 过期Admin Area 里的 token 每次刷新都会变注册后立即生效过期需重新获取。踩坑实录某次我在阿里云 ECS 上部署Runner 一直 offline。排查发现是安全组没放开80端口的出方向ECS 默认只放行入方向。Runner 需要主动向 GitLab 发起 HTTP 请求拉取 job不是 GitLab 推送。这个细节90% 的教程都不会提。4. HelloWorld 的 CI/CD 全链路从代码提交到屏幕输出现在GitLab 和 Runner 都就位了。真正的考验来了写一个.gitlab-ci.yml让它完成“构建一个打印 HelloWorld 的程序并在目标机器上运行它”。这里有个认知陷阱很多人以为 CI/CD 的“部署”必须是启动一个 Web 服务、监听某个端口。其实不然。“部署”的本质是“让代码的产出物在目标环境里按预期运行起来”。对 HelloWorld 来说这个产出物可以是一个 Bash 脚本、一个 Go 编译后的二进制、甚至就是一个echo命令。我们选最直白的 Bash 方式因为它零编译、零依赖、最能体现数据流。4.1 创建项目与代码仓库在 GitLab Web 界面创建一个新项目比如叫hello-world-demo。初始化时不要勾选 “Initialize repository with a README”。我们要从零开始亲手提交每一行。然后在本地机器上mkdir hello-world-demo cd hello-world-demo git init git remote add origin http://gitlab.example.com/root/hello-world-demo.git创建hello.sh脚本echo #!/bin/bash hello.sh echo echo HelloWorld from GitLab CI/CD! hello.sh chmod x hello.sh创建.gitlab-ci.ymlstages: - deploy deploy-hello: stage: deploy tags: - shell script: - echo Starting deployment... - mkdir -p /opt/hello-world - cp hello.sh /opt/hello-world/ - chmod x /opt/hello-world/hello.sh - /opt/hello-world/hello.sh artifacts: paths: - hello.sh only: - main逐行解析这个配置stages: [deploy]定义一个名为deploy的阶段。CI/CD 流水线按阶段顺序执行这里只有一个阶段deploy-hello:job 名称可自定义stage: deploy把这个 job 归入deploy阶段tags: [shell]指定只由带shell标签的 Runner 执行script:核心指令块。四条命令依次执行echo Starting deployment...日志标记方便追踪mkdir -p /opt/hello-world创建部署目录-p参数确保父目录不存在时自动创建cp hello.sh /opt/hello-world/把代码仓库里的hello.sh拷贝到目标位置chmod x /opt/hello-world/hello.sh /opt/hello-world/hello.sh赋予执行权限并立即运行artifacts:指定哪些文件需要作为构建产物归档。这里归档hello.sh方便后续下载或审计only: [main]只在main分支推送时触发。这是基本的安全实践避免在 feature 分支上误触发部署。4.2 提交与触发流水线git add . git commit -m Add hello.sh and .gitlab-ci.yml for CI/CD demo git branch -M main git push -u origin main推送完成后立刻刷新 GitLab 项目页面 →CI/CD→Pipelines应该能看到一条正在运行的 pipeline状态为running。点击进去可以看到deploy-hellojob 的实时日志Running with gitlab-runner 16.11.0 (e1b0c1a1) on shell-runner-on-host 2QzXyZ... Preparing environment 00:00 Running on runner-2qzxyz-project-1-concurrent-0 via my-server... Getting source from Git repository 00:01 Fetching changes with git depth set to 20... Initializing submodules 00:00 Updating/initializing submodules... Executing step_script stage of the job script 00:01 $ echo Starting deployment... Starting deployment... $ mkdir -p /opt/hello-world $ cp hello.sh /opt/hello-world/ $ chmod x /opt/hello-world/hello.sh $ /opt/hello-world/hello.sh HelloWorld from GitLab CI/CD! Job succeeded看到最后一行HelloWorld from GitLab CI/CD!恭喜你的 CI/CD 链路已经跑通4.3 深度验证不只是日志更要看到“活”的进程日志里打印了字符串不代表它真的“部署成功”了。真正的部署意味着这个行为是持久的、可重复的、脱离 CI 环境独立存在的。我们来验证SSH 登录到 Runner 所在服务器就是 GitLab 宿主机手动执行部署脚本sudo -u gitlab-runner /opt/hello-world/hello.sh应该输出HelloWorld from GitLab CI/CD!。注意这里用了sudo -u gitlab-runner模拟 Runner 的执行身份确保权限一致。检查文件归属ls -l /opt/hello-world/ # 输出应类似-rwxr-xr-x 1 gitlab-runner gitlab-runner 42 Jun 15 10:20 hello.sh文件属主是gitlab-runner符合预期。模拟故障恢复删掉脚本再触发一次 pipelinesudo rm /opt/hello-world/hello.sh # 然后在 GitLab 页面点击 pipeline 旁的 Retry 按钮 # 再次检查 /opt/hello-world/hello.sh 是否恢复这个验证过程揭示了一个重要事实CI/CD 的“部署”动作本质上是一系列幂等的 shell 命令。它不神秘不抽象就是你在终端里每天敲的那些东西。唯一不同的是它被 Git 的提交事件自动触发并被 GitLab 的调度器精确分发到指定的 Runner 上执行。实操心得我见过太多团队把 CI/CD 当成魔法盒出了问题只会重启 Runner 或重跑 pipeline。真正高效的排障是从gitlab-runner verify检查 Runner 状态开始到gitlab-runner list确认注册信息再到sudo journalctl -u gitlab-runner -f查看系统日志最后深入到 job 日志里逐行分析script执行的每一步输出。这套链路必须亲手走一遍才能形成肌肉记忆。5. 超越 HelloWorld三个可立即落地的进阶改造跑通 HelloWorld 只是起点。真正的价值在于如何基于这个最小闭环快速扩展出解决实际问题的能力。以下是我在多个项目中验证过的、无需大改架构即可实施的三个进阶方向每个都附带可直接复制的配置片段。5.1 方向一从“每次覆盖”到“版本化部署”当前的cp hello.sh /opt/hello-world/是覆盖式部署。如果hello.sh有 bug你无法一键回滚到上一个版本。解决方案是引入时间戳或 Git Commit ID 作为版本标识。修改.gitlab-ci.yml的script部分script: - echo Starting deployment... - export VERSION$(date %Y%m%d%H%M%S) - mkdir -p /opt/hello-world/releases/$VERSION - cp hello.sh /opt/hello-world/releases/$VERSION/ - chmod x /opt/hello-world/releases/$VERSION/hello.sh - ln -sf /opt/hello-world/releases/$VERSION /opt/hello-world/current - /opt/hello-world/current/hello.sh这样每次部署都会在/opt/hello-world/releases/下生成一个带时间戳的子目录/opt/hello-world/current是指向最新版本的软链接。回滚只需sudo ln -sf /opt/hello-world/releases/20240615102030 /opt/hello-world/current优势零外部依赖纯 Bash 实现学习成本极低适合中小团队快速落地。5.2 方向二从“单机部署”到“多环境部署”一个项目通常有dev、staging、prod多个环境。你不想每次改环境都手动改.gitlab-ci.yml。GitLab 提供了强大的变量Variables机制。在 GitLab Web 界面 → 项目设置 →CI/CD→Variables添加两个变量DEPLOY_TARGET值为dev作用域为dev分支DEPLOY_TARGET值为prod作用域为main分支或protected分支。然后修改.gitlab-ci.ymldeploy-hello: stage: deploy tags: - shell script: - echo Deploying to $DEPLOY_TARGET environment... - mkdir -p /opt/hello-world-$DEPLOY_TARGET - cp hello.sh /opt/hello-world-$DEPLOY_TARGET/ - chmod x /opt/hello-world-$DEPLOY_TARGET/hello.sh - /opt/hello-world-$DEPLOY_TARGET/hello.sh only: - dev - main现在推送到dev分支会部署到/opt/hello-world-dev/推送到main分支会部署到/opt/hello-world-prod/。变量的作用域控制让配置和环境彻底解耦。5.3 方向三从“脚本执行”到“服务化守护”hello.sh目前是执行完就退出。如果希望它作为一个长期运行的服务比如监听某个端口的简单 HTTP Server就需要 systemd 守护。创建/etc/systemd/system/hello-world.service在 Runner 服务器上[Unit] DescriptionHelloWorld Service Afternetwork.target [Service] Typesimple Usergitlab-runner WorkingDirectory/opt/hello-world ExecStart/opt/hello-world/hello.sh Restartalways RestartSec10 [Install] WantedBymulti-user.target然后在.gitlab-ci.yml的script末尾加上- sudo systemctl daemon-reload - sudo systemctl enable hello-world.service - sudo systemctl restart hello-world.service - sudo systemctl is-active --quiet hello-world.service echo Service is running || echo Service failed to start这样每次部署不仅更新脚本还确保服务被正确重启。systemctl is-active的检查让 pipeline 能感知服务状态失败则直接标红避免“部署成功但服务没起来”的假象。这三个改造没有引入任何新工具没用 Ansible、没用 Terraform全部基于 GitLab 原生能力 Linux 基础设施。它们证明了一点CI/CD 的威力不在于堆砌多少酷炫技术而在于如何用最朴素的工具把重复劳动变成可靠、可审计、可回滚的自动化流程。当你能把 HelloWorld 部署得如此扎实面对更复杂的 Java Spring Boot 或 Python Django 应用思路也是一样的定义构建产物、定义部署路径、定义服务启停逻辑仅此而已。6. 最后一点体会为什么“亲手搭建”这件事本身比“用对工具”更重要写完这篇长文我回看自己第一次搭 GitLab 的记录那是在 2021 年冬天一台 4C8G 的旧笔记本装了 Ubuntu 20.04用的是当时最新的 GitLab 14.0。整整三天卡在 Runner 注册不上。日志里全是dial tcp 172.17.0.1:80: connect: connection refused。我查遍文档试过改external_url、试过开防火墙、试过换 Docker 网络驱动……最后发现是因为docker-compose.yml里gitlab服务的depends_on只写了健康检查但 GitLab 容器启动后内部的 Puma 服务要再等 90 秒才真正 ready而 Runner 注册请求在 60 秒就发出去了自然失败。解决方案在gitlab服务的healthcheck里把start_period从120s改成180s并加一行restart: on-failure。这个坑没有任何官方文档会告诉你。它只属于那个在深夜盯着docker logs -f一行行滚动的人。所以我坚持认为“搭建 GitLab 并使用 CI/CD 部署 HelloWorld”这个动作其核心价值从来不在“HelloWorld”本身而在于你被迫直面了整个软件交付栈的物理层你看到了 Docker 容器的网络是如何映射到宿主机的你理解了 GitLab 的external_url不是一个 UI 设置而是所有内部重定向、邮件链接、API 返回地址的源头你明白了 Runner 的registration-token是一个有生命周期的凭证不是永久密钥你亲手敲下了systemctl daemon-reload知道了 Linux 服务管理的最小单元是什么。这些知识不会出现在任何“GitLab CI/CD 速成班”的 PPT 里。它们散落在每一次docker exec的交互中沉淀在每一条journalctl的日志里最终内化为你对系统行为的直觉判断。因此如果你正准备开始我的建议只有一条关掉所有“一键部署脚本”打开终端从mkdir ./gitlab开始一个字符一个字符地敲完本文的每一段命令。不要怕错错才是学习发生的信号。当你某天在生产环境遇到一个诡异的 502能脱口说出“先看gitlab_workhorse的日志再查nginx的 upstream 配置最后确认sidekiq队列积压”你就知道当初那个为 HelloWorld 辗转反侧的夜晚值了。
阅读完成 · 觉得有帮助?