从“本地打镜像”到流水线
本地 docker build 打镜像、手动推到仓库、再 kubectl set image,在只有一两个服务时还能忍;服务一多,问题就变成:谁推的、用的哪个 commit、测试跑没跑、回滚到哪个版本。CI/CD 的价值不是“自动化炫技”,而是让每一次发布都可追溯、可重复。
GitHub Actions 对开源和小团队最友好:托管 runner、按仓库计费、和 PR 天然集成。下面这条流水线覆盖构建、测试、打镜像、推 GHCR,改一改就能用于私有仓库。
一条工作流的骨架
name: build-and-push
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
permissions:
contents: read
packages: write
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Build and test
run: mvn -B -ntp verify
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha
- uses: docker/build-push-action@v6
with:
context: .
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
几个设计点值得解释:
permissions只给最小集合。推 GHCR 需要packages: write,读代码需要contents: read,不给write-all。concurrency在同一个分支上取消旧构建,避免连续 push 时多个镜像同时推。push在 PR 时为 false,只验证构建;合并到 main 或打 tag 才推送。metadata-action自动生成main、sha-xxxx、v1.2.3多组标签,回滚时按 sha 拉取最准确。cache-from/to: type=gha复用层缓存,Java 镜像的依赖层能省下大量时间。
多阶段 Dockerfile
CI 里跑完 mvn verify,镜像构建就应该跳过测试,只负责打包:
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build
COPY pom.xml .
RUN mvn -B -ntp dependency:go-offline
COPY src ./src
RUN mvn -B -ntp package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /build/target/*.jar app.jar
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"]
先拷 pom.xml 再拷源码,是为了让依赖层在源码变化时仍能命中缓存。运行镜像只保留 JRE,体积能小一半以上。alpine 用的是 musl,个别 native 库可能不兼容,遇到 UnsatisfiedLinkError 就换 ubuntu 或 distroless 基础镜像。
安全:权限、密钥和供应链
第一,能用 GITHUB_TOKEN 就不要建长期 PAT。需要访问云厂商时优先用 OIDC 联合身份,把 id-token: write 加上,让云平台临时颁发凭证,避免把 AK/SK 塞进 Secrets。
第二,第三方 action 尽量锁到 commit SHA,而不是 @v4 这种浮动标签。标签可以被移动,SHA 不会。配合 Dependabot 定期升级,安全性和可维护性都能兼顾。
第三,日志里不要打印环境变量和命令行参数。GitHub 会对已知 secret 做掩码,但拼接后的字符串、base64 之后的内容不一定能识别。set -x 在调试完一定要去掉。
第四,镜像仓库的可见性要确认。GHCR 的包默认继承仓库权限,公开仓库的包可能被匿名拉取,私有镜像记得检查 package 设置。
第五,生产发布建议再套一层 GitHub Environment,配置 required reviewers 和分支保护,把“合并即上线”改成“合并后人工确认”。个人项目可以省,团队项目这一步能挡住大部分误操作。
常见坑与排查
坑一:403 推不上去。 90% 是 permissions 没给 packages: write,或者 workflow 用的是只读的 fork PR 上下文。PR 里不推镜像,合并后再推,能避开一大半问题。
坑二:缓存不生效。 cache-from: type=gha 要求 buildx 已初始化,且缓存 key 和构建上下文匹配。用 mode=max 才能缓存中间层,默认的 min 只缓存最终层。
坑三:架构不匹配。 在 arm64 的 runner 上构建,推到 amd64 节点会报 exec format error。要么固定 platforms: linux/amd64,要么用 QEMU 做多架构,但构建时间会明显变长。
坑四:Docker 上下文太大。 把 target/、.git/、node_modules/ 都打进上下文,构建几分钟都卡在传输。写 .dockerignore,这是收益最高的一步。
坑五:tag 覆盖导致回滚困难。 只用 latest 时,回滚只能靠猜。至少保留 sha 标签,生产环境用不可变 tag,配合 Deployment 的 image 字段做精确回滚。
排查步骤
- 先看失败在哪一步,是编译、测试、构建还是推送。
- 编译失败本地复现
mvn -B -ntp verify,确认不是 CI 环境差异(JDK 版本、编码、时区)。 - 推送失败查
permissions、登录步骤和包可见性,用docker manifest inspect确认镜像是否真的推上去了。 - 构建慢看缓存命中和
.dockerignore,不要一上来就加机器。 - 回滚时用
kubectl set image指到旧的 sha 标签,确认滚动更新完成再观察指标。 - Actions 本身出问题时,用
act在本地跑一遍关键 job,能快速区分是 workflow 写错还是 runner 环境问题。
评论