自动化部署

实践用 GitHub 做自动化部署

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

2026年6月13日12 分钟阅读
实践用 GitHub 做自动化部署

这篇笔记记录一下我怎么用 GitHub Actions 做一个简单的自动部署。

我想要的效果其实很直接:

  1. 我在本地改完代码,提交并推送到 GitHub。
  2. GitHub 发现 main 分支有新代码,自动执行我写好的配置文件。
  3. GitHub 连接服务器,让服务器拉取最新代码。
  4. 服务器重新构建并启动项目。

这样以后改完代码,不用每次都手动登录服务器,再敲一遍 git pull 和启动命令。

GitHub Actions 是什么

GitHub Actions 可以理解为 GitHub 提供的自动化工具。我们在项目里放一个 YAML 配置文件,告诉 GitHub:

  • 什么时候执行,比如推送到 main 分支时。
  • 在什么机器上执行,比如 GitHub 提供的 Ubuntu 环境。
  • 具体做什么,比如连接服务器、拉代码、重新启动服务。

官方文档里的结构图比较直观:

GitHub Actions 的基本结构

一次自动化任务叫作一个 workflow。workflow 里可以有多个 job,每个 job 里再按顺序执行多个 step。

我们要写的配置文件放在项目的这个目录里:

text
.github/workflows/deploy.yml

我这次要做的流程

我这里用的是比较容易理解的一种方式:让 GitHub Actions 通过 SSH 登录服务器,然后在服务器上执行部署命令。

正在渲染流程图...

如果项目只是一个简单网站,最后一步可能是 npm run build。如果项目是 Docker 部署,最后一步通常会用 docker compose up -d --build。不同项目会有区别,前面的链路差不多。

第一步:服务器先准备好项目

先登录服务器,把项目放在一个固定目录里。比如:

bash
mkdir -p /srv/my-app
cd /srv/my-app
git clone git@github.com:你的用户名/你的仓库名.git .

如果仓库是私有的,服务器本身也要有权限从 GitHub 拉代码。后面准备好 deploy 用户以后,可以在服务器上为它生成一对 SSH 密钥:

bash
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 仓库的:

text
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 权限稳妥一些。

bash
sudo useradd -m -s /bin/bash deploy
sudo mkdir -p /srv/my-app
sudo chown -R deploy:deploy /srv/my-app

如果用 Docker,可以把这个用户加入 Docker 组:

bash
sudo usermod -aG docker deploy

注意:加入 Docker 组以后,这个用户的权限已经很高了。改完用户组后,要重新登录一次才会生效。

接着给 deploy 用户准备另一对登录密钥。这个密钥是给 GitHub Actions 登录服务器用的。适合在自己的电脑上生成,不要把私钥留在服务器上:

bash
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 的内容追加到服务器上的:

text
/home/deploy/.ssh/authorized_keys

私钥文件 github-actions-deploy 的完整内容后面放进 GitHub Secrets。私钥不要提交到仓库。

第三步:在 GitHub 里配置 Secrets

密码、私钥这些内容不要直接写进 YAML 文件,也不要提交到仓库。

到 GitHub 仓库的:

text
Settings -> Secrets and variables -> Actions

先进入仓库的 Settings:

GitHub 仓库的 Settings 入口

然后进入 Actions 的 Secrets 页面:

GitHub Actions Secrets 页面

创建这些 Repository secrets:

Secret 名称内容
SERVER_HOST服务器 IP 或域名
SERVER_PORTSSH 端口,一般是 22
SERVER_USER部署用户,比如 deploy
SERVER_SSH_KEYGitHub Actions 登录服务器使用的私钥
SERVER_KNOWN_HOSTS服务器的 SSH 主机公钥记录

SERVER_KNOWN_HOSTS 可以先在自己信任的环境里生成:

bash
ssh-keyscan -p 22 -H 你的服务器地址

把输出保存为 Secret。更严谨一点,可以先核对服务器上的 SSH 指纹,再保存结果。不要在自动部署脚本里每次临时执行 ssh-keyscan,否则第一次连接时没有真正验证服务器身份。

第四步:写 workflow 配置文件

在项目里新建:

text
.github/workflows/deploy.yml

内容可以先写成这样:

yaml
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,只保留手动触发:

yaml
on:
  workflow_dispatch:

第五步:提交代码并观察运行结果

把 workflow 文件提交到仓库:

bash
git add .github/workflows/deploy.yml
git commit -m "ci: add automatic deployment"
git push origin main

提交后,打开仓库里的 Actions 标签页:

GitHub 仓库的 Actions 入口

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

GitHub Actions workflow 运行图

我踩到的坑

1. 普通用户总是需要提权

这个是我最先碰到的问题。用 admin 用户登录以后,手动执行时觉得没问题,因为我可以随时输 sudo 密码。但是 GitHub Actions 是无人值守执行的,不能部署到一半停下来等我输入密码。

我的处理顺序是:

  1. 先用 root 手动跑一遍命令,确认问题是不是权限导致的。
  2. 再换成专门的 deploy 用户。
  3. 把项目目录、Docker 权限、需要重启的服务权限一次配好。
  4. 尽量不要让 GitHub Actions 长期直接使用 root

如果确实要用 sudo,可以只允许固定命令免密码执行,不要直接给一整套无限制权限。比如只允许重启某一个服务:

text
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart my-app

这条规则可以用 sudo visudo 放进单独的 sudoers 文件里。

2. 私钥格式不对

SERVER_SSH_KEY 要保存完整私钥,包括开头和结尾:

text
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----

少一行、多一个空格,或者复制错成公钥,SSH 都会失败。

3. 服务器能登录,但是 git pull 失败

这通常是把两套 SSH 密钥混在一起了。

GitHub Actions 能登录服务器,只能说明第一套密钥没问题。服务器还要有权限访问 GitHub 私有仓库,也就是前面提到的 Deploy key。

可以先登录服务器手动测试:

bash
cd /srv/my-app
git pull --ff-only origin main

4. 服务器目录里有人手动改过代码

如果服务器上直接改了代码,git pull --ff-only 可能会失败。我觉得这是好事,因为服务器上的代码最好只通过仓库更新。

真有临时修改,也应该在本地改完后提交到 GitHub,再让部署脚本拉下来。这样以后才知道线上到底跑的是哪个版本。

5. 日志里不要输出 Secret

GitHub 会对 Secrets 做脱敏,但最好还是不要主动打印。特别是私钥、Token、数据库密码,不要为了排错直接 echo 出来。

后面可以继续改进的地方

上面这一套已经能用,但还是比较基础。项目变复杂以后,可以继续加:

  • 在部署前先跑测试,测试通过后再部署。
  • production 环境加人工审批。
  • 部署失败时保留旧版本,方便回滚。
  • 用 tag 或 release 部署,不直接跟着每次 main 分支提交上线。
  • 给 workflow 加状态徽章,直接在 README 里看最近一次是否成功。