自动化部署
实践用 GitHub 做自动化部署
这篇笔记记录一下我怎么用 GitHub Actions 做一个简单的自动部署。 我想要的效果其实很直接: 1. 我在本地改完代码,提交并推送到 GitHub。 2. GitHub 发现 main 分支有新代码,自动执行我写好的配置文件。 3. GitHub 连接服务器,让服务器拉取最新代码。 4. 服务器重新构建并启动

这篇笔记记录一下我怎么用 GitHub Actions 做一个简单的自动部署。
我想要的效果其实很直接:
- 我在本地改完代码,提交并推送到 GitHub。
- GitHub 发现
main分支有新代码,自动执行我写好的配置文件。 - GitHub 连接服务器,让服务器拉取最新代码。
- 服务器重新构建并启动项目。
这样以后改完代码,不用每次都手动登录服务器,再敲一遍 git pull 和启动命令。
GitHub Actions 是什么
GitHub Actions 可以理解为 GitHub 提供的自动化工具。我们在项目里放一个 YAML 配置文件,告诉 GitHub:
- 什么时候执行,比如推送到
main分支时。 - 在什么机器上执行,比如 GitHub 提供的 Ubuntu 环境。
- 具体做什么,比如连接服务器、拉代码、重新启动服务。
官方文档里的结构图比较直观:

一次自动化任务叫作一个 workflow。workflow 里可以有多个 job,每个 job 里再按顺序执行多个 step。
我们要写的配置文件放在项目的这个目录里:
.github/workflows/deploy.yml我这次要做的流程
我这里用的是比较容易理解的一种方式:让 GitHub Actions 通过 SSH 登录服务器,然后在服务器上执行部署命令。
正在渲染流程图...
如果项目只是一个简单网站,最后一步可能是 npm run build。如果项目是 Docker 部署,最后一步通常会用 docker compose up -d --build。不同项目会有区别,前面的链路差不多。
第一步:服务器先准备好项目
先登录服务器,把项目放在一个固定目录里。比如:
mkdir -p /srv/my-app
cd /srv/my-app
git clone git@github.com:你的用户名/你的仓库名.git .如果仓库是私有的,服务器本身也要有权限从 GitHub 拉代码。后面准备好 deploy 用户以后,可以在服务器上为它生成一对 SSH 密钥:
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo -u deploy ssh-keygen -t ed25519 -f /home/deploy/.ssh/id_ed25519 -C "server-deploy-key"
sudo cat /home/deploy/.ssh/id_ed25519.pub然后到 GitHub 仓库的:
Settings -> Deploy keys -> Add deploy key把公钥加进去。服务器只需要拉代码的话,不要勾选写入权限。
这里有一个容易混淆的点:一共会用到两套 SSH 权限。
| 用途 | 谁连接谁 | 怎么配置 |
|---|---|---|
| GitHub Actions 登录服务器 | GitHub Actions -> 服务器 | 私钥放进 GitHub Secrets,公钥放进服务器的 authorized_keys |
| 服务器拉取私有仓库 | 服务器 -> GitHub | 私钥留在服务器,公钥添加为仓库的 Deploy key |
如果仓库是公开的,第二套密钥可以省掉,直接用 HTTPS 地址拉代码也可以。
第二步:准备一个专门部署的用户
我一开始用的是 admin 用户,后来发现每次部署都可能遇到提权问题。比如 Docker、写目录、重启服务,只要权限少一块,脚本就会卡住。
最省事的办法看起来是直接用 root。我自己排查问题时也会先用 root 验证一遍,因为这样很容易判断到底是不是权限问题。
但是正式长期使用时,最好还是单独建一个部署用户,比如 deploy,只给它部署需要的权限。这样比让 GitHub Actions 直接拿到服务器的 root 权限稳妥一些。
sudo useradd -m -s /bin/bash deploy
sudo mkdir -p /srv/my-app
sudo chown -R deploy:deploy /srv/my-app如果用 Docker,可以把这个用户加入 Docker 组:
sudo usermod -aG docker deploy注意:加入 Docker 组以后,这个用户的权限已经很高了。改完用户组后,要重新登录一次才会生效。
接着给 deploy 用户准备另一对登录密钥。这个密钥是给 GitHub Actions 登录服务器用的。适合在自己的电脑上生成,不要把私钥留在服务器上:
ssh-keygen -t ed25519 -f github-actions-deploy -C "github-actions-deploy"
ssh-copy-id -i github-actions-deploy.pub deploy@你的服务器地址如果电脑上没有 ssh-copy-id,就手动把 github-actions-deploy.pub 的内容追加到服务器上的:
/home/deploy/.ssh/authorized_keys私钥文件 github-actions-deploy 的完整内容后面放进 GitHub Secrets。私钥不要提交到仓库。
第三步:在 GitHub 里配置 Secrets
密码、私钥这些内容不要直接写进 YAML 文件,也不要提交到仓库。
到 GitHub 仓库的:
Settings -> Secrets and variables -> Actions先进入仓库的 Settings:

然后进入 Actions 的 Secrets 页面:

创建这些 Repository secrets:
| Secret 名称 | 内容 |
|---|---|
SERVER_HOST | 服务器 IP 或域名 |
SERVER_PORT | SSH 端口,一般是 22 |
SERVER_USER | 部署用户,比如 deploy |
SERVER_SSH_KEY | GitHub Actions 登录服务器使用的私钥 |
SERVER_KNOWN_HOSTS | 服务器的 SSH 主机公钥记录 |
SERVER_KNOWN_HOSTS 可以先在自己信任的环境里生成:
ssh-keyscan -p 22 -H 你的服务器地址把输出保存为 Secret。更严谨一点,可以先核对服务器上的 SSH 指纹,再保存结果。不要在自动部署脚本里每次临时执行 ssh-keyscan,否则第一次连接时没有真正验证服务器身份。
第四步:写 workflow 配置文件
在项目里新建:
.github/workflows/deploy.yml内容可以先写成这样:
name: Deploy to server
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: production
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Prepare SSH
env:
SERVER_SSH_KEY: ${{ secrets.SERVER_SSH_KEY }}
SERVER_KNOWN_HOSTS: ${{ secrets.SERVER_KNOWN_HOSTS }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$SERVER_SSH_KEY" > ~/.ssh/id_ed25519
printf '%s\n' "$SERVER_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/known_hosts
- name: Deploy
env:
SERVER_HOST: ${{ secrets.SERVER_HOST }}
SERVER_PORT: ${{ secrets.SERVER_PORT }}
SERVER_USER: ${{ secrets.SERVER_USER }}
run: |
ssh -i ~/.ssh/id_ed25519 \
-p "$SERVER_PORT" \
-o BatchMode=yes \
"$SERVER_USER@$SERVER_HOST" <<'EOF'
set -e
cd /srv/my-app
git pull --ff-only origin main
docker compose up -d --build
EOF这里做了几件事:
push.branches: main:推送到main分支时自动部署。workflow_dispatch:也允许我在 GitHub 页面上手动点一次运行。concurrency:同一时间只保留一个生产环境部署,避免短时间连续提交时互相干扰。set -e:其中一条命令报错后,脚本直接停止,不要假装部署成功。git pull --ff-only:只允许正常快进更新,服务器上如果有人手动改了代码,不会稀里糊涂地自动合并。docker compose up -d --build:重新构建并后台启动容器。不是 Docker 项目的话,换成自己的构建和启动命令。
如果暂时不想每次推送都部署,可以先删掉 push,只保留手动触发:
on:
workflow_dispatch:第五步:提交代码并观察运行结果
把 workflow 文件提交到仓库:
git add .github/workflows/deploy.yml
git commit -m "ci: add automatic deployment"
git push origin main提交后,打开仓库里的 Actions 标签页:

每次运行都可以点进去看日志。GitHub 也会给 workflow 生成运行图,哪个步骤失败会比较直观:

我踩到的坑
1. 普通用户总是需要提权
这个是我最先碰到的问题。用 admin 用户登录以后,手动执行时觉得没问题,因为我可以随时输 sudo 密码。但是 GitHub Actions 是无人值守执行的,不能部署到一半停下来等我输入密码。
我的处理顺序是:
- 先用
root手动跑一遍命令,确认问题是不是权限导致的。 - 再换成专门的
deploy用户。 - 把项目目录、Docker 权限、需要重启的服务权限一次配好。
- 尽量不要让 GitHub Actions 长期直接使用
root。
如果确实要用 sudo,可以只允许固定命令免密码执行,不要直接给一整套无限制权限。比如只允许重启某一个服务:
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart my-app这条规则可以用 sudo visudo 放进单独的 sudoers 文件里。
2. 私钥格式不对
SERVER_SSH_KEY 要保存完整私钥,包括开头和结尾:
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----少一行、多一个空格,或者复制错成公钥,SSH 都会失败。
3. 服务器能登录,但是 git pull 失败
这通常是把两套 SSH 密钥混在一起了。
GitHub Actions 能登录服务器,只能说明第一套密钥没问题。服务器还要有权限访问 GitHub 私有仓库,也就是前面提到的 Deploy key。
可以先登录服务器手动测试:
cd /srv/my-app
git pull --ff-only origin main4. 服务器目录里有人手动改过代码
如果服务器上直接改了代码,git pull --ff-only 可能会失败。我觉得这是好事,因为服务器上的代码最好只通过仓库更新。
真有临时修改,也应该在本地改完后提交到 GitHub,再让部署脚本拉下来。这样以后才知道线上到底跑的是哪个版本。
5. 日志里不要输出 Secret
GitHub 会对 Secrets 做脱敏,但最好还是不要主动打印。特别是私钥、Token、数据库密码,不要为了排错直接 echo 出来。
后面可以继续改进的地方
上面这一套已经能用,但还是比较基础。项目变复杂以后,可以继续加:
- 在部署前先跑测试,测试通过后再部署。
- 给
production环境加人工审批。 - 部署失败时保留旧版本,方便回滚。
- 用 tag 或 release 部署,不直接跟着每次
main分支提交上线。 - 给 workflow 加状态徽章,直接在 README 里看最近一次是否成功。