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 storage?Runner 注册时用root还是专用系统用户?before_script里该不该apt update?script阶段的命令是直接执行还是封装成脚本?这些选择没有标准答案,但每个答案背后,都连着资源开销、安全边界、可维护性与排障效率。
关键词里虽然空着,但整个过程天然锚定四个不可绕过的技术坐标:GitLab CE(Community Edition)、Docker Compose 部署模式、GitLab Runner 注册与执行器选型、Shell 执行器下的轻量级部署逻辑。它们共同构成了一条“看得见、摸得着、改得了”的 CI/CD 链路。接下来,我会带你一步步走完这条链路,不跳过任何一行配置的含义,不省略任何一个看似琐碎的验证步骤,并把我在三次真实迁移中踩出的坑,原样摊开给你看。
2. 从 Docker Compose 开始:为什么不用 Omnibus 包,也不推荐裸机安装?
很多人第一次搭 GitLab,会直奔官网下载.deb或.rpm包,执行sudo EXTERNAL_URL="https://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_PASSWORD=gitlab_db_password' - 'POSTGRES_USER=gitlab' - 'POSTGRES_DB=gitlabhq_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 官方支持多种执行器(executor):docker、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 --user=gitlab-runner --working-directory=/home/gitlab-runner sudo gitlab-runner start注意:
--user=gitlab-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-untagged="true" \ --locked="false" \ --access-level="not_protected"逐个参数解释:
--url:必须和 GitLab 的external_url完全一致,包括协议和端口。这里是http://gitlab.example.com/,结尾的/不能少;--registration-token:Admin Area 里复制的全局 token;--description:给 Runner 起个名字,便于识别;--tag-list:打标签。后续.gitlab-ci.yml里可以用tags: [shell]指定这个 Runner 执行;--executor "shell":明确指定执行器类型;--run-untagged="true":允许执行没有打 tag 的 job(方便 HelloWorld 测试);--locked="false":不锁定 Runner,允许多个项目共享;--access-level="not_protected":允许在非 protected 分支上运行(开发阶段方便)。
注册成功后,你会看到类似Runner registered successfully. Feel free to start it, but if it's 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.yml:
stages: - 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.shHelloWorld 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.shgitlab-runner,符合预期。 - 模拟故障恢复:删掉脚本,再触发一次 pipeline:
sudo 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.yml:
deploy-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] Description=HelloWorld Service After=network.target [Service] Type=simple User=gitlab-runner WorkingDirectory=/opt/hello-world ExecStart=/opt/hello-world/hello.sh Restart=always RestartSec=10 [Install] WantedBy=multi-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 辗转反侧的夜晚,值了。