用 GitHub Actions 自动部署 Astro 博客到服务器
- # ci-cd
- # github-actions
- # astro
- # deploy
这几天给博客接了一套 CI/CD:代码推到 GitHub 之后,GitHub Actions 自动安装依赖、检查类型、构建 Astro 静态文件,然后通过 SSH 把 dist/ 同步到自己的服务器。
这篇文章记录一下完整过程。里面涉及服务器地址、用户名、私钥、部署目录等内容,我都会用占位符表示,避免把敏感信息写进博客或仓库。
目标
我的博客是一个 Astro 静态站点,构建产物在 dist/ 目录里。理想的部署流程是:
- 本地提交代码并 push 到 GitHub
- GitHub Actions 自动执行检查和构建
- 构建成功后,通过 SSH 登录服务器
- 用
rsync上传新的dist/ - 切换服务器上的
current软链接,让 Nginx 指向新版本
这样以后写文章或者改页面,只需要正常提交代码,不需要手动登录服务器复制文件。
服务器目录
我采用了一个很常见的 release 目录结构:
DEPLOY_PATH/
├── current -> releases/<commit-sha>
└── releases/
├── <commit-sha-a>/
└── <commit-sha-b>/
其中 DEPLOY_PATH 是服务器上的部署根目录,例如:
/var/www/example.com
Nginx 不直接指向某个固定 release,而是指向:
DEPLOY_PATH/current
这样每次部署时,只需要上传到新的 release 目录,再把 current 软链接切过去。部署过程更清晰,也方便回滚。
Nginx 配置大致如下:
server {
listen 80;
server_name example.com;
root /var/www/example.com/current;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(css|js|png|jpg|jpeg|gif|svg|webp|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
这里的域名和路径都只是示例,实际部署时换成自己的即可。
SSH 密钥
GitHub Actions 需要一种方式登录服务器。比较合适的做法是生成一对专门用于部署的 SSH 密钥,而不是复用自己的日常登录密钥。
生成方式:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./id_ed25519 -N ""
生成后会得到两个文件:
id_ed25519 # 私钥,放到 GitHub Secrets
id_ed25519.pub # 公钥,放到服务器 authorized_keys
注意:
- 私钥不要提交到仓库
- 私钥不要写进博客
- 私钥不要截图发到公开地方
- 公钥虽然不是私密信息,但也没必要放进文章里
服务器上把公钥加入对应用户的 authorized_keys:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "<public-key>" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
这里有一个容易混淆的点:SERVER_USER 不是由密钥决定的,而是由公钥放在哪个服务器用户下面决定的。
如果公钥放在:
/home/deploy/.ssh/authorized_keys
那么 SERVER_USER 就是:
deploy
如果公钥放在:
/root/.ssh/authorized_keys
那么 SERVER_USER 就是:
root
我更推荐使用单独的 deploy 用户,而不是直接用 root。
GitHub Secrets
部署信息不应该直接写在 workflow 文件里,而是放到 GitHub Secrets。
需要配置这些变量:
SERVER_HOST # 服务器地址
SERVER_USER # SSH 登录用户
SERVER_PORT # SSH 端口,通常是 22
SERVER_SSH_KEY # 私钥完整内容
DEPLOY_PATH # 服务器部署根目录
其中 SERVER_SSH_KEY 要填私钥的完整内容,包括开头和结尾:
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
不要填 .pub 文件内容。.pub 是公钥,应该放在服务器上。
GitHub Actions Workflow
最终 workflow 长这样:
name: Deploy
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: deploy-production
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Enable Corepack
run: corepack enable
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm typecheck
- name: Lint
run: pnpm lint
- name: Build
run: pnpm build
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SERVER_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p "${{ secrets.SERVER_PORT }}" "${{ secrets.SERVER_HOST }}" >> ~/.ssh/known_hosts
- name: Prepare release directory
run: |
ssh -i ~/.ssh/deploy_key -p "${{ secrets.SERVER_PORT }}" \
"${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" \
"mkdir -p '${{ secrets.DEPLOY_PATH }}/releases/${{ github.sha }}'"
- name: Upload files
run: |
rsync -az --delete \
-e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SERVER_PORT }}" \
dist/ \
"${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ secrets.DEPLOY_PATH }}/releases/${{ github.sha }}/"
- name: Activate release
run: |
ssh -i ~/.ssh/deploy_key -p "${{ secrets.SERVER_PORT }}" \
"${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" \
"ln -sfn '${{ secrets.DEPLOY_PATH }}/releases/${{ github.sha }}' '${{ secrets.DEPLOY_PATH }}/current'"
这个 workflow 里有几个点值得注意。
首先,项目使用 pnpm,所以需要先执行:
corepack enable
然后 actions/setup-node 开启 pnpm 缓存:
cache: pnpm
其次,我把 typecheck、lint 和 build 都放在部署前面。这样如果代码本身有问题,部署会直接停止,不会把坏版本发布到服务器。
最后,部署不是直接覆盖线上目录,而是上传到:
releases/<commit-sha>
再切换:
current -> releases/<commit-sha>
这比直接 rsync dist/ current/ 更容易观察每次部署对应哪个 commit。
遇到的问题
这次接入过程中,主要遇到两个问题。
第一个是 GitHub Actions 关于 Node.js 20 的提示:
Node.js 20 is deprecated.
这不是项目代码里的 Node 版本,而是 GitHub Actions 本身的运行时提示。解决方式是升级官方 action 版本,比如使用:
uses: actions/checkout@v6
uses: actions/setup-node@v6
第二个是 SSH 登录失败:
Permission denied (publickey,...)
这个错误通常不是 workflow 写错了,而是 SSH 凭据没有配对成功。排查顺序可以是:
- GitHub Secret 里的
SERVER_SSH_KEY是否填了私钥,而不是公钥 - 服务器上对应用户的
authorized_keys是否包含匹配的公钥 SERVER_USER是否和公钥所在用户一致- 服务器上的
.ssh和authorized_keys权限是否正确 - 服务器是否允许该用户通过 SSH 公钥登录
简单说就是:私钥在 GitHub,公钥在服务器,用户名要对上。
安全边界
CI/CD 最大的风险不是 workflow 写得复杂,而是不小心把敏感信息暴露出去。
我这次刻意遵守了几个原则:
- 不把私钥提交到仓库
- 不把真实服务器地址写进文章
- 不把真实部署用户写进文章
- 不把真实部署路径写进文章
- 不在 workflow 里硬编码敏感信息
- 尽量使用权限较小的部署用户
如果以后需要进一步收紧权限,可以让 deploy 用户只拥有站点目录的写权限,不给它 sudo 权限。这样即使部署密钥泄露,影响范围也会小很多。
小结
这套流程接好之后,博客发布体验会顺很多:
写文章 -> git push -> GitHub Actions 构建 -> 自动部署到服务器
对个人博客来说,这已经足够轻量。没有引入额外平台,也没有复杂的发布系统,只是把 GitHub Actions、SSH、rsync 和 Nginx 这些成熟工具组合起来。
它不华丽,但很实用。以后发布文章时,少一次手动登录服务器,就少一次出错机会。