GitHub Actions 自动构建和部署
用 CI 构建容器镜像,再通过受限 SSH 发布到单机 Compose 环境,加入并发控制、健康检查和回滚边界。
自动部署的目标不是让每次 push 都直接改生产,而是让发布过程可重复、可审计、失败可见。本文采用一个保守的单机流程:主分支变更先运行测试并构建不可变镜像,将镜像推送到 GHCR;受保护的 production environment 审批后,通过 SSH 在服务器拉取指定 tag 并重建 Compose 服务。
如果镜像仓库或部署平台不同,替换登录与传输步骤即可。示例使用第三方 GitHub Action 时必须审阅其代码和权限要求;正式环境应固定 action 到完整 commit SHA,并定期升级。
发布流水线结构
flowchart LR
push[主分支提交 / 手动发布] --> test[测试与静态检查]
test --> build[构建 OCI 镜像]
build --> scan[镜像扫描 / 生成 SBOM]
scan -->|通过| registry[(GHCR 不可变 tag)]
registry --> gate{production 环境审批}
gate --> deploy[SSH 到部署主机]
deploy --> pull[拉取指定镜像并更新 Compose]
pull --> health[等待 readiness / smoke test]
health -->|通过| done[记录发布版本]
health -->|失败| rollback[恢复上一个镜像 tag]
不要用 latest 表达某次发布。使用提交 SHA 作为镜像 tag,部署记录能回溯到源码;更严格的供应链场景还应验证签名和 provenance,并在生产节点以 digest 部署。
GitHub Actions 工作流示例
先在仓库的 Actions 设置中授权 packages: write,把生产密钥放在 production environment,并为该环境启用所需分支规则和审批。服务器应预装 Docker Compose 插件,且 SSH 用户只能执行部署所需操作。
# .github/workflows/deploy.yml
name: build-and-deploy
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: production-deploy
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
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=sha,format=long
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
steps:
- name: Configure SSH
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- name: Deploy image
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
IMAGE: ${{ needs.build.outputs.image }}
run: |
ssh "$DEPLOY_HOST" "cd /srv/leon-service && APP_IMAGE='$IMAGE' docker compose pull app && APP_IMAGE='$IMAGE' docker compose up -d --no-deps app"
ssh "$DEPLOY_HOST" "cd /srv/leon-service && docker compose ps app"
这是骨架而非复制即用的安全模板:把 @v4 等 action ref 换成经审阅的完整 commit SHA;按项目加入单元测试、静态分析、依赖扫描、镜像扫描和发布后的 smoke test。工作流权限从只读开始,仅在构建 job 授予写包权限。GHCR 若为私有包,生产主机需要安全配置只读拉取凭证。
SSH、主机和权限
在 GitHub Environment secrets 中配置 DEPLOY_HOST、DEPLOY_SSH_KEY 和预先核验的 DEPLOY_KNOWN_HOSTS。服务器身份 key 应通过可信渠道核验后保存;不要在 workflow 里运行 ssh-keyscan 后就直接信任结果,它只能拿到当前网络返回的 key,不能证明服务器身份。
部署账号应使用专用密钥和最小权限。更进一步,可通过 authorized_keys 的 command= 限定入口脚本,或使用短期凭证/云 OIDC 身份,减少长期 SSH 私钥。不要把 Docker socket 暴露给不可信用户;能控制 Docker daemon 的用户通常拥有主机级高权限。
失败检测与回滚
docker compose ps 仅展示状态,不足以证明新版本能够服务真实请求。生产工作流应等待 readiness endpoint 成功,再调用一个低风险 smoke test。示例:
for attempt in $(seq 1 20); do
if curl --fail --silent https://service.example.com/actuator/health/readiness >/dev/null; then
exit 0
fi
sleep 3
done
echo "readiness probe did not become healthy" >&2
exit 1
回滚最好由服务器侧脚本基于已记录的上一个镜像 digest 执行,避免把任意命令拼进工作流输入。回滚应用镜像并不自动回滚数据库 schema;使用向前兼容迁移,并将不可逆数据变更安排在独立步骤。
发布系统还应保留镜像与日志,输出部署的 commit SHA、环境、执行人、开始/结束时间和探测结果。对关键生产环境启用审批和部署并发锁,避免两次发布同时修改同一 Compose 项目。
常见踩坑
- 在
pull_request工作流里暴露生产 secrets:来自 fork 的 PR 不应获得部署密钥。构建与部署分成不同 job,并让部署只在可信分支和受保护环境运行。 - 把私钥写进命令行参数或日志:通过受控环境变量临时写入权限为
600的文件,避免set -x,运行结束后清理。 - 禁用 SSH host key 校验图省事:
StrictHostKeyChecking=no会使中间人攻击更容易;使用核验后的known_hosts。 - 部署 tag 是浮动值:同一个 tag 被覆盖会让回滚和审计失去确定性。使用 commit SHA tag,记录并最好校验 digest。
- 生产节点无条件执行
docker compose down:这会扩大停机范围并可能误删依赖服务。只更新目标服务;数据卷清理命令尤其要审查。 - 工作流取消导致状态未知:生产 job 设置明确的 concurrency 策略;每次部署都写入版本记录,并在 job 结束时检查服务健康。
- 过度相信 Actions 自动脱敏:日志拼接、编码后的密钥或结构化秘密可能无法正确脱敏。不要打印秘密,不要把秘密嵌进构建参数或 artifact。