在之前的折腾中,我们的独立博客运行在远程 Linux 服务器上,采用 Hugo + Nginx (Docker Compose) 的架构。但日常写作若每次都直接在服务器上编辑或手动传输文件,既不方便实时预览,也存在权限与版本管理隐患。
本文记录一次完整的工程化演进:将博客项目映射至本地开发环境,配合本地 Hugo 实时热重载预览,并通过创建服务器专属受限账户与 GitHub Actions 流水线,实现只要 git push 即可全自动安全部署到生产服务器。
一、整体架构设计
演进后的整体工作流如下:
[ 本地工作站 (Windows) ]
│ hugo server -D (实时本地预览 http://localhost:1313)
│ git push origin main
▼
[ GitHub 仓库 (私有/公开) ]
│ 触发 GitHub Actions 自动化流水线
│ 容器内执行 hugo --minify 生成静态资源
│ 通过 SSH (专用 Ed25519 密钥) 登录远端
▼
[ 远程 Linux 服务器 ]
└── 受限用户 blog (UID 1000)
├── 仅拥有 /data/blog 读写与编译权限
├── 杜绝整机 root 权限泄漏风险
└── Nginx 容器自动挂载 public/ 目录,静态页面毫秒级生效
二、远程服务器端:最小权限安全加固
将部署权限交给自动化 CI/CD 工具时,绝不能直接提供 root 账户与私钥。最佳做法是创建仅能管理博客的普通系统用户。
1. 创建受限系统账户
在服务器上新建名为 blog 的普通账户:
# 创建普通用户 blog(不设置交互密码,仅允许公钥登录)
sudo useradd -m -s /bin/bash blog
# 创建 .ssh 密钥目录
sudo mkdir -p /home/blog/.ssh
sudo chmod 700 /home/blog/.ssh
2. 生成 GitHub 专用 Ed25519 部署密钥
专钥专用是安全运维的核心准则。我们在服务器上为 GitHub Actions 独立生成一对密钥:
# 生成专用密钥对
sudo ssh-keygen -t ed25519 -N "" -f /home/blog/.ssh/github_deploy_key -C "github-actions-blog-deploy"
# 将部署公钥加入 authorized_keys 认证列表
sudo cat /home/blog/.ssh/github_deploy_key.pub | sudo tee -a /home/blog/.ssh/authorized_keys
# 规范权限
sudo chmod 600 /home/blog/.ssh/authorized_keys
sudo chmod 600 /home/blog/.ssh/github_deploy_key
sudo chmod 644 /home/blog/.ssh/github_deploy_key.pub
sudo chown -R blog:blog /home/blog/.ssh
提示:如果在本地也希望通过
blog账户直推,只需把本地已有的公钥(如~/.ssh/id_ed25519.pub)也一并追加到/home/blog/.ssh/authorized_keys中即可。
3. 移交博客工作目录所有权
将博客部署目录的所有权由 root 完整移交给 blog 用户:
sudo chown -R blog:blog /data/blog
sudo chmod -R u=rwX,g=rX,o=rX /data/blog
因为 Hugo 是静态文件编译器,blog 用户拥有 /data/blog 后,即可直接运行 hugo --minify 将页面产物输出到 /data/blog/public/,整个编译和发布过程完全无需 root 权限。
4. 精确限制 Docker / Nginx 重载权限
由于将用户加入 docker 组等同于赋予其整机 root 权限(可挂载根目录逃逸),因此严禁给 blog 用户分配 docker 组。我们使用 sudoers 白名单,精确放行 Nginx 语法检查与热重载命令:
编辑 /etc/sudoers.d/blog-manage:
blog ALL=(ALL) NOPASSWD: /usr/bin/docker exec blog-nginx nginx -t, /usr/bin/docker exec blog-nginx nginx -s reload
执行语法检查:
sudo visudo -c -f /etc/sudoers.d/blog-manage
三、本地环境搭建与镜像
1. 安装 Hugo Extended
Hugo 的 PaperMod 主题依赖 Extended 版本以支持 Sass/SCSS 处理。
在 Windows 本地终端安装:
winget install Hugo.Hugo.Extended
验证安装:
hugo version
# 输出示例:hugo v0.167.0+extended windows/amd64
2. 镜像远端博客源文件
使用 ssh + tar 流管道将远端源码拉取到本地开发目录(排除生成物 public/):
mkdir -p blog && cd blog
ssh <your-server-alias> "tar -C /data/blog --exclude='public' --exclude='.hugo_build.lock' -cf - ." | tar -xf - -C .
3. 本地启动开发服务器
在本地 blog 根目录执行:
hugo server -D
- 本地访问:
http://localhost:1313 -D参数表示允许预览草稿 (draft: true)。- 当你在本地保存 Markdown 文件时,浏览器会自动热重载刷新页面。
四、GitHub Actions CI/CD 流水线配置
1. 编写工作流文件
在项目根目录下创建 .github/workflows/deploy.yml:
name: Deploy Blog to Remote Server
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: '0.146.0'
extended: true
- name: Build Static Site with Hugo
run: hugo --minify
- name: Deploy to Remote Server
env:
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_PORT: ${{ secrets.SSH_PORT || '22' }}
TARGET_DIR: '/data/blog'
run: |
if [ -z "$SSH_HOST" ] || [ -z "$SSH_USER" ] || [ -z "$SSH_KEY" ]; then
echo "::error::Missing required secrets. Please configure them in repository settings."
exit 1
fi
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "$SSH_KEY" | tr -d '\r' > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
# 收集主机指纹(不使用 ping)
ssh-keyscan -p "$SSH_PORT" "$SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null
# 增量安全同步源码与编译产物到服务器
rsync -avz --delete \
--exclude='.git' \
--exclude='.github' \
--exclude='.hugo_build.lock' \
-e "ssh -p $SSH_PORT -i ~/.ssh/id_rsa -o StrictHostKeyChecking=accept-new" \
./ "$SSH_USER@$SSH_HOST:$TARGET_DIR/"
echo "Deploy completed successfully! Blog is updated on remote server."
2. 在 GitHub 仓库配置 Secrets
进入 GitHub 仓库页面:Settings -> Secrets and variables -> Actions -> New repository secret,添加以下三项:
| Secret 键名 | 取值说明 | 获取方式 |
|---|---|---|
SSH_HOST | 服务器公网 IP 或解析域名 | 服务器网络配置 |
SSH_USER | blog | 填入受限账户名 blog |
SSH_PRIVATE_KEY | 专属部署私钥完整文本 | 服务器执行 sudo cat /home/blog/.ssh/github_deploy_key |
五、日常写作与发布流程
完成以上配置后,日常撰写博客将变得无比轻松:
- 新建文章:
hugo new posts/my-awesome-post.md - 本地实时预览:打开
hugo server -Dhttp://localhost:1313边写边看效果。 - 提交与自动发布:推送成功后,GitHub Actions 会在几十秒内自动完成代码拉取、静态编译并推送到远端 VPS,网站即刻更新上线!
git add . git commit -m "feat: 发布新文章:我的精彩分享" git push origin main
附录:解决 Git 执行时反复弹出 CredentialHelperSelector
现象描述
在 Windows 环境下运行 Git 命令(如 git push、git fetch)时,界面反复弹出窗口:
CredentialHelperSelector
“Select a credential helper: manager / wincred …”
原因分析
某些集成环境或便携版 Git(如 WorkBuddy 或 PortableGit)在系统的全局配置文件(etc/gitconfig)中默认注册了 credential.helper = helper-selector。每次需要凭据交互时,Git 都会调用该选择器弹窗让用户挑选,即使选择了也可能因为权限问题未能将选择持久化到全局配置中。
彻底解决方案
打开终端,执行以下两条命令重置凭据助手:
# 1. 传入空字符串,清除系统底层继承的所有 helper-selector
git config --global credential.helper ""
# 2. 明确将全局凭据助手锁定为官方的 Git Credential Manager
git config --global --add credential.helper manager
设置后再次执行 Git 操作,弹窗将彻底消失,Git 会静默调用 Windows 系统的凭据管理器进行认证。