在传统的软件交付流程中,开发环境能跑通的服务到了测试环境就出问题,运维同学手工部署耗时又容易出错,这些痛点催生了 CI/CD 的普及。而 Docker 的出现,让构建产物从一个"包加一堆配置文档"变成了一个自包含的镜像,无论在哪台机器上运行,行为都完全一致。可以说,Docker 与 CI/CD 的结合,是 DevOps 体系中性价比最高的一次技术组合。这篇文章就来系统地讲讲 Docker 在 CI/CD 流水线中的具体用法。

Docker 在 CI/CD 流水线中到底解决什么问题
要理解 Docker 在流水线中的价值,得先看没有它的时候流程是什么样的。传统的 CI 流程通常是拉取代码、执行编译、跑测试,然后把构建出来的制品(jar 包、二进制文件)上传到制品仓库,再由部署脚本分发到各台服务器上。这个流程最大的问题在于:构建产物对运行环境有强依赖。一个 Java 应用可能依赖特定版本的 JDK,一个 Python 应用可能依赖系统级的 C 库,任何一处不匹配都会导致"在我机器上是好的"这种经典场景。
Docker 的核心价值就是把应用和它的运行环境一起打包。CI 阶段构建出来的不再是裸的制品,而是一个分层镜像,里面包含了操作系统依赖、运行时、依赖库和应用代码本身。这样无论这个镜像被调度到开发服务器、测试环境还是生产集群,运行行为都是确定的。这直接解决了环境漂移问题,也让 CD 阶段的回滚变得极其简单——只需要把上一个版本的镜像重新拉起来即可。
除此之外,Docker 还带来了隔离性和启动速度上的收益。CI 节点上跑的每一次构建任务都可以在独立的容器中进行,构建工具、依赖版本互不干扰,一台物理机可以安全地服务多个项目。相比虚拟机,容器秒级的启动速度也让流水线的整体耗时显著下降。这些特性叠加起来,使得 Docker 几乎成了 CI/CD 的事实标准。
编写适合 CI 场景的 Dockerfile:分层缓存与多阶段构建
在流水线中使用 Docker,第一步是写好 Dockerfile。很多团队直接把开发用的 Dockerfile 拿来跑 CI,结果发现每次构建都要十几分钟,原因通常出在缓存策略上。Docker 的镜像构建是分层进行的,每一条指令生成一层,只要某一层的输入没变,构建时就会直接复用缓存。利用这个机制,应该把变化频率低的内容放在前面,变化频繁的放在后面。
以 Node.js 项目为例,先复制 package.json 并安装依赖,再复制源码,这样只要依赖清单不变,即使代码天天改,npm install 那一层也能命中缓存,构建时间可以从十几分钟缩短到一两分钟。
FROM node:20-alpine WORKDIR /app # 先复制依赖清单,利用分层缓存 COPY package.json package-lock.json ./ RUN npm ci --only=production # 再复制源码 COPY . . CMD ["node", "server.js"]
第二个关键技术是多阶段构建。对于编译型语言,编译环境往往很重,比如 Go 需要完整的工具链,Java 需要 Maven 和 JDK,但运行阶段只需要一个精简的运行时。多阶段构建允许在一个 Dockerfile 中定义多个阶段,编译阶段产出二进制或 jar 包,运行阶段只把这个产物复制过来,最终镜像的体积可以缩小一个数量级。
# 第一阶段:编译 FROM golang:1.22 AS builder WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server # 第二阶段:运行 FROM alpine:3.19 COPY --from=builder /app/server /app/server EXPOSE 8080 ENTRYPOINT ["/app/server"]
上面的例子中,编译阶段的 Go 工具链、源码、缓存都不会进入最终镜像,最终镜像只有十几兆,拉取和启动都更快。在 CI 场景下,小镜像意味着更短的推送和拉取时间,这对流水线整体吞吐量的提升非常直接。
实战:在 Jenkins 和 GitHub Actions 中编排流水线
原理讲完,来看两条最常用的流水线配置。先看 Jenkins。Jenkins 中通常通过 Jenkinsfile 声明流水线,结合 Docker 插件可以让构建任务直接运行在容器里。一条典型的流水线包括拉代码、构建镜像、运行测试、推送镜像、部署五个阶段。
pipeline {
agent any
environment {
REGISTRY = 'registry.ipipp.com/myteam'
IMAGE = "${REGISTRY}/webapp:${env.BUILD_NUMBER}"
}
stages {
stage('构建镜像') {
steps {
sh 'docker build -t $IMAGE .'
}
}
stage('运行测试') {
steps {
sh 'docker run --rm $IMAGE npm test'
}
}
stage('推送镜像') {
steps {
withCredentials([usernamePassword(
credentialsId: 'registry-cred',
usernameVariable: 'USER',
passwordVariable: 'PASS')]) {
sh 'echo $PASS | docker login $REGISTRY -u $USER --password-stdin'
sh 'docker push $IMAGE'
}
}
}
}
}再看 GitHub Actions 的写法。它的优势是与代码仓库深度集成,配置文件就放在仓库的 .github/workflows 目录下,CI 节点自带 Docker 环境,不需要自己维护构建机。
name: ci-cd
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 构建镜像
run: docker build -t webapp:${{ github.sha }} .
- name: 登录镜像仓库
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.ipipp.com -u ${{ secrets.REGISTRY_USER }} --password-stdin
- name: 推送镜像
run: |
docker tag webapp:${{ github.sha }} registry.ipipp.com/myteam/webapp:${{ github.sha }}
docker push registry.ipipp.com/myteam/webapp:${{ github.sha }}两种方案的核心阶段是一样的:构建、测试、推送到镜像仓库。镜像仓库是 CI 和 CD 之间的衔接点,CI 只负责产出经过验证的镜像并打上不可变的标签(比如 git commit 哈希),CD 阶段再根据这个标签去拉取和部署。用 commit 哈希做标签而不是用 latest,可以保证部署的版本永远可追溯、可回滚。
常见踩坑点与优化建议
落地过程中有几个高频问题值得提前防范。第一个是密钥泄露。有些团队习惯在构建时用 Docker build 的 --build-arg 传递数据库密码或 API 密钥,这是很危险的,因为通过 build-arg 传入的值会留在镜像的历史层里,任何拿到镜像的人执行 docker history 都能看到。正确做法是把密钥通过环境变量或密钥管理服务在运行时注入,构建期只传非敏感配置。
第二个是缓存失效导致的构建变慢。常见原因是 COPY . . 把无关文件也复制进了构建上下文,比如 node_modules、日志文件,任何一个小改动都会让后续层缓存失效。解决办法是编写合理的 .dockerignore 文件,把不需要进镜像的内容排除掉,这既能加速构建又能减小镜像体积。
第三个是部署策略。CD 阶段直接 docker stop 旧容器再启新容器,会造成短暂的服务中断。对于有可用性要求的服务,应该采用滚动更新:新容器启动并通过健康检查后,再把旧容器下线。如果团队已经在用 Kubernetes,可以把 CI 产出的镜像交给 Deployment 管理,配合 readinessProbe 实现零停机发布;如果暂时没有 K8s,用 Docker Compose 配合 docker rollout 类的脚本或 Nginx 反向切换也能达到类似效果。
最后给一个实践清单:镜像标签永远用不可变的标识而不是 latest;Dockerfile 里只装运行必需的依赖;构建上下文配合 .dockerignore 裁剪;敏感信息一律运行时注入;部署前务必跑一遍容器级健康检查。把这几条落实到位,Docker 加持下的 CI/CD 流水线就能真正做到又快又稳,让团队把精力放回业务本身。