6月6日 20:23

Docker 容器怎么接入 CI/CD?GitHub Actions 和 GitLab CI 集成

代码提交后自动构建镜像、跑测试、部署——这就是 CI/CD 和 Docker 的结合。Docker 让构建环境一致、部署产物标准化,CI/CD 让这一切自动运行。这篇讲 Docker 在主流 CI/CD 平台中的集成方式。

Docker 在 CI/CD 中的三个角色

  1. 构建环境:CI Runner 本身跑在 Docker 容器里,保证每次构建环境一致
  2. 构建产物docker build 产出镜像,推到 Registry,部署时直接拉镜像
  3. 部署目标:生产环境拉镜像启动容器,不需要在服务器上装运行时

GitHub Actions + Docker

基本构建和推送

yaml
# .github/workflows/deploy.yml name: Build and Push Docker Image on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: | myorg/myapp:latest myorg/myapp:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max

cache-from: type=gha 用 GitHub Actions 的缓存加速构建——没有变化的层直接复用,不用重新构建。首次构建可能要 5 分钟,后续只要 1-2 分钟。

多阶段构建减小镜像

dockerfile
# 构建阶段 FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # 运行阶段——只有产物,没有源码和 devDependencies FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules CMD ["node", "dist/index.js"]

多阶段构建让最终镜像只有运行时需要的文件——从 1GB+ 缩小到 200MB 以下。

GitLab CI + Docker

Docker-in-Docker 构建

yaml
# .gitlab-ci.yml build: image: docker:24 services: - docker:24-dind variables: DOCKER_TLS_CERTDIR: "/certs" before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

docker:24-dind 服务启动一个 Docker 守护进程,让 CI 容器里可以执行 docker build。注意要加 DOCKER_TLS_CERTDIR 确保通信安全。

使用 GitLab Container Registry

GitLab 自带 Container Registry,不用额外配 Docker Hub:

yaml
script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

$CI_REGISTRY_IMAGE 是 GitLab 预设变量,指向当前项目的 Registry 地址。

镜像标签策略

bash
# 不推荐:只用 latest myapp:latest # 推荐:语义化版本 + Git SHA 双标签 myapp:1.2.3 myapp:sha-abc1234 # 回滚时指定版本 docker pull myapp:1.2.2
标签用途示例
latest默认最新版开发环境用
语义化版本生产部署1.2.3
Git SHA精确追溯sha-abc1234
分支名预览环境main, staging

生产环境永远用精确版本号或 SHA,不用 latest——latest 指向的镜像随时可能变,出了问题无法回滚。

部署阶段

SSH 部署到服务器

yaml
# GitHub Actions - name: Deploy to server uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: deploy key: ${{ secrets.SSH_PRIVATE_KEY }} script: | docker pull myorg/myapp:${{ github.sha }} docker stop myapp || true docker rm myapp || true docker run -d --name myapp \ --restart unless-stopped \ -p 3000:3000 \ myorg/myapp:${{ github.sha }}

docker-compose 部署

服务器上放一个 docker-compose.yml,CI 只需要触发 docker compose pull && docker compose up -d

yaml
# 服务器上的 docker-compose.yml services: app: image: myorg/myapp:latest ports: - "3000:3000" restart: unless-stopped
bash
# CI 部署脚本 ssh deploy@server "cd /app && docker compose pull && docker compose up -d"

零停机部署

上面的方式会短暂中断服务。用双容器切换实现零停机:

bash
# 1. 启动新容器在不同端口 docker run -d --name myapp-new -p 3001:3000 myorg/myapp:new-version # 2. 健康检查 until curl -f http://localhost:3001/health; do sleep 1; done # 3. 切换 Nginx 上游 # 更新 nginx upstream 指向 3001 docker exec nginx nginx -s reload # 4. 停掉旧容器 docker stop myapp-old && docker rm myapp-old # 5. 重命名新容器 docker rename myapp-new myapp-old

安全最佳实践

  • 不要用 root 跑容器:Dockerfile 里加 USER app
  • 扫描镜像漏洞docker scout cves myapp:latest 或 Trivy
  • 最小基础镜像:用 alpinedistroless 代替 ubuntu
  • Secret 不进镜像:数据库密码等通过环境变量或 Secret 注入,不写进 Dockerfile
  • .dockerignore 排除无关文件.gitnode_modules.env 不应该进构建上下文
shell
# .dockerignore .git node_modules .env *.md .dockerignore
标签:Docker