共计 7974 个字符,预计需要花费 20 分钟才能阅读完成。
文章目录 [显示]
Docker 30 天实战系列 · 第 22 天
你有没有经历过这样的场景:代码写完了,本地测试也过了,然后开始手动构建镜像、打 tag、推到镜像仓库。做了三遍之后发现——第一次忘了改版本号,第二次推错了仓库,第三次终于对了,但已经浪费了一个小时。
更惨的是,团队里每个人构建出来的镜像居然不一样,因为大家本地环境不同。这种 " 在我机器上没问题 " 的噩梦,从开发阶段一直延续到了部署阶段。
今天我们就来彻底解决这个问题。用 GitHub Actions 搭建一条自动化流水线,让代码推上去之后,构建、测试、推镜像全部自动完成。你只管写代码,剩下的交给机器。
本文你将学到
- GitHub Actions 的核心概念和工作原理
- 编写完整的 Docker 构建推送 workflow
- 多架构镜像构建(同时支持 x86 和 ARM)
- 构建缓存优化,让 CI 速度翻倍
- 实战调试 workflow 的常用技巧
阅读时间 : 约 12 分钟
实操时间 : 约 30 分钟
难度等级 : 中级(需要有 Docker 基础和 GitHub 账号)
什么是 CI/CD
先说说这两个缩写。CI 是 Continuous Integration(持续集成),CD 是 Continuous Delivery/Deployment(持续交付 / 部署)。
打个比方:你开了一家面包店。
- CI 就像是你的自动和面机——面粉倒进去,自动揉面、发酵、检查面团质量。每次有新面粉(代码)进来,机器自动帮你处理好,发现面粉有问题(测试失败)立刻报警。
- CD 就是和面之后自动送进烤箱、烤好了自动摆上货架。整个过程不需要人工介入。
对应到 Docker 的场景:
代码推送 → 自动运行测试 → 自动构建镜像 → 自动推送到镜像仓库 → 自动部署到服务器
CI 阶段 CD 阶段
GitHub Actions 核心概念
GitHub Actions 是 GitHub 内置的 CI/CD 平台,免费额度对个人项目完全够用。在动手之前,先理清几个核心概念:
┌─────────────────────────────────────────────┐
│ Workflow │
│ (.github/workflows/xxx.yml) │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Job 1 │ │ Job 2 │ │
│ │ (构建测试) │───→│ (推送镜像) │ │
│ │ │ │ │ │
│ │ ┌────────┐ │ │ ┌────────┐ │ │
│ │ │ Step 1 │ │ │ │ Step 1 │ │ │
│ │ │ 拉代码 │ │ │ │ 登录仓库│ │ │
│ │ └────────┘ │ │ └────────┘ │ │
│ │ ┌────────┐ │ │ ┌────────┐ │ │
│ │ │ Step 2 │ │ │ │ Step 2 │ │ │
│ │ │ 跑测试 │ │ │ │ 推镜像 │ │ │
│ │ └────────┘ │ │ └────────┘ │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────┘
- Workflow(工作流):一个 YAML 文件,定义了整个自动化流程
- Job(任务):Workflow 里的一组步骤,跑在同一台虚拟机上
- Step(步骤):Job 里的单个操作,可以是运行命令或调用别人写好的 Action
- Action(动作):可复用的步骤模块,就像积木一样拼装使用
- Runner(运行器):实际执行任务的虚拟机,GitHub 免费提供
实操:搭建 Docker 自动构建流水线
第一步:准备项目
我们用一个简单的 Node.js 项目来演示。先创建项目结构:
mkdir docker-ci-demo && cd docker-ci-demo
git init
创建一个简单的应用 app.js:
const http = require('http');
const server = http.createServer((req, res) => {res.writeHead(200, { 'Content-Type': 'application/json'});
res.end(JSON.stringify({
status: 'ok',
version: process.env.APP_VERSION || '1.0.0',
timestamp: new Date().toISOString()
}));
});
server.listen(3000, () => console.log('Server running on port 3000'));
创建 Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY app.js .
EXPOSE 3000
CMD ["node", "app.js"]
第二步:编写基础 Workflow
创建 .github/workflows/docker-build.yml:
name: Build and Push Docker Image
# 触发条件:推送到 main 分支,或打 tag
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{github.repository}}
jobs:
build-and-push:
runs-on: ubuntu-latest
# 授予 GITHUB_TOKEN 权限,用于推送到 ghcr.io
permissions:
contents: read
packages: write
steps:
# 1. 拉取代码
- name: Checkout code
uses: actions/checkout@v4
# 2. 登录到 GitHub Container Registry
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{env.REGISTRY}}
username: ${{github.actor}}
password: ${{secrets.GITHUB_TOKEN}}
# 3. 提取镜像标签和标签
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{env.REGISTRY}}/${{env.IMAGE_NAME}}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
# 4. 构建并推送
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{github.event_name != 'pull_request'}}
tags: ${{steps.meta.outputs.tags}}
labels: ${{steps.meta.outputs.labels}}
这个 workflow 做了什么:
- 代码推送到 main 或打 tag 时触发
- 登录 GitHub 自带的容器镜像仓库(ghcr.io,免费的)
- 自动根据分支名、tag 名生成镜像标签
- 构建 Docker 镜像并推送
第三步:提交并触发
git add .
git commit -m "feat: add Docker CI/CD workflow"
git remote add origin https://github.com/ 你的用户名 /docker-ci-demo.git
git push -u origin main
推送之后,打开 GitHub 仓库页面,点击 Actions 标签页,就能看到 workflow 正在运行了。
预期输出(在 Actions 页面看到的日志):
Run docker/build-push-action@v5
Building Docker image...
#1 [internal] load build definition from Dockerfile
#2 [internal] load .dockerignore
#3 [1/2] FROM node:20-alpine
#4 [2/2] COPY app.js .
#5 exporting to image
Pushing ghcr.io/yourname/docker-ci-demo:main
Done!
进阶:多架构构建
现在越来越多人用 ARM 架构的机器(比如 Mac M 系列芯片、AWS Graviton),如果你只构建 x86 镜像,ARM 用户拉下来跑会很慢(需要模拟执行)。多架构构建让一个镜像同时支持 x86 和 ARM。
把 workflow 升级一下:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
# 新增:安装 QEMU,用于模拟 ARM 架构
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# 新增:安装 Buildx,支持多架构构建
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{env.REGISTRY}}
username: ${{github.actor}}
password: ${{secrets.GITHUB_TOKEN}}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{env.REGISTRY}}/${{env.IMAGE_NAME}}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
# 关键:指定目标平台
platforms: linux/amd64,linux/arm64
push: ${{github.event_name != 'pull_request'}}
tags: ${{steps.meta.outputs.tags}}
labels: ${{steps.meta.outputs.labels}}
核心变化只有三处:装 QEMU、装 Buildx、加 platforms 参数。就这么简单,你的镜像就同时支持 x86 和 ARM 了。
进阶:缓存优化
多架构构建有个问题——慢。因为要构建两个平台,时间直接翻倍。而且每次 CI 跑完虚拟机就销毁了,下次又要从头构建。
Docker 层缓存可以解决这个问题。思路是把构建缓存存到 GitHub Actions Cache 里,下次构建直接复用。
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{github.event_name != 'pull_request'}}
tags: ${{steps.meta.outputs.tags}}
labels: ${{steps.meta.outputs.labels}}
# 缓存配置
cache-from: type=gha
cache-to: type=gha,mode=max
就加了两行:cache-from 和 cache-to,指定用 GitHub Actions 的缓存(type=gha)。mode=max 表示缓存所有层,不只是最终层。
实际效果对比:
无缓存构建:约 3-5 分钟(多架构)有缓存构建:约 30-60 秒(依赖没变的情况下)
速度提升 5-10 倍,非常值得。
完整的生产级 Workflow
把上面所有内容整合起来,加上测试环节,这是一个生产可用的完整配置:
name: Docker CI/CD Pipeline
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{github.repository}}
jobs:
# 第一阶段:测试
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
# 第二阶段:构建并推送镜像(测试通过后才执行)build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{env.REGISTRY}}
username: ${{github.actor}}
password: ${{secrets.GITHUB_TOKEN}}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{env.REGISTRY}}/${{env.IMAGE_NAME}}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{steps.meta.outputs.tags}}
labels: ${{steps.meta.outputs.labels}}
cache-from: type=gha
cache-to: type=gha,mode=max
注意 needs: test 这行——构建任务依赖测试任务,测试不过就不构建。这就像工厂的质检环节,不合格的产品不允许出厂。
推送到 Docker Hub
如果你想推到 Docker Hub 而不是 ghcr.io,改动很小:
env:
REGISTRY: docker.io
IMAGE_NAME: 你的 DockerHub 用户名 / 你的项目名
# 登录步骤改成:- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{secrets.DOCKERHUB_USERNAME}}
password: ${{secrets.DOCKERHUB_TOKEN}}
需要在 GitHub 仓库的 Settings > Secrets and variables > Actions 里添加 DOCKERHUB_USERNAME 和 DOCKERHUB_TOKEN 两个密钥。Token 在 Docker Hub 的 Account Settings > Security 里生成。
调试 Workflow 的实用技巧
workflow 写错了怎么办?分享几个实用技巧:
1. 本地验证 YAML 语法
# 安装 actionlint(workflow 语法检查工具)brew install actionlint # macOS
# 或
go install github.com/rhysd/actionlint/cmd/actionlint@latest
# 检查语法
actionlint .github/workflows/docker-build.yml
2. 用 act 在本地跑 workflow
# 安装 act
brew install act # macOS
# 本地运行(需要 Docker)act push --job build-and-push
act 会用 Docker 模拟 GitHub Actions 的运行环境,不用每次都推代码触发了。
3. 加调试步骤
在 workflow 里临时加一步,打印环境信息:
- name: Debug info
run: |
echo "Event: ${{github.event_name}}"
echo "Ref: ${{github.ref}}"
echo "SHA: ${{github.sha}}"
docker version
docker buildx version
常见问题 Q&A
Q1: 构建时报 "denied: permission_denied",怎么回事?
大概率是权限配置问题。检查两个地方:一是 workflow 里 permissions 部分有没有 packages: write;二是仓库的 Settings > Actions > General 里,"Workflow permissions" 要选 "Read and write permissions"。如果推 Docker Hub,检查 Secrets 里的 Token 是否有 push 权限。
Q2: 多架构构建太慢了,有什么办法?
除了前面说的 cache-from/cache-to,还可以考虑把两个架构拆成两个并行 Job 分别构建,最后用 docker buildx imagetools create 合并成一个 manifest。这样两个架构同时构建,时间减半。另外,Dockerfile 优化也很关键——把不常变的层放前面,利用好层缓存。
Q3: 我想在 PR 的时候也构建镜像(但不推送),怎么做?
上面的配置已经支持了。关键在 push: ${{github.event_name != 'pull_request'}},PR 触发时 push 为 false,只构建不推送。这样可以在合并前验证 Dockerfile 有没有写坏。
小结
今天我们从零搭建了一条完整的 Docker CI/CD 流水线:
- 基础 workflow:代码推送自动构建、自动推镜像
- 多架构构建 :一份 Dockerfile 同时产出 x86 和 ARM 镜像
- 缓存优化 :利用 GitHub Actions Cache 大幅加速构建
- 生产级配置 :先测试再构建,质量门禁一个不少
自动化的好处不只是省时间。更重要的是一致性——每次构建都在相同的环境下执行,不会再出现 " 在我机器上没问题 " 的尴尬。而且整个过程有日志、可追溯、可审计,出了问题能快速定位。
明天是 Day 23,我们来聊聊私有镜像仓库。不是所有镜像都适合放在公共仓库里,公司内部的业务代码、包含敏感配置的镜像,都需要一个安全的私有仓库来存放。我们会搭建 Harbor 私有仓库,并把它接入今天的 CI/CD 流水线。