CI/CD 自动部署流水线:GitHub Actions 推送到服务器
先讲一个真实场景。某团队四个人维护一个日活两万的社区站,部署一直靠"负责人手动 SSH 上去 git pull 再重启"。一次周一早上,有人把一段没跑过测试的代码合并进了 main,十点上线后线上报错,等他们发现已经是午休后,那一整个上午的转化都受影响。事后复盘,问题不在代码,而在流程:发布依赖人、没有自动化、没有回滚。
CI/CD 的最终目标是"代码合并后自动发布到生产"。对大多数跑在 VPS 或云服务器上的中小项目,最常见的落地方式是:GitHub Actions 在云端构建,通过 SSH 把产物推送到服务器,再在服务器上执行部署命令。这也是Git 自动部署工作流的自动化升级版。
一、整体架构
push main
→ GitHub Actions(构建、测试)
→ scp 上传构建产物到服务器
→ SSH 执行部署脚本(rsync / 拉镜像 / 重启容器)
→ 健康检查
这条链路里,CI 负责"可重复地构建出产物",服务器只做"替换产物并重启",职责清晰,出问题容易定位。
二、准备工作:SSH 密钥与 Secrets
- 在服务器上创建专用部署用户,并生成密钥对:
adduser deployer
mkdir -p /var/www/app && chown -R deployer:deployer /var/www/app
ssh-keygen -t ed25519 -a 200 -f ~/.ssh/deploy_key
cat ~/.ssh/deploy_key.pub | ssh deployer@server 'cat >> ~/.ssh/authorized_keys'
-
把私钥与连接信息存入 GitHub Secrets(不要写进仓库):
DEPLOY_KEY、SERVER_HOST、SERVER_USER、SERVER_PORT。 -
建议限制部署用户只能操作目标目录,比如用
sudo -u或把可执行命令白名单化,避免密钥泄露后攻击者能横向操作整台服务器。服务器初始化可参考服务器初始化脚本。
三、Workflow:SCP 上传 + SSH 执行
以下示例使用社区常用的 appleboy/scp-action 与 appleboy/ssh-action:
name: Deploy to Server
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload via SCP
uses: appleboy/scp-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.DEPLOY_KEY }}
port: ${{ secrets.SERVER_PORT }}
source: "dist/*"
target: /var/www/app/dist
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.DEPLOY_KEY }}
port: ${{ secrets.SERVER_PORT }}
script: |
cd /var/www/app
systemctl reload nginx
curl -fsS http://127.0.0.1/healthz || exit 1
几个关键点:
- 用 SSH 密钥而非密码,私钥只存在于 GitHub Secrets,日志里也不会暴露口令;
source只上传构建产物(dist/*),避免覆盖服务器上的其他文件;- 部署后执行健康检查,失败则让流水线标红,不让坏版本"静默上线"。
为了让"标红"之后还能快速恢复,建议在服务器上维护一个可回滚的部署方式。以静态站为例,保留最近两次构建,用软链切换当前版本:
# 服务器上的 deploy.sh(由 SSH 步骤调用)
VERSION=$1
ln -sfn /var/www/releases/$VERSION /var/www/current
systemctl reload nginx
curl -fsS http://127.0.0.1/healthz || {
ln -sfn /var/www/releases/$((VERSION-1)) /var/www/current
systemctl reload nginx
exit 1
}
这样"新版本失败自动回退上一版",比直接覆盖更稳。无论流程重跑多少次,软链切换都是幂等的,服务器不会进入"半部署"状态;把"部署"与"验证"拆成独立步骤,验证失败时也只需重跑验证,不必重复上传。
四、容器化部署
如果目标服务器跑 Docker,SSH 脚本改为在服务器上构建/拉取镜像并滚动重启容器:
script: |
cd /opt/app
docker compose pull web
docker compose up --no-deps -d web
sleep 5
curl -fsS http://127.0.0.1:8080/healthz || exit 1
这种做法配合 Docker Compose 生产部署 中的健康检查与重启策略,可以实现基本自动化的滚动发布。更完善的蓝绿/金丝雀策略见零停机部署策略。
五、部署方式对比
| 方式 | 自动化程度 | 回滚速度 | 适合 |
|---|---|---|---|
| 手动 SSH + git pull | 低 | 慢 | 起步阶段 |
| Actions + SCP/rsync 静态产物 | 高 | 中(软链秒切) | 静态站、前端 SPA |
| Actions + 镜像仓库 + 容器重启 | 高 | 中(镜像即版本) | Node/Python 服务、多实例 |
| 完整 K8s / ArgoCD | 极高 | 高(自动回滚) | 大规模微服务 |
对大多数中小项目,第二、三行已经足够,不必为了"上 CD"而引入 K8s。
六、密钥安全
- 密钥永远存在 GitHub Secrets,workflow 里用
${{ secrets.XXX }}引用; - 不给部署用户 root 权限,限制目标目录写权限;
- 建议定期轮换部署密钥(例如 90 天),并在服务器上移除旧的
authorized_keys条目; - 更先进的方案是用 OIDC 短时令牌替代长期密钥,见Docker 支持 GitHub Actions OIDC。
七、进阶:构建镜像推送到仓库
规模稍大时,更稳妥的做法是:Actions 构建 Docker 镜像 → 推送到镜像仓库(GHCR/Docker Hub)→ 服务器只负责拉取并重启。好处是构建与运行环境彻底分离、产物可追溯:
- name: Build and push image
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/yourorg/app:${{ github.sha }}
镜像的 SHA 就是版本号,回滚 = 重新拉上一个 tag,排查问题时也能精确定位到某次提交。
八、常见问题
Q:Actions 报 "Permission denied (publickey)"? 多半是密钥没配好:确认私钥完整粘贴进 DEPLOY_KEY、服务器 authorized_keys 权限为 600、部署用户的家目录不能被其他用户写。
Q:每次部署都很慢? 先看是否每次全量重装依赖。用 actions/cache 缓存 node_modules 或 pnpm store,能省下一大半安装时间;同时用 concurrency 保证同一分支只有一个部署在跑。
Q:部署完线上没变化? 检查上传的 target 目录是否和 Web 服务器配置的根目录一致,以及 Nginx/Apache 是否真的 reload 成功。
九、与 GitLab 对比
使用 GitLab 的团队可参考GitLab CI/CD 最佳实践,两种平台思路一致:构建在 CI 侧,部署靠 SSH/容器。完整的流水线概念见网站 CI/CD 流水线搭建,GitHub Actions 入门见GitHub Actions CI/CD 配置教程。
16IDC 观察
"GitHub Actions + SSH"是中小项目性价比最高的自动部署形态:不引入 K8s、不维护自建 CI 服务器,就能把"手工 SSH 上传、手工重启"变成"push 即发布"。它最大的价值不是省去那几分钟,而是让发布过程可重复、可追溯、可回滚。从 SCP 上传起步,再逐步演进到容器镜像 + 滚动发布,是一条稳妥的成长路径。
参考:https://docs.github.com/en/actions 、https://github.com/appleboy/scp-action 、https://github.com/appleboy/ssh-action