VuePress 博客自动部署实践
VuePress 博客自动部署实践
本博客基于 VuePress 框架与 VuePress-Theme-Hope 主题搭建。由于主题内置的自动部署功能主要支持 GitHub Pages,而笔者期望将博客部署至个人轻量应用服务器并实现自动化流程,因此开展了本项目的实践。
项目概述
需求分析
本项目旨在实现个人博客的自动化部署与高效运维,具体需求如下:
- 功能需求:博客源代码通过 GitHub 私有仓库进行托管,并利用 GitHub Actions 实现持续集成与持续部署(CI/CD)。当代码推送至主分支后,应自动触发并完成完整的部署流程。
- 安全需求:在 GitHub Actions 的自动化部署流程中,需要对服务器进行安全的身份验证。为遵循最小权限原则,部署过程中使用的用户凭据不应具备 root 权限。
- 性能需求:自动化部署过程需要具备高效率,以缩短从代码提交到线上生效的延迟。同时,应降低对服务器资源的消耗,将服务器的核心性能用于处理用户访问请求,而非博客内容的构建与管理。
- 协议升级需求:启用 HTTP/2 或 HTTP/3 协议支持,利用其多路复用、头部压缩等新特性,显著减少页面加载延迟,提升在复杂网络环境下的用户访问体验。
技术选型
基于上述需求,本项目选定的技术栈如下:
- 持续集成/持续部署(CI/CD):采用 GitHub Actions 作为核心自动化工具,实现代码提交后自动触发构建和部署工作流。
- Web 服务与反向代理:利用 Docker 对 Nginx 进行容器化封装,实现一个轻量且高效的 HTTP 反向代理服务。此方式简化了部署和管理,并提高了服务的隔离性。
- HTTPS 与证书管理:通过 Certbot 实现 SSL/TLS 证书的自动申请、配置与续期,确保网站的 HTTPS 连接长期有效且无需人工干预。
- 服务管理:使用 systemd 来管理 Docker 守护进程和 Certbot 的自动续期计时器,以保证服务的稳定运行和管理的便捷性。
技术选型背景
Docker 与容器化:Docker 是一种能将应用及其所有依赖打包到一个标准化单元(称为“容器”)中的技术。本项目中使用 Docker 封装 Nginx,可以避免直接在服务器上安装 Nginx 及其复杂的依赖,实现了环境隔离,让部署和迁移变得极其简单。
Nginx 与反向代理:Nginx 是一款高性能的 Web 服务器。作为反向代理时,它接收来自客户端(用户)的请求,然后将请求转发到后端的服务(如此处的博客文件),并将后端服务的响应返回给客户端。这种方式可以隐藏后端服务的具体实现,并轻松实现负载均衡、SSL/TLS 加密等功能。
Certbot 与 HTTPS:HTTPS 通过 SSL/TLS 协议为网站通信提供加密,保障数据传输安全。Certbot 是一个自动化工具,用于从 Let's Encrypt 免费获取 SSL/TLS 证书并自动配置到 Web 服务器上,还能处理证书的自动续期,极大地简化了 HTTPS 的部署流程。
systemd:
systemd
是 Linux 系统中现代化的系统和服务管理器。它负责在系统启动时启动服务、监控其运行状态并在服务失败时尝试重启。在本项目中,它用于管理 Nginx 容器和 Certbot 的定时续期任务,确保服务的稳定性和无人值守运行。HTTP/2 & HTTP/3 与性能优化:虽然 HTTP/1.1 应用广泛,但其“队头阻塞”问题会限制页面加载性能。HTTP/2 通过引入多路复用(Multiplexing)机制,允许在单个 TCP 连接上同时并行处理多个请求和响应,彻底解决了这一瓶颈。此外,它还通过头部压缩(Header Compression)等技术进一步减少了传输开销。而 HTTP/3 则更进一步,它基于 QUIC 协议(运行在 UDP 之上),不仅继承了 HTTP/2 的优点,还解决了 TCP 层的队头阻塞问题,并优化了连接建立过程,使得网络切换(如从 Wi-Fi 切换到移动数据)时的连接保持更加无缝,尤其利好移动端用户。
项目整体架构如下图所示:
实现细节
服务器环境配置
在开始具体操作之前,需要先为服务器安装必要的软件。本文以 Ubuntu 24.04 为例,所有操作均通过 apt
包管理器完成。
首先,更新软件包列表并安装 Docker、Certbot 以及 rsync:
apt update
apt install -y docker.io certbot rsync
安装完成后,启动 Docker 服务并设置为开机自启动:
systemctl enable --now docker
防火墙与安全组提醒
在进行后续操作前,请务必检查并配置您的服务器防火墙或云服务商的安全组规则,确保以下端口已正确放行:
- SSH 端口(默认为
22/TCP
):用于远程登录和rsync
数据同步。 - HTTP 端口(
80/TCP
):用于 Certbot 进行 HTTP-01 验证。 - HTTPS 端口(
443/TCP
):用于标准的 HTTPS 访问。 - QUIC/HTTP3 端口(
443/UDP
):为确保 HTTP/3 协议正常工作,必须放行此 UDP 端口。
创建部署用户
为实现博客的安全自动部署,需在服务器上创建一个权限受限的专用用户,用于管理部署过程。
#!/bin/sh
username="blog_user"
# 该脚本必须以 root 权限运行
if [ "$(id -u)" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# 创建博客管理用户
useradd \
-c "Blog Administrator, used to maintain blog resources and SSL certificates" \
-m \
-s "/bin/sh" \
"${username}"
# 确保博客管理用户无密码且无法通过密码登录
passwd -dlq "${username}"
# 生成 ssh 密钥,算法为 ed25519,文件名为 "${username}_key"
ssh-keygen \
-t ed25519 \
-C "Authentication for Blog Administrator" \
-N "" \
-f "${username}_key"
# 获取博客管理用户的用户目录
user_dir=$(getent passwd "${username}" | cut -d ':' -f6)
# 将公钥添加到博客管理用户的 authorized_keys 文件中
mkdir -p "${user_dir}/.ssh"
cat "${username}_key.pub" >>"${user_dir}/.ssh/authorized_keys"
# 提醒用户保存私钥
echo "Please save the private key file: ${username}_key"
# 退出
exit 0
上述脚本创建了一个名为 blog_user
的用户,并生成了用于 GitHub Actions 身份验证的 SSH 密钥 blog_user_key
。
SSH 密钥如何工作?
SSH 密钥对(公钥和私钥)提供了一种比密码更安全的身份验证方法。
- 私钥 (
blog_user_key
):存放在发起连接的客户端(此处为 GitHub Actions 运行器),必须严格保密。 - 公钥 (
blog_user_key.pub
):存放在目标服务器的~/.ssh/authorized_keys
文件中,可以公开。
当 GitHub Actions 尝试连接服务器时,它会用私钥对一个随机数据进行签名,并将签名后的数据发送给服务器。服务器收到后,用预存的公钥进行验证。如果验证成功,则证明客户端确实持有对应的私钥,从而允许连接。整个过程无需传输密码,避免了密码被截获或暴力破解的风险。
blog_user
用户的家目录下还需要创建博客所需的目录结构,命令如下:
mkdir -p ~blog_user/blog # 博客目录,存储静态文件,blog_user 具备读写权限
mkdir -p ~blog_user/cert # 证书目录,包含私钥和证书,Nginx 挂载读取
mkdir -p ~blog_user/logs # Nginx 日志目录
mkdir -p ~blog_user/nginx # Nginx 配置目录
mkdir -p ~blog_user/scripts # systemd 相关脚本目录
配置 Nginx HTTP 服务
本项目选用 Docker 容器化的 Nginx 作为 HTTP 服务,Nginx 的初始化脚本为 init_nginx.sh
。
#!/bin/sh
# 该脚本必须以 root 权限运行
if [ "$(id -u)" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# 博客管理用户的用户名
username="blog_user"
# 启动 docker 中的 nginx
docker run \
--detach \
--name blog_nginx \
nginx:latest
# 获取博客管理用户的用户目录
user_dir=$(getent passwd "${username}" | cut -d ':' -f6)
# 从 docker 中拷贝 nginx 配置文件到博客管理用户的用户目录
docker cp "blog_nginx:/etc/nginx" "${user_dir}"
# 删除 docker 中的 nginx 容器
docker rm -f blog_nginx
为便于管理,通过 docker_nginx.service
文件配置一个 systemd 服务,该文件定义了 Docker 容器的启动命令与参数。
[Unit]
Description="Blog Nginx Service"
After=docker.service network.target
Requires=docker.service
[Service]
# 启动 nginx 前先删除之前的 nginx 容器
ExecStartPre=-/usr/bin/docker rm -f blog_nginx
# 启动 docker 中的 nginx
ExecStart=/usr/bin/docker run \
--name blog_nginx \
-p 80:80 \
-p 443:443 \
-p 443:443/udp \
-v /home/blog_user/cert:/home/blog_user/cert \
-v /home/blog_user/blog:/home/blog_user/blog \
-v /home/blog_user/logs:/var/log/nginx \
-v /home/blog_user/nginx:/etc/nginx \
nginx:latest
# 重新加载 nginx 配置
ExecReload=/usr/bin/docker exec blog_nginx nginx -s reload
# 停止 nginx
ExecStop=/usr/bin/docker stop blog_nginx
[Install]
WantedBy=multi-user.target
由于 Certbot 证书签发需要通过 HTTP-01 方式进行验证,因此需预先配置一个临时的 HTTP 服务。blog_verify_or_redirect.conf
文件用于此目的。
# 定义 HTTP 服务 (80 端口)
server {
listen 80;
listen [::]:80;
server_name blog.agicy.cn;
# 用于 Let's Encrypt 的验证
location /.well-known {
root /home/blog_user/blog;
}
# 重定向到 HTTPS
location / {
return 301 https://$host$request_uri;
}
}
什么是 HTTP-01 验证?
HTTP-01 是 Certbot 用来验证您对域名拥有控制权的一种方式。其工作流程如下:
- 您告诉 Certbot 要为
your-domain.com
申请证书。 - Certbot 的颁发机构(如 Let's Encrypt)会给出一个随机令牌(Token)。
- 颁发机构要求您将这个令牌放置在
http://your-domain.com/.well-known/acme-challenge/
路径下的一个特定文件中。 - 颁发机构通过公网访问该 URL,如果能成功获取到正确的令牌,就证明了您确实控制着该域名,从而同意签发证书。
因此,在申请证书前,必须先启动一个能响应此路径访问的 HTTP 服务。
配置 Certbot 自动证书
以下是安装与配置 Certbot 的步骤,certbot_init.sh
文件用于初始化。
#!/bin/sh
username="blog_user"
domain="blog.agicy.cn"
email="2359800311@qq.com"
# 当前用户应当具有 root 权限
if [ "$(id -u)" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# 博客目录在用户目录下的 blog 文件夹,计算博客目录的绝对路径
blog_dir="$(getent passwd "${username}" | cut -d ':' -f6)/blog"
# 生成 SSL 证书
certbot certonly --webroot \
--email="${email}" \
--webroot-path="${blog_dir}" --domains="${domain}"
证书签发完成后,需要设置定时任务以实现自动续期。首先创建续期脚本 certbot_renew.sh
。
#!/bin/sh
username="blog_user"
domain="blog.agicy.cn"
# 该脚本必须以 root 权限运行
if [ "$(id -u)" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# 计算博客管理用户的用户目录
user_dir="$(getent passwd "${username}" | cut -d ':' -f6)"
# 创建博客管理用户目录下的 cert 文件夹
mkdir -p "${user_dir}/cert"
# 生成 SSL 证书
certbot renew
# 通过 umask 设定生成的文件权限不超出 -rwxr-x---
umask 0027
# 将生成的 SSL 证书复制到 blog_user 的用户目录,设定权限为 -rw-r-----
cp "/etc/letsencrypt/live/${domain}/privkey.pem" "${user_dir}/cert"
cp "/etc/letsencrypt/live/${domain}/fullchain.pem" "${user_dir}/cert"
chmod 640 "${user_dir}/cert/privkey.pem"
chmod 640 "${user_dir}/cert/fullchain.pem"
# 获取 blog_nginx 容器的中 nginx 用户的组
nginx_gid=$(docker exec blog_nginx id -g nginx)
# 输出 nginx 用户的组
echo "nginx group id is ${nginx_gid}"
# 设置 SSL 证书的组为 nginx
chgrp -R "${nginx_gid}" "${user_dir}/cert"
# 刷新 Nginx 配置
docker exec blog_nginx nginx -s reload
接着,创建用于执行续期任务的 systemd 服务文件 certbot_renew.service
。
[Unit]
Description="Let's Encrypt certificates for blog.agicy.cn"
[Service]
Type=oneshot
ExecStart=/bin/sh "/home/blog_user/scripts/certbot_renew.sh"
[Install]
WantedBy=multi-user.target
最后,创建 systemd 定时器 certbot_renew.timer
以定期触发服务。
[Unit]
Description="Renew Let's Encrypt certificates for blog.agicy.cn"
[Timer]
Unit=certbot_renew.service
OnCalendar=*-*-* 00:00:00
[Install]
WantedBy=multi-user.target
启用 Nginx HTTPS
完成证书签发后,即可配置正式的 HTTPS 服务。创建 blog.conf
配置文件,它用于启用 HTTPS,或者将 HTTP 服务重定向到 HTTPS。
# 定义 HTTPS 服务 (443 端口)
server {
listen 443 ssl; # 监听 TCP 443 端口,启用 SSL 和 HTTP/2
listen [::]:443 ssl; # 同时监听 IPv6 的 TCP 443 端口,启用 SSL 和 HTTP/2
listen 443 quic reuseport; # 监听 UDP 443 端口,启用 HTTP/3
listen [::]:443 quic reuseport; # 同时监听 IPv6 的 UDP 443 端口,启用 HTTP/3
server_name blog.agicy.cn;
# 启用 HTTP/2
http2 on;
# 启用 HTTP/3
http3 on;
# SSL 证书配置
ssl_certificate /home/blog_user/cert/fullchain.pem; # SSL 证书文件路径
ssl_certificate_key /home/blog_user/cert/privkey.pem; # SSL 私钥文件路径
# 定义默认字符集
charset utf-8;
# 处理静态文件(全部文件)
location / {
root /home/blog_user/blog; # 网站根目录
index index.html index.htm; # 默认索引文件
expires 30d; # 设置静态资源缓存时间
add_header Alt-Svc 'h3=":443"; ma=86400';
add_header Cache-Control public;
add_header Access-Control-Allow-Origin *.hitokoto.cn;
}
# 禁止访问隐藏文件和目录
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# 配置错误页面
error_page 404 /404.html;
location = /404.html {
root /home/blog_user/blog;
internal;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# 配置访问日志
access_log /var/log/nginx/blog.agicy.cn.access.log main;
error_log /var/log/nginx/blog.agicy.cn.error.log warn;
}
开启 HTTP/2 与 HTTP/3 支持
在上述 blog.conf
配置文件中,我们通过简单的指令开启了对新一代 HTTP 协议的支持,从而大幅提升网站性能。
重要提示:要使 HTTP/3 生效,您使用的 Nginx 必须在编译时包含了 HTTP/3 模块(例如,通过 --with-http_v3_module
参数编译),或者直接使用已经内置该模块的 Docker 镜像。本项目中使用的 Nginx Docker 镜像已支持此功能。
配置 GitHub Actions 工作流
最后一步是配置 GitHub Actions 工作流,对应工作流配置文件 deploy-docs.yml
。其核心原理是:当代码推送到主分支时,自动构建 VuePress 站点,并通过 rsync
将生成的静态文件同步到服务器的目标目录。由于该目录已挂载到 Nginx 容器,因此部署立即生效。
name: 部署文档
on:
push:
branches:
- main
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# 如果你文档需要 Git 子模块,取消注释下一行
# submodules: true
- name: 设置 pnpm
uses: pnpm/action-setup@v4
- name: 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: 安装依赖
run: |
corepack enable
pnpm install --frozen-lockfile
- name: 构建文档
env:
NODE_OPTIONS: --max_old_space_size=8192
run: |-
pnpm run docs:build
> src/.vuepress/dist/.nojekyll
- name: rsync deployments
uses: burnett01/rsync-deployments@7.0.2
with:
switches: -avzr --delete
path: "src/.vuepress/dist/"
remote_path: ${{ secrets.DEPLOY_PATH }}
remote_host: ${{ secrets.DEPLOY_HOST }}
remote_port: ${{ secrets.DEPLOY_PORT }}
remote_user: ${{ secrets.DEPLOY_USER }}
remote_key: ${{ secrets.DEPLOY_KEY }}
下面是工作流的执行时序图:
什么是 rsync?
rsync
(Remote Sync) 是一个功能强大的文件同步工具。它的核心优势是采用“增量”同步算法,即在同步文件时,它会先比较源和目标文件的差异,然后只传输发生变化的部分,而不是每次都完整复制所有文件。这使得 rsync
在部署静态网站等场景下效率极高,能显著减少数据传输量和部署时间。
该工作流依赖以下 GitHub Secrets,需要在仓库的 Settings > Secrets and variables > Actions
中进行配置:
DEPLOY_PATH
:博客文件在服务器上的绝对路径。DEPLOY_HOST
:服务器主机的域名或 IP 地址。DEPLOY_PORT
:服务器 SSH 连接端口(默认为 22)。DEPLOY_USER
:部署用户名,即blog_user
。DEPLOY_KEY
:用于 SSH 连接的私钥,即blog_user_key
的内容。
什么是 GitHub Secrets?
GitHub Secrets 是用于在 GitHub 仓库中存储敏感信息(如密码、API 密钥、私钥)的功能。
- 安全性:Secrets 的值在保存后无法被再次查看,只可以更新或删除。
- 自动注入:当工作流运行时,GitHub Actions 会将这些 Secrets 作为环境变量安全地注入到运行器中,供脚本使用。
- 日志保护:GitHub Actions 会自动屏蔽日志中出现的 Secrets 内容,防止敏感信息意外泄露。
严禁将私钥、密码等敏感信息直接硬编码在工作流 .yml
文件中,务必使用 Secrets 进行管理。
配置完成后,向 GitHub 仓库推送一次提交,即可在 Actions 页面观察工作流的执行情况,验证部署是否成功。
总结
通过本项目的实践,成功地在轻量应用服务器上构建了一套完整的博客自动化部署与运维流程。该方案不仅实现了代码提交后的 CI/CD,还通过用户权限控制、Docker 容器化和 systemd 服务管理,兼顾了安全性与运行效率,为个人项目的自动化管理提供了一个可靠的范例。