使用 GitLab CI/CD 的 Docker Runner 时,经常会看到类似日志:
Using Docker executor with image alpine:3.20 ...
$ apk add --no-cache openssh-client rsync
fetch https://dl-cdn.alpinelinux.org/...
Installing openssh-client...
Installing rsync...
每次流水线都要重新下载和安装软件包,既浪费时间,也容易受到网络波动影响。
本文介绍如何制作一个预装依赖的 CI 基础镜像,让流水线直接使用本地镜像,不再重复执行 apk add。
一、为什么每次都重新安装
Docker Runner 每次执行 Job 时,都会创建一个新的临时容器。
如果 CI 配置是:
image: alpine:3.20
before_script:
- apk add --no-cache openssh-client rsync
实际过程就是:
创建全新的 alpine 容器
→ 下载 APK 软件索引
→ 下载 openssh-client、rsync
→ 安装依赖
→ 执行部署
→ 销毁容器
解决思路是:
提前制作一个包含 SSH 和 rsync 的 Docker 镜像(如果你的cicd需要其他包,就换成其他包,这里以ssh和rsync为例)
→ CI 每次直接启动该镜像
→ 不再执行 apk add
二、适用条件
- GitLab Runner 使用
dockerexecutor。 - Runner 服务器已经安装 Docker。
- CI 中通过
image:指定运行镜像。
如果使用的是 shell executor,image: 不会生效,本文方案不适用。
三、确认 Runner 配置
登录 GitLab Runner 所在服务器:
ssh root@127.0.0.2
检查 Runner:
gitlab-runner list
docker version
检查 /etc/gitlab-runner/config.toml ,Docker Runner 的配置通常类似:
[[runners]]
name = "test"
executor = "docker"
[runners.docker]
image = "alpine:3.20"
pull_policy = ["if-not-present"]
volumes = ["/cache"]
这里最重要的是:
pull_policy = ["if-not-present"]
它表示:
本机有镜像 → 直接使用
本机没有镜像 → 尝试从镜像仓库下载
因此,只需要在 Runner 服务器上提前构建好镜像,CI 就可以直接使用。
四、创建部署基础镜像
1. 建立镜像目录
在 Runner 服务器执行:
mkdir -p /opt/gitlab-ci-images/deploy-alpine
cd /opt/gitlab-ci-images/deploy-alpine
2. 创建 Dockerfile
执行:
vim Dockerfile
进入 Vim 后:
- 按
a进入编辑模式。 - 粘贴下面的内容。
- 按
Esc。 - 输入
:wq并回车。
Dockerfile 内容:
FROM alpine:3.20
RUN apk add --no-cache openssh-client rsync
这个镜像以 alpine:3.20 为基础,并在构建阶段提前安装:
openssh-clientrsync
3. 构建镜像
执行:
docker build -t local/gitlab-deploy:alpine3.20-v1 .
参数含义:
docker build 构建镜像
-t 设置镜像名称和版本
local/gitlab-deploy 镜像名称
alpine3.20-v1 镜像版本
. 使用当前目录的 Dockerfile
4. 检查镜像
docker image inspect local/gitlab-deploy:alpine3.20-v1
查看镜像列表:
docker images | grep gitlab-deploy
5. 验证工具
docker run --rm local/gitlab-deploy:alpine3.20-v1 \
sh -c 'ssh -V && rsync --version | head -n 1'
预期能够看到 SSH 和 rsync 的版本信息。
五、修改 .gitlab-ci.yml
原来的 CI 配置可能是:
deploy-master:
stage: deploy
image: alpine:3.20
before_script:
- apk add --no-cache openssh-client rsync
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
修改为:
deploy-master:
stage: deploy
- image: alpine:3.20
+ image: local/gitlab-deploy:alpine3.20-v1
before_script:
- - apk add --no-cache openssh-client rsync
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
完整部署任务可以类似:
stages:
- deploy
deploy-master:
stage: deploy
image: local/gitlab-deploy:alpine3.20-v1
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
resource_group: production-deploy
variables:
DEPLOY_HOST: "127.0.0.2"
DEPLOY_USER: "root"
DEPLOY_PATH: "/data/my-project"
before_script:
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- |
if [ -n "$GITLAB_CICD_ED25519_PRIVATE_KEY_B64" ]; then
printf '%s' "$GITLAB_CICD_ED25519_PRIVATE_KEY_B64" |
base64 -d > ~/.ssh/gitlab_cicd_ed25519
else
echo "缺少部署私钥"
exit 1
fi
- chmod 600 ~/.ssh/gitlab_cicd_ed25519
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
- export DEPLOY_SSH="ssh -i ~/.ssh/gitlab_cicd_ed25519 -o IdentitiesOnly=yes"
script:
- $DEPLOY_SSH "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$DEPLOY_PATH'"
- |
rsync -az --delete \
-e "$DEPLOY_SSH" \
--exclude='.git/' \
--exclude='.env' \
--exclude='node_modules/' \
./ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/"
environment:
name: production
六、提交并验证
修改 .gitlab-ci.yml 后提交:
git add .gitlab-ci.yml
git commit -m "优化 CI 部署镜像,避免重复安装依赖"
git push
新流水线日志应该变成:
Using Docker executor with image local/gitlab-deploy:alpine3.20-v1
Using locally found image version due to "if-not-present"
Executing "step_script" stage
$ mkdir -p ~/.ssh
不应再出现:
$ apk add --no-cache openssh-client rsync
fetch https://dl-cdn.alpinelinux.org/...
Installing openssh-client...
七、以后如何更新镜像
如果以后还需要安装 curl、bash、git 等工具,修改 Dockerfile:
FROM alpine:3.20
RUN apk add --no-cache \
bash \
curl \
git \
openssh-client \
rsync
使用新版本标签构建:
docker build -t local/gitlab-deploy:alpine3.20-v2 .
然后更新 .gitlab-ci.yml:
image: local/gitlab-deploy:alpine3.20-v2
建议使用 v1、v2 等固定版本,不要长期使用 latest。这样可以明确知道流水线使用了哪个镜像,也方便出现问题时回退。