news 2026/10/15 18:06:35

自建GitLab CI/CD:HelloWorld项目自动部署完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自建GitLab CI/CD:HelloWorld项目自动部署完整实践

刚接触CI/CD的时候,我觉得那是后端大团队才玩的东西。后来被部署问题逼了几次,才意识到一台服务器加一个HelloWorld项目,就能把流水线跑顺。这篇文章就是完整记录:自己搭建GitLab,注册Runner,写一个Flask版HelloWorld,再通过CI/CD自动部署到目标主机。适合刚入门DevOps、想搭一套私有CI/CD环境的人。内容不复杂,但每一步都有坑,我会把配参数的原因一并讲清楚。

1. 项目拆解:一条HelloWorld流水线背后需要什么

1.1 CI/CD到底是什么,这个项目能解决什么问题

CI/CD翻译过来就是持续集成和持续部署。持续集成指每次代码提交后自动触发构建、测试,让问题尽早暴露;持续部署指通过流水线把产物自动发布到目标环境。很多小团队一开始不需要复杂平台,但至少要把“提交代码、构建镜像、发布服务”这个过程自动化。我选择用一个HelloWorld项目来跑通全流程,因为它足够简单——只要流水线能自动完成,后面换成复杂业务只是堆步骤而已。

在这个项目里,我们做的事情可以拆成四段:代码提交到GitLab、GitLab Runner感知到变更、Runner执行构建与测试、最后通过SSH登录目标服务器部署容器。整个过程不依赖任何人手动操作,以后你在一家小团队里可以把这套思路复制到任何Java、Go、Node项目上。这个HelloWorld的价值在于验证流程,而不是业务功能。

1.2 自建GitLab还是用现成的托管平台

GitLab本身分为开源版、商业版以及官方提供的SaaS服务。我最终选择自建,基于三个原因:代码放内网更可控、免费版已经包含CI/CD功能、服务器资源占用并不夸张。如果你已经有托管平台,可以直接跳过搭建步骤,但自托管能让你真正理解Runner和GitLab之间的通信逻辑,这对排查问题很有帮助。

自建GitLab最常见的方式有Omnibus安装包和Docker容器两种。Omnibus适合裸机安装,和系统耦合深,升级有心理负担;Docker方式直接把GitLab封装成容器,备份迁移都方便,所以我采用Docker Compose。服务器方面,GitLab最低建议内存4GB,我这台机器是8GB,实际跑GitLab加Runner加构建任务,剩余资源大概还能支撑一两个小型服务,做个学习和中型项目试点完全够用。

2. 服务器准备与GitLab安装

2.1 基础环境要求

选一台干净的内网服务器。操作系统建议使用Ubuntu 22.04,当然Debian、CentOS Stream都可以,前提是能装Docker。需要提前装好Docker和Compose插件,命令我直接放在这里:

sudo apt update sudo apt install -y curl git vim curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo apt install -y docker-compose-plugin

检查Docker是否正常:

docker version docker compose version

这里有一个经验:如果服务器上跑着防火墙或SELinux,先不要关闭安全功能,而是放行相应端口。GitLab主要用到SSH(默认2222)、HTTP/HTTPS(默认80和443)。如果你要加HTTPS,还需要放行443。为了省事,我在测试环境直接把本机防火墙对这三个端口放行。

2.2 用Docker Compose跑起GitLab

我习惯把GitLab的数据目录放在/srv/gitlab,这样后续备份直接打包目录就行。创建目录和配置文件:

sudo mkdir -p /srv/gitlab sudo chmod -R 755 /srv/gitlab

然后写一个docker-compose.yml:

version: '3.6' services: gitlab: image: gitlab/gitlab-ce:16.11.5-ce.0 container_name: gitlab restart: always hostname: gitlab.example.local environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://gitlab.example.local' gitlab_rails['time_zone'] = 'Asia/Shanghai' gitlab_rails['gitlab_shell_ssh_port'] = 2222 GITLAB_ROOT_PASSWORD: 'YourStrongPassword123' ports: - '80:80' - '443:443' - '2222:22' volumes: - /srv/gitlab/config:/etc/gitlab/gitlab - /srv/gitlab/logs:/var/log/gitlab - /srv/gitlab/data:/var/opt/gitlab shm_size: '256m'

这里有几个点要解释一下。GITLAB_ROOT_PASSWORD是初始化管理员密码,如果不在环境变量里设置,GitLab会随机生成一个24小时有效的临时密码,放在容器的/etc/gitlab/initial_root_password文件中。我建议显式设置,省得还要进容器找密码。external_url决定了网页访问地址,如果你用IP,就直接写http://192.168.x.x,后面配置Runner和Registry的时候都会用到这个地址。

注意SSH端口。GitLab内置的Git仓库支持SSH克隆,但容器内22端口不能直接映射到宿主机22,否则会和宿主机SSH冲突。所以我映射为2222:22,克隆地址会变成ssh://git@gitlab.example.local:2222/group/project.git,这个细节很容易踩坑。

启动命令很简单:

sudo docker compose up -d

首次启动会比较久,我实测大约3到5分钟。观察启动进度:

sudo docker logs -f gitlab

当日志中出现"puma server starting"或"GitLab is ready"时,就可以通过浏览器访问了。如果打不开页面,大概率是内存不够或者端口被占用,解决办法后面章节会专门讲。

2.3 初始化配置:关闭注册、调整显示时区

第一次打开GitLab页面,用root账号登录。登录后我建议立刻做三件事:关闭公开注册、修改root显示名、确认SSH端口设置。

关闭注册的位置在管理后台:左下角Admin Area -> Settings -> General -> Sign-up restrictions,取消Sign-up enabled选项,保存。这一步很重要,因为如果不关闭,内网任何人都能注册账号,安全隐患很大。还有一些更细的设置,比如限制邮箱后缀、允许管理员审核注册,这些按团队需求开就行。

检查SSH端口:Admin Area -> Settings -> Network -> GitLab Shell,确认SSH端口显示为2222。如果这里与容器映射不一致,用户会发现克隆地址的端口号是错误的。建议直接改好,后续项目创建时会自动生成正确克隆地址。

到这里GitLab本身已经可用了,但还没有Runner,流水线是跑不起来的。

3. 安装与注册GitLab Runner

3.1 Runner执行器选型:shell还是docker

GitLab Runner是执行Pipeline任务的角色,GitLab本身不直接运行构建任务。Runner可以装在单独的机器,也可以和GitLab同机;但它和GitLab实例是通过网络通信的,Runner主动轮询GitLab领取任务。

Runner最关键的选择是执行器。最常用的是shell和docker。shell执行器直接在Runner所在机器上执行命令,优点是没有额外容器开销,缺点是依赖宿主机环境,如果上次构建改了系统库,下一次构建可能莫名其妙失败。docker执行器为每个Job动态创建一个容器执行命令,环境完全封装在镜像里,可重复性非常好,我选择它。

有些人会把宿主Docker Socket挂载给Runner容器,从而让Job里的docker命令直接操作宿主机Docker。这种做法效率高但风险也高,一旦流水线里有恶意代码,等于直接拿到了宿主机的Docker权限。我采用更稳妥的docker -in-docker模式:构建Job里通过Docker服务来执行docker build,Runner容器本身不挂载宿主Socket。

3.2 注册Runner的具体步骤

首先获取注册令牌。在项目主页进入Settings -> CI/CD -> Runners,展开Runner设置,会看到两个令牌:一个是项目专用令牌,一个是管理员级令牌。如果希望在多个项目复用Runner,使用管理员级令牌,在Admin Area -> CI/CD -> Runners查看。

Runner也可以用官方容器安装。先创建Runner的数据目录:

sudo mkdir -p /srv/gitlab-runner

然后执行注册:

sudo docker run --rm -it \ -v /srv/gitlab-runner/config:/etc/gitlab-runner \ gitlab/gitlab-runner:latest register \ --non-interactive \ --url http://gitlab.example.local \ --registration-token YOUR_PROJECT_TOKEN \ --executor docker \ --docker-image docker:24.0.7 \ --description "docker-runner" \ --tag-list "docker" \ --docker-privileged

解释几个参数:--url是GitLab网页地址,--registration-token是刚才复制的令牌,--executor docker表示采用Docker执行器,--docker-image docker:24.0.7表示Job容器默认使用这个基础镜像,--tag-list docker给Runner打标签,流水线Job里必须带上tags: [docker]才会被该Runner接收,--docker-privileged是docker -in-docker模式必需的,不然Job容器内部启动Docker Daemon会失败。

注册成功后,正式启动Runner:

sudo docker run -d \ --name gitlab-runner \ --restart always \ -v /srv/gitlab-runner/config:/etc/gitlab-runner \ gitlab/gitlab-runner:latest

回到GitLab后台的Runners页面,刷新就能看到这个Runner状态为online。注册时如果填错了URL或令牌,Runner会一直保持offline,最直接的排查方法就是看Runner容器的日志:

sudo docker logs gitlab-runner

3.3 Runner并发与标签配置

Runner默认并发数其实是0,需要手动设置。你的Runner配完后,如果不调并发,一次只能跑一个Job。我一般会编辑Runner配置文件/srv/gitlab-runner/config/config.toml,在顶层加:

concurrent = 4

然后重启Runner容器。这个并发数要根据服务器CPU和内存来定。GitLab本身吃内存,Runner构建又要吃资源,我建议刚开始不要超过2,避免机器内存爆掉。构建Job的日志默认会输出到GitLab网页,但容器内可能堆积日志文件,如果你发现磁盘占满,优先检查Runner容器的日志目录。

另外,自定义大量Runner时,标签体系要提前规划。比如backend,frontend,docker,shell,流水线里通过tags精确指定用哪台Runner,不要所有Runner共用同一个标签,否则构建环境不可控。

4. 编写HelloWorld项目与.gitlab-ci.yml

4.1 项目结构:一个最简单的Flask应用

为了让HelloWorld体现“构建+部署”的完整意义,我选择用Flask写一个返回当前时间的小服务。项目结构如下:

HelloWorld/ ├── app.py ├── requirements.txt ├── Dockerfile └── .gitlab-ci.yml

app.py代码非常简单:

from flask import Flask from datetime import datetime import os app = Flask(__name__) @app.route('/') def hello(): return f"Hello GitLab CI/CD, current time is {datetime.now()}" if __name__ == '__main__': port = int(os.environ.get('PORT', 5000)) app.run(host='0.0.0.0', port=port)

requirements.txt只需要一行:

Flask==3.0.0

Dockerfile:

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 5000 CMD ["python", "app.py"]

我把镜像基于python:3.11-slim,足够小,构建也快。业务代码只有两个文件,Pipeline的核心价值在于把这两个文件变成可运行容器,再发布到目标环境。

4.2 流水线结构:test、build、deploy三段式

.gitlab-ci.yml是整套CI/CD的核心。我建议刚上手时只拆三个阶段:test、build、deploy。test阶段跑单元测试,build阶段构建镜像并推送,deploy阶段通过SSH登录目标服务器拉取镜像运行。后续有需求再加static_analysis、performance_test等,不要一上来就搞十几段。

先放完整配置文件:

stages: - test - build - deploy variables: DOCKER_HOST: tcp://docker:2375 DOCKER_TLS_CERTDIR: "" DOCKER_DRIVER: overlay2 test: stage: test image: python:3.11-slim tags: - docker script: - pip install --no-cache-dir -r requirements.txt - python -m pytest app_test.py rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "main"' build: stage: build image: docker:24.0.7 services: - name: docker:24.0.7-dind alias: docker tags: - docker script: - echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin - docker build -t $CI_REGISTRY_IMAGE:latest . - docker push $CI_REGISTRY_IMAGE:latest rules: - if: '$CI_COMMIT_BRANCH == "main"' deploy: stage: deploy image: alpine:3.18 tags: - docker variables: DEPLOY_HOST: "192.168.1.100" DEPLOY_USER: "deployer" before_script: - apk add --no-cache openssh-client bash docker-cli - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh - ssh-keyscan $DEPLOY_HOST >> ~/.ssh/known_hosts 2>/dev/null script: - ssh $DEPLOY_USER@$DEPLOY_HOST " docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD && docker pull $CI_REGISTRY_IMAGE:latest && docker rm -f helloworld || true && docker run -d --name helloworld -p 8080:5000 $CI_REGISTRY_IMAGE:latest " rules: - if: '$CI_COMMIT_BRANCH == "main"'

这里有几个设计点需要说明。

test阶段我用了一个新文件app_test.py,简单验证首页返回状态码200:

from app import app def test_home(): client = app.test_client() resp = client.get('/') assert resp.status_code == 200

再把test阶段中的命令改为python -m pytest app_test.py,这样每次推代码都会先跑一次测试。

build阶段用到了GitLab内置的CI/CD变量。$CI_REGISTRY是GitLab内置仓库地址,$CI_REGISTRY_USER和$CI_REGISTRY_PASSWORD是当前用户自动生成的凭据,$CI_REGISTRY_IMAGE是项目对应的镜像名。这些变量不需要你自己配置,GitLab会自动注入。

deploy阶段是很多人出问题的地方。因为部署目标服务器和GitLab不一定在一个网络,我选择通过SSH远程执行命令。这里需要提前配置两个项目变量:SSH_PRIVATE_KEY是部署用户私钥,DEPLOY_HOST我已经写死在variables里,但私钥必须放到项目设置里,因为私钥不该提交到仓库。

4.3 项目变量与SSH私有密钥配置

进入项目Settings -> CI/CD -> Variables,新增两个变量:

变量名值是否需要保护
SSH_PRIVATE_KEY部署用户私钥原内容是
DEPLOY_HOST例如192.168.1.100否

我把SSH_PRIVATE_KEY设置为Masked和Protected。Masked表示在Job日志中自动隐藏,Protected表示只有受保护分支的Pipeline才能用。如果你的部署用户还没有密钥,先在目标服务器上生成一对:

ssh-keygen -t ed25519 -C "gitlab-deploy" -f ~/.ssh/id_ed25519

再把公钥追加到目标服务器~/.ssh/authorized_keys。测试环境下可以直接用root,但在生产环境建议创建一个低权限用户,只给他执行docker命令的权限。

部署脚本里有一个细节:echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -。如果私钥从网页复制粘贴,容易把换行变成CRLF,导致SSH密钥验证失败。加tr -d '\r'可以在注入前删掉回车符,这个坑我踩过好几次。

ssh-keyscan $DEPLOY_HOST >> ~/.ssh/known_hosts 2>/dev/null是为了跳过首次连接时指纹确认,不然SSH会卡在yes/no交互上。注意重定向到~/.ssh/known_hosts,不需要打印结果到日志。

5. 自动部署到目标服务器:两种实用方式

5.1 方式一:SSH远程部署,适合已有独立服务器

上面.gitlab-ci.yml里的deploy阶段就是方式一。流程是:Runner在容器里执行SSH命令,登录部署服务器,然后依次执行docker login、docker pull、删除旧容器、运行新容器。这样更新代码后,流水线自动完成整个发布过程。

有一个容易忽略的地方:目标服务器必须能访问GitLab Registry。如果你的GitLab地址是http://gitlab.example.local,Registry地址默认可能是相同的host,只是端口不同(我这里是5050)。部署服务器上Docker拉取私有镜像前,需要配置insecure registry。修改/etc/docker/daemon.json:

{ "insecure-registries": ["gitlab.example.local:5050"] }

然后重启Docker:

sudo systemctl restart docker

如果不配置,你会看到x509: certificate signed by unknown authority或http: server gave HTTP response to HTTPS client之类的报错。很多教程里没有提这一点,实际部署阶段会卡很久。

使用SSH方式部署时,还需注意目标服务器的端口和防火墙。如果部署服务用的是8080端口,记得放通目标服务器防火墙:

sudo ufw allow 8080/tcp

然后在浏览器访问http://目标服务器IP:8080,应该能看到Hello World和时间。

5.2 方式二:通过Docker Socket直接发布到同一台机器

如果你的目标和GitLab在同一台服务器,或者你只想快速演示,还有一种省事方式:在deploy阶段直接让Job调用宿主机Docker。但我们要注意安全性,不太建议把宿主Docker Socket挂给普通Runner。如果你只是内网学习,可以把Runner注册为shell执行器,然后在Job里直接写docker命令,用本地Docker代替远程Docker。优点是省去SSH凭证,缺点是不适合真实团队。

我更倾向的方式是:部署目标服务器安装一个轻量级管理工具,比如Portainer,通过API执行更新。这种方式适合团队内不止一个服务、希望统一管理所有容器的情况。不过部署逻辑会多一层,初学者建议先用SSH方案,等熟练了再往Agent或平台化方向演进。

5.3 验证发布结果

流水线跑完后,回到Pipeline页面,三个Job应该都显示绿色通过。点开deploy Job日志,可以看到SSH执行的过程,最后docker run的容器ID输出出来。再到部署服务器执行:

docker ps | grep helloworld

能看到容器处于Up状态。访问http://192.168.1.100:8080/,页面返回Hello GitLab CI/CD, current time is ...,说明全流程已经跑通。

这时可以做一次验证:修改app.py里的返回文本,提交到main分支,观察GitLab自动触发新Pipeline,几十秒后页面内容更新。这个过程比手动部署舒服太多,后续发布的痛点基本都解决了。

6. 实践中的坑与速查表

6.1 我踩过的5个典型问题

GitLab搭建和CI/CD跑通过程中,有几个问题几乎每个人都会遇到。我整理成表格,方便对照排查。

现象根本原因解决办法
GitLab启动后页面502,等待时间很久内存不足或首次初始化未完成至少4GB内存,加2GB swap,等待5分钟
Runner显示offline注册URL不对 / 令牌错误 / 网络不通查看Runner容器日志,用gitlab-runner list检查
Job一直pending,没有Runner接收Job的tags和Runner的tag不匹配在Job里添加正确的tags:,或给Runner加对应标签
docker build报 Cannot connect to the Docker daemon没有启用docker:dind service,或DOCKER_HOST未设置在Job的services中加docker:dind,设置DOCKER_HOST变量
deploy阶段SSH连接被拒绝私钥格式不对或known_hosts未生成用tr -d '\r'处理私钥,加ssh-keyscan到known_hosts

还有一个隐藏问题:GitLab Runner注册时使用了--docker-privileged,有些人担心特权模式不安全。在纯内网、流水线代码可信的场景下,这个设置是标准做法。但如果Runner要服务多个项目,其中某些项目不可信,我建议把Runner划成不同的标签,并用Kubernetes或Docker的security context限制权限,不要一刀切。

6.2 关于仓库、镜像与备份的几个建议

GitLab本身的数据很重要,一定要做备份。最简单的备份方式是把/srv/gitlab整个目录按天打包到另外一块磁盘或对象存储。也可以使用GitLab内置备份命令:

sudo docker exec gitlab gitlab-backup create

这一步会在容器内生成tar包,然后你需要把它复制出容器,放到独立存储上。只备份数据卷不备份配置文件是不完整的,我建议把config目录也纳入备份范围。数据库和配置都在,恢复才可靠。

镜像仓库里的镜像,虽然不是核心代码,但生产发布依赖它们。如果希望保留历史版本,记得在CI/CD变量里使用不同tag,比如$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA,不要只用latest覆盖。HelloWorld无所谓,正式项目必须这么做,否则出问题时找不到上一个可用版本。在构建阶段,可以把push两行改一下:

docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -t $CI_REGISTRY_IMAGE:latest . docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA docker push $CI_REGISTRY_IMAGE:latest

部署时也拉取对应提交版本,而不是latest,这样能精准回滚。

流水线里还可以加一个“手动部署”按钮,把deploy阶段的rules改成:

rules: - if: '$CI_COMMIT_BRANCH == "main"' when: manual

这样每次提交后不会自动部署,需要有人手动点击deploy Job才能发布。对于需要发布审批的团队,这一步很实用。不过纯演示场景我保持自动发布,直观看到效果。

整体跑通之后,你会觉得CI/CD并没有那么高不可攀。从一台服务器、一个Runner和二十行配置文件开始,已经可以自动化完成构建到部署的所有环节。我个人的体会是,真正值得投入的不是跑通一次,而是把流水线运行得稳定、安全。比如尽量使用不可变镜像、部署用户使用独立SSH密钥、Pipeline里不写明文密码,这些习惯越早养成越好。后续如果再扩展,可以把HelloWorld替换成微服务项目,加上SonarQube、堡垒机、Kubernetes这些能力,但核心心智模型不会变。希望这篇记录能帮你少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 18:06:06

OpenCV幽灵版本4.9.0真相:本地编译避坑与版本校验实战

简介:本资源为OpenCV 4.9.0官方版本的完整构建包(含contrib扩展模块),面向计算机视觉初学者、算法工程师及深度学习开发者,提供开箱即用的C开发支持,适用于图像处理、目标检测、特征匹配、工业质检与AR应用…

作者头像 李华
网站建设 2026/10/15 18:05:28

用C语言读写STL文件:二进制与ASCII格式解析实战

看到标题里的“stl”,估计不少人的第一反应是:C标准模板库?怎么又和C语言文件读写凑一块了?这里先澄清一下,我们要聊的是另一种STL——STereoLithography,也就是3D打印和CAD领域最常见的三角网格文件格式。…

作者头像 李华
网站建设 2026/10/15 18:02:48

网络排障必备的8个命令,从原理到实战一次讲透

搞网络维护或者经常帮人处理电脑问题的人,应该都见过这种场景:打开命令行,敲字母,回车,屏幕滚出一堆数字和英文,懂的人三秒定位问题,不懂的人只能截图到处问。其实这些网络命令就是操作系统自带…

作者头像 李华