一、项目架构与流程
开发者 GitLab CI/CD Harbor
┌──────────────┐ ┌─────────────────────────┐ ┌──────────┐
│ 本地git 打tag │──push──▶│ GitLab CE 识别tag │ │ 镜像仓库 │
└──────────────┘ │ │ │ │ hb.reg.com│
│ ▼ │ └──────────┘
│ GitLab Runner(容器化) │ ▲
│ docker:27 工作容器 │ │ push
│ git clone → build │────────────┘
│ → push → 部署 │
└─────────────────────────┘
组件版本:
| 组件 | 版本 | 作用 |
|---|---|---|
| GitLab CE | 社区版 | 代码仓库 + CI/CD 调度 |
| GitLab Runner | 17.4.0 | 执行 CI job(docker executor) |
| 工作镜像 | docker:27 | job 运行容器(含 docker CLI) |
| Harbor | 2.15.2 | 私有镜像仓库 |
| 应用 | Flask + gunicorn | 演示 Web 服务 |
二、项目准备
1. 本地 Python 项目(qinyuxing.py)
一个极简 Flask 应用,用于演示 CI/CD 构建:
from flask import Flask, jsonify
import socket, datetime
app = Flask(__name__)
@app.route("/")
def index():
return f"<h1>qinyuxing-python-app</h1><p>{socket.gethostname()}</p>"
@app.route("/healthz")
def healthz():
return jsonify({"status": "ok"})
2. Dockerfile(关键:入口名要和 Flask 对象匹配)
FROM python:3.13-slim
COPY . /app
WORKDIR /app
RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ \
"gunicorn>=21.2.0" -r requirements.txt
EXPOSE 5000
CMD ["gunicorn", "-b", "0.0.0.0:5000", "qinyuxing:app", "--workers", "4", "--threads", "2"]
⚠️ 坑:
CMD里qinyuxing:app= 模块名:Flask对象名。如果文件叫qinyuxing.py、对象叫app,就写qinyuxing:app。别写成app:app(那是 app.py 才是)。
3. .gitlab-ci.yml(简洁版)
variables:
IMAGE_NAME: hb.reg.com/project/$CI_PROJECT_NAME
stages:
- build
- deploy
build_image:
stage: build
rules:
- if: $CI_COMMIT_TAG
script:
- docker login hb.reg.com -u admin -p Harbor12345
- docker build -t $IMAGE_NAME:$CI_COMMIT_TAG .
- docker push $IMAGE_NAME:$CI_COMMIT_TAG
- docker rmi -f $IMAGE_NAME:$CI_COMMIT_TAG
deploy_image:
stage: deploy
rules:
- if: $CI_COMMIT_TAG
script:
- docker pull $IMAGE_NAME:$CI_COMMIT_TAG
- docker rm -f $CI_PROJECT_NAME || true
- docker run --name $CI_PROJECT_NAME -d -p 5000:5000 $IMAGE_NAME:$CI_COMMIT_TAG
4. 触发方式:打 tag(不是 push 分支)
git add .
git commit -m "init"
git tag v1.0.0
git push origin main
git push origin v1.0.0 # 打tag推送才触发流水线
⚠️ 坑:
rules: - if: $CI_COMMIT_TAG只认 tag 。普通git push分支不会触发,必须git push origin <tag>。
三、搭建步骤
步骤1:部署 GitLab Runner(docker executor)
Runner 用容器跑,挂载宿主 docker.socket 实现 docker-outside-of-docker:
docker run -d --name gitlab-runner --restart always \
--add-host gitlab.test.com:192.168.211.251 \
--add-host hb.reg.com:192.168.211.101 \
--group-add 995 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/config:/etc/gitlab-runner \
gitlab/gitlab-runner:v17.4.0
--add-host给 Runner 主容器注入内网域名解析;--group-add 995让容器用户有 docker 组权限。
步骤2:注册 Runner(连 GitLab)
docker exec -it gitlab-runner gitlab-runner register
# 填 GitLab URL、token、executor=docker、image=docker:27
步骤3:配置 config.toml(extra_hosts 传给 work 容器)
[runners.docker]
image = "docker:27"
extra_hosts = ["gitlab.test.com:192.168.211.251", "hb.reg.com:192.168.211.101"]
步骤4:根治 Harbor 自签证书(关键!)
Harbor 用 HTTPS 服务,必须让所有客户端(web/runner)信任它的 CA,否则 docker push/pull 报 x509。正确做法是重新生成标准 CA 证书体系(而不是用 insecure 跳过校验):
① 在 Harbor 服务器(.101)生成标准 CA + 服务器证书
mkdir -p /data/harbor-cert
# 生成根 CA(CA:TRUE)
openssl genrsa -out /data/harbor-cert/ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.crt -subj "/C=CN/ST=Chongqing/L=Chongqing/O=Chengke/OU=Harbor/CN=harbor-ca" \
-addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
# 生成服务器密钥 + CSR(SAN 含域名和 IP)
openssl genrsa -out hb.reg.com.key 2048
openssl req -new -key hb.reg.com.key -out hb.reg.com.csr \
-subj "/C=CN/ST=Chongqing/L=Chongqing/O=Chengke/OU=Harbor/CN=hb.reg.com"
# 用根 CA 签发服务器证书(加 SAN)
cat > ext.cnf <<'EOF'
[server]
basicConstraints = CA:FALSE
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:hb.reg.com, DNS:localhost, IP:192.168.211.101
EOF
openssl x509 -req -in hb.reg.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out hb.reg.com.crt -days 3650 -sha256 -extfile ext.cnf -extensions server
# 验证证书链
openssl verify -CAfile ca.crt hb.reg.com.crt # 应显示 OK
② 把新证书配置到 Harbor 并重启
# 更新 harbor.yml 指向新证书
sed -i 's|certificate: .*|certificate: /data/harbor-cert/hb.reg.com.crt|' /usr/local/harbor/harbor.yml
sed -i 's|private_key: .*|private_key: /data/harbor-cert/hb.reg.com.key|' /usr/local/harbor/harbor.yml
# 同步到 nginx 容器实际读取的位置
cp /data/harbor-cert/hb.reg.com.crt /data/harbor/secret/cert/server.crt
cp /data/harbor-cert/hb.reg.com.key /data/harbor/secret/cert/server.key
# 重新生成并重启 Harbor
cd /usr/local/harbor && ./prepare && docker-compose down && docker-compose up -d
③ 把根 CA 分发到所有客户端(web .81/.82、runner .252)
mkdir -p /etc/docker/certs.d/hb.reg.com
echo "<harbor-ca.crt 的 base64>" | base64 -d > /etc/docker/certs.d/hb.reg.com/ca.crt
# 加入系统信任(curl 等用)
cp /etc/docker/certs.d/hb.reg.com/ca.crt /usr/share/pki/ca-trust-source/anchors/harbor-ca.crt
update-ca-trust
# 重启 docker 生效
systemctl restart docker
关键:服务器证书必须由 CA:TRUE 的根 CA 签发 (而不是自签 CA:FALSE)。客户端只需信任这个根 CA 即可,无需 insecure。验证:
openssl verify -CAfile ca.crt 服务器证书返回 OK。
四、踩坑大全(亲测,按轻重排序)
坑1:Runner 报 client version 1.43 is too old(最卡)
报错:
Preparation failed: Error response from daemon: client version 1.43 is too old.
Minimum supported API version is 1.44 (docker.go:956)
根因 :GitLab Runner 二进制内置的 docker client 库 API=1.43,连宿主机 dockerd(min API 1.44) 不兼容。 解决:宿主机 dockerd 降最低 API:
/etc/systemd/system/docker.service.d/override.conf:
[Service]
Environment="DOCKER_MIN_API_VERSION=1.40"
systemctl daemon-reload && systemctl restart docker
验证 :docker version 显示 minimum version 1.40。
⚠️ 坑:重启 docker 后,Runner 容器要用
docker restart gitlab-runner重启,否则挂载旧 socket,连到旧 dockerd。
坑2:无法解析主机 gitlab.test.com
根因 :job 的 work 容器(docker:27)没有内网域名映射,git clone 走公网 DNS 失败。 解决:
-
Runner 容器加
--add-host(上面的步骤1) -
config.toml 配
extra_hosts(把 hosts 传给每个 job 的 work 容器)
注意:work 容器不继承 Runner 容器的 --add-host,必须靠 Runner 的
extra_hosts配置。
坑3:docker push 报 x509: certificate signed by unknown authority
报错:
failed to fetch oauth token: Post "https://hb.reg.com/service/token":
tls: failed to verify certificate: x509: certificate signed by unknown authority
根因 :Harbor 原来的证书是自签且声明 CA:FALSE (自己签自己但声明"不是 CA")。docker 的 Go TLS 严格校验 CA:TRUE 的根 CA 签发的证书,自签 CA:FALSE 永远验证不过。
解决(根治):重新生成标准证书体系(见上文步骤4):
-
生成 CA:TRUE 的根 CA → 用它签发服务器证书(SAN 含域名+IP)
-
更新 harbor.yml + nginx 容器证书 + 重启 Harbor
-
把根 CA 分发到所有客户端(certs.d + 系统信任 + 重启 docker)
-
客户端
docker pull走正式 TLS 校验,无需 insecure
⚠️ 终极真相:自签证书如果 CA:FALSE,docker 的 Go TLS 必然拒绝 (要求证书链末端是 CA:TRUE 的根)。所以要么生成 CA:TRUE 根证书签发,要么用 insecure 跳过------前者是正确根治,后者是临时偷懒。
坑4:容器内 /etc/hosts 只读
现象 :job 里 echo "..." >> /etc/hosts 失败 → job exit 1。 根因 :容器启动后 /etc/hosts 只读。 解决 :不要用 echo 写 hosts,改用 Runner 的 extra_hosts 配置 (容器启动时注入)。若脚本里非写不可,用 tee -a(Alpine 下 /etc/hosts 可写的情况才有效,优先靠 extra_hosts)。
坑5:Alpine 命令差异
工作镜像 docker:27 是 Alpine:
-
update-ca-trust不存在 → 用update-ca-certificates -
精简,可能缺工具,脚本要兼容
坑6:Dockerfile 入口名
CMD ["gunicorn",..., "app:app"] 默认找 app.py。若你的文件是 qinyuxing.py,必须写成 qinyuxing:app。
坑7:镜像路径(pull 404 / NOT_FOUND)
现象 :docker pull hb.reg.com/myapp/myapp:1.0 报 401 Unauthorized / project not found。 根因 :Harbor 没有 myapp 这个项目 (只有 cicd/k8s/library/project)。你 CI/CD push 到的其实是 project 项目。 解决 :pull 正确的镜像路径 hb.reg.com/project/python-cicd:<tag>(和你 .gitlab-ci.yml 的 IMAGE_NAME 一致)。
五、最终验证
1. 本地手动测试 build + push(在 .252 上)
docker build -t hb.reg.com/project/cicdtest:v1 .
docker push hb.reg.com/project/cicdtest:v1 # 最后能看到 digest 即成功
2. GitLab 流水线
打 tag 推送 → GitLab Pipeline 里看到 build/deploy 两个 job 绿勾 → Job succeeded
3. 确认镜像进 Harbor + web 拉取
# 在 web 机器上
echo Harbor12345 | docker login hb.reg.com -u admin --password-stdin
docker pull hb.reg.com/project/python-cicd:<tag> # 成功 = 证书+权限全通
六、正确配置速查
| 配置 | 值 |
|---|---|
| Runner 版本 | gitlab/gitlab-runner:v17.4.0 |
| 工作镜像 | docker:27 |
| Runner extra_hosts | ["gitlab.test.com:...", "hb.reg.com:..."] |
| Harbor CA | 根 CA(harbor-ca,CA:TRUE)签发服务器证书 |
| 客户端信任 | /etc/docker/certs.d/hb.reg.com/ca.crt + update-ca-trust |
| 触发方式 | git push origin <tag> |
| 镜像地址 | hb.reg.com/project/python-cicd:<tag> |
| 错误镜像名 | 别用不存在的项目,如 myapp/myapp(Harbor 无此项目) |
七、一句话总结
-
触发 :打 tag 推送(
rules: if: $CI_COMMIT_TAG) -
执行 :GitLab Runner 拉
docker:27容器跑login → build → push -
最坑:①Runner 内置 docker client 太旧(降 dockerd API)②Harbor 证书自签 CA:FALSE(重新生成 CA:TRUE 根证书)
-
经验 :Harbor 证书要根治(生成标准 CA 签发),别用 insecure 临时跳过;client 需信任根 CA
本文记录了从零搭建 GitLab CI/CD + Harbor 镜像仓库的完整过程,含 Runner 容器化部署、docker executor 配置、Harbor 自签证书根治(生成 CA:TRUE 根 CA 签发服务器证书)、客户端 CA 分发,以及 7 个真实踩坑(docker client 版本、hosts 解析、x509 证书、只读 /etc/hosts、Alpine 命令差异、Dockerfile 入口名、镜像路径错误)。