前言
之前我的博客部署到 VPS 使用的是 Jenkins + Docker 的方案(具体在这篇文章)。Jenkins 占用资源较大,实测长期运行不重启的话,内存能达到近 1GB,所以想放弃使用。但希望保留既能部署到 GitHub Pages,又能部署到服务器的功能。
近期借助 AI 工具,多次尝试,成功实现了基于 GitHub Actions + Docker + Webhook 的自动化部署方案。以下是完整的搭建记录,已经过验证,按照这个流程可以成功搭建服务。
在开始之前需要准备两个仓库,以我的为例:
- 私有仓库:
DEKVIW/ZDBS
(博客源码私有仓库)
- 公开仓库:
DEKVIW/DEKVIW.github.io
(GitHub Pages 公开仓库)
在保持现有结构基础上,添加 Docker 化的服务器自动部署:
1 2 3
| 本地hexo博客 → ZDBS(私有源码) → GitHub Actions → DEKVIW.github.io(GitHub Pages) ↓ (webhook通知) Webhook服务器 → git pull更新 → Docker Nginx服务
|
第一步:服务器环境准备
安装必要依赖(Debian)
根据环境需要选择安装,已经具备的环境就不需要重复安装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| apt update && apt upgrade -y
curl -fsSL https://get.docker.com | bash curl -L "https://github.com/docker/compose/releases/download/v2.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose
systemctl start docker systemctl enable docker
curl -fsSL https://deb.nodesource.com/setup_18.x | bash - apt-get install -y nodejs npm install -g pm2
docker --version docker-compose --version node --version
|
创建项目目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| mkdir -p /root/data/hexo-webhook/{logs/nginx,scripts,configs,nginx/conf.d,nginx/html}
mkdir -p /root/data/hexo-webhook/nginx/html/{hexo,static,unhxi,mdnice}
cd /root/data/hexo-webhook/nginx/html/hexo git clone https://github.com/DEKVIW/DEKVIW.github.io.git .
cat > .gitignore << 'EOF' cizhi/ EOF
|
目录结构说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| /root/data/hexo-webhook/ ├── logs/ # 所有日志文件 ├── scripts/ # webhook和监控脚本 ├── configs/ # PM2和其他配置 ├── docker-compose.yml # Docker Compose配置 └── nginx/ # Nginx Docker相关 ├── conf.d/ # 站点配置文件 ├── html/ # 多站点静态文件目录 │ ├── hexo/ # 博客站点(Git管理) │ │ ├── .git/ # Git仓库 │ │ ├── index.html # 博客文件 │ │ ├── posts/ # 博客文件 │ │ ├── css/ # 博客文件 │ │ ├── js/ # 博客文件 │ │ └── .gitignore # Git忽略配置 │ ├── static/ # 静态资源目录(作者个性化) │ ├── mdnice/ # mdnice文件目录(作者个性化) │ └── unhxi/ # 文件下载目录(作者个性化) └── nginx.conf # Nginx主配置文件
|
第二步:Nginx 配置
创建 Nginx 主配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
| cat > /root/data/hexo-webhook/nginx/nginx.conf << 'EOF' user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid;
worker_rlimit_nofile 65535;
events { multi_accept on; worker_connections 2048; }
http { include /etc/nginx/mime.types; default_type application/octet-stream; server_tokens off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
limit_req_zone $binary_remote_addr zone=example_zone:50m rate=35r/s; limit_req zone=example_zone burst=100 nodelay;
limit_conn_zone $binary_remote_addr zone=addr:20m; limit_conn addr 25;
limit_rate_after 50m; limit_rate 20m;
open_file_cache max=2000 inactive=30s; open_file_cache_valid 60s; open_file_cache_min_uses 2; open_file_cache_errors on;
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:20m max_size=1g inactive=30m; proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=my_proxy_cache:20m max_size=1g inactive=30m;
gzip on; gzip_static on; gzip_proxied any; gzip_vary on; gzip_comp_level 4; gzip_buffers 8 256k; gzip_min_length 50; gzip_types application/atom+xml application/javascript application/json application/vnd.api+json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/richtext text/plain text/x-script text/x-component text/x-java-source text/x-markdown text/javascript text/xml application/x-perl application/x-httpd-cgi multipart/bag multipart/mixed application/wasm;
client_body_timeout 60s; client_header_timeout 60s; send_timeout 60s; keepalive_timeout 120s; keepalive_requests 8000;
fastcgi_cache_key "$scheme$request_method$host$request_uri$http_accept_encoding"; fastcgi_cache_methods GET HEAD; fastcgi_cache_bypass $http_cookie; fastcgi_no_cache $http_cookie; fastcgi_cache_valid 200 301 302 120m; fastcgi_cache_valid 404 10m; fastcgi_cache_valid 500 502 503 504 0; fastcgi_cache_lock on; fastcgi_cache_lock_timeout 5s; fastcgi_cache_background_update on; fastcgi_buffering on; fastcgi_buffer_size 128k; fastcgi_buffers 16 1024k; fastcgi_busy_buffers_size 8m; fastcgi_keep_conn on; fastcgi_intercept_errors on;
proxy_cache_key "$scheme$request_method$host$request_uri$http_accept_encoding"; proxy_cache_methods GET HEAD; proxy_cache_bypass $http_cookie; proxy_no_cache $http_cookie; proxy_cache_valid 200 301 302 120m; proxy_cache_valid 404 10m; proxy_cache_valid 500 502 503 504 0; proxy_cache_lock on; proxy_cache_lock_timeout 5s; proxy_cache_background_update on; proxy_buffering on; proxy_buffer_size 128k; proxy_buffers 16 1024k; proxy_busy_buffers_size 8m; proxy_intercept_errors on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy "no-referrer"; add_header Permissions-Policy "geolocation=(), microphone=()"; add_header Vary "Accept-Encoding" always;
include /etc/nginx/conf.d/*.conf; } EOF
|
创建站点配置文件
配置名字dekviw-blog.conf
可以自己随便改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| cat > /root/data/hexo-webhook/nginx/conf.d/dekviw-blog.conf << 'EOF' server { listen 80; server_name localhost;
limit_req zone=example_zone burst=100 nodelay; limit_conn addr 25;
error_page 404 /404.html; location = /404.html { root /usr/share/nginx/html; internal; }
location /static/ { root /usr/share/nginx/html; expires 1d; add_header Cache-Control "public";
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } }
location /unhxi/ { root /usr/share/nginx/html; autoindex on; autoindex_exact_size off; autoindex_localtime on; expires 1d;
location ~* \.(zip|rar|7z|tar|gz|bz2|pdf|doc|docx|xls|xlsx|ppt|pptx)$ { expires 7d; add_header Cache-Control "public"; } }
location / { root /usr/share/nginx/html/hexo; index index.html; try_files $uri $uri/ =404;
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; }
location ~* \.html$ { expires 1h; add_header Cache-Control "public, must-revalidate"; } }
location /health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; }
location ~ /\. { deny all; access_log off; log_not_found off; }
location ~ /\.ht { deny all; } } EOF
|
创建 DockerCompose 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| cat > /root/data/hexo-webhook/docker-compose.yml << 'EOF' version: '3.8'
services: nginx: image: nginx:alpine container_name: hexo-nginx ports: - "8080:80" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/html:/usr/share/nginx/html:ro - ./logs/nginx:/var/log/nginx environment: - TZ=Asia/Shanghai restart: unless-stopped networks: - hexo-network
networks: hexo-network: driver: bridge EOF
|
第三步:Webhook 服务器
创建配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| cat > /root/data/hexo-webhook/scripts/config.js << 'EOF' module.exports = { // Webhook 密钥(请修改为您的安全密钥) WEBHOOK_SECRET: process.env.WEBHOOK_SECRET || 'your-super-secret-key-here-12345',
// 服务器端口 PORT: process.env.PORT || 3000,
// 路径配置 NGINX_HTML_PATH: '/root/data/hexo-webhook/nginx/html/hexo',
// 日志配置 LOG_LEVEL: process.env.LOG_LEVEL || 'info' }; EOF
|
生成安全密钥并写入配置文件
1
| SECRET_KEY=$(openssl rand -hex 32) && sed -i "s/'your-super-secret-key-here-12345'/'$SECRET_KEY'/g" /root/data/hexo-webhook/scripts/config.js && echo "=== 生成的密钥(复制到 GitHub Secrets)===" && echo "$SECRET_KEY" && echo "=== 密钥已写入配置文件 ==="
|
创建 Webhook 服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
| cat > /root/data/hexo-webhook/scripts/webhook-server.js << 'EOF' const express = require('express'); const crypto = require('crypto'); const { exec } = require('child_process'); const fs = require('fs'); const path = require('path');
// 加载配置 const config = require('./config');
const app = express(); const PORT = config.PORT; const SECRET = config.WEBHOOK_SECRET; const NGINX_HTML_PATH = config.NGINX_HTML_PATH;
app.use(express.json());
// 验证 webhook 签名 function verifySignature(req, res, next) { const signature = req.headers['x-hub-signature-256']; if (!signature) { return res.status(401).json({ error: 'Missing signature' }); }
const bodyString = JSON.stringify(req.body); const expectedSignature = 'sha256=' + crypto .createHmac('sha256', SECRET) .update(bodyString) .digest('hex');
if (signature !== expectedSignature) { return res.status(401).json({ error: 'Invalid signature' }); }
next(); }
// 部署博客 function deployBlog() { return new Promise((resolve, reject) => { console.log('开始部署博客...');
// 切换到博客目录 process.chdir(NGINX_HTML_PATH);
// 执行 Git 操作 const commands = [ 'git fetch origin', 'git reset --hard origin/main', 'git clean -fd' ];
let currentCommand = 0;
function executeNext() { if (currentCommand >= commands.length) { console.log('部署完成');
// 重新创建 .gitignore 文件以保护个性化目录 try { const gitignoreContent = ` cizhi/ upimg/ rmbg/ qrc/ fyh/ bbd/ bzsm/ nav/ wzq/
!.gitignore `;
fs.writeFileSync('.gitignore', gitignoreContent); console.log('.gitignore 文件已重新创建,个性化目录得到保护'); } catch (error) { console.warn('创建 .gitignore 文件失败:', error.message); // 不阻止部署流程,只记录警告 }
resolve(); return; }
const command = commands[currentCommand]; console.log(`执行命令: ${command}`);
exec(command, (error, stdout, stderr) => { if (error) { console.error(`命令执行失败: ${command}`, error); reject(error); return; }
if (stdout) console.log(stdout); if (stderr) console.log(stderr);
currentCommand++; executeNext(); }); }
executeNext(); }); }
// Webhook 端点 app.post('/webhook', verifySignature, async (req, res) => { try { console.log('收到 webhook 请求:', req.body);
// 部署博客 await deployBlog();
// 验证部署是否成功 if (!fs.existsSync(path.join(NGINX_HTML_PATH, 'index.html'))) { throw new Error('部署后未找到 index.html'); }
res.json({ success: true, message: '部署成功' });
} catch (error) { console.error('Webhook 处理错误:', error); res.status(500).json({ error: '部署失败', details: error.message }); } });
// 健康检查端点 app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), config: { port: PORT, nginxPath: NGINX_HTML_PATH } }); });
// 启动服务器 app.listen(PORT, () => { console.log(`Webhook 服务器运行在端口 ${PORT}`); console.log(`博客路径: ${NGINX_HTML_PATH}`); console.log(`配置加载完成`); }); EOF
|
创建 package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| cat > /root/data/hexo-webhook/scripts/package.json << 'EOF' { "name": "hexo-webhook-server", "version": "1.0.0", "description": "Webhook server for Hexo blog deployment", "main": "webhook-server.js", "scripts": { "start": "node webhook-server.js", "dev": "nodemon webhook-server.js", "config": "node -e \"console.log(require('./config'))\"" }, "dependencies": { "express": "^4.18.2" }, "devDependencies": { "nodemon": "^3.0.1" } } EOF
|
安装依赖并启动服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| cd /root/data/hexo-webhook/scripts npm install
npm run config
pm2 start webhook-server.js --name "hexo-webhook" --log /root/data/hexo-webhook/logs/webhook.log
pm2 startup pm2 save
pm2 status pm2 logs hexo-webhook
|
第四步:GitHub Actions 工作流配置
- 在\BlogRoot.github 文件夹下新建 workflows 文件夹
- 在 workflows 文件夹下新建 autodeploy.yml 文件
- 将以下代码复制到 autodeploy.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| name: DEKVIW博客自动部署
on: push: branches: [main] release: types: [published]
jobs: deploy: runs-on: ubuntu-latest
steps: - name: 检查源码 uses: actions/checkout@v4 with: ref: main submodules: recursive
- name: 设置Node.js环境 uses: actions/setup-node@v4 with: node-version: "18" cache: "npm"
- name: 设置时区 run: | export TZ='Asia/Shanghai' echo "TZ=Asia/Shanghai" >> $GITHUB_ENV
- name: 缓存依赖 uses: actions/cache@v3 with: path: node_modules key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.OS }}-node-
- name: 安装依赖 run: | npm install hexo-cli -g npm install
- name: 构建静态文件 run: | hexo clean hexo generate
- name: 生成时间戳 run: | echo "COMMIT_TIME=$(date +'%Z %Y-%m-%d %A %H:%M:%S')" >> $GITHUB_ENV
- name: 部署到GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: personal_token: ${{ secrets.GITHUBTOKEN }} external_repository: DEKVIW/DEKVIW.github.io publish_branch: main publish_dir: ./public commit_message: ${{ github.event.head_commit.message }} [${{ env.COMMIT_TIME }}] user_name: ${{ secrets.GITHUBUSERNAME }} user_email: ${{ secrets.GITHUBEMAIL }}
- name: 通知服务器更新 run: | # 创建webhook负载 WEBHOOK_PAYLOAD='{"repository":{"name":"DEKVIW.github.io","full_name":"DEKVIW/DEKVIW.github.io"},"ref":"refs/heads/main","head_commit":{"message":"${{ github.event.head_commit.message }}","timestamp":"${{ github.event.head_commit.timestamp }}","id":"${{ github.sha }}"},"pusher":{"name":"${{ github.actor }}"}}'
SIGNATURE=$(echo -n "$WEBHOOK_PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.WEBHOOK_SECRET }}" | cut -d' ' -f2)
# 发送webhook curl -X POST \ -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: sha256=$SIGNATURE" \ -H "User-Agent: GitHub-Actions-DEKVIW" \ -d "$WEBHOOK_PAYLOAD" \ ${{ secrets.WEBHOOK_URL }} \ --max-time 30 \ --retry 2 \ --connect-timeout 10 continue-on-error: true
|
重要说明:
- 将
DEKVIW
替换为你的 GitHub 用户名
- 将
DEKVIW.github.io
替换为你的 GitHub Pages 仓库名
- 确保
ZDBS
仓库中已安装 Hexo 相关依赖
第五步:GitHub Secrets 配置
Secret 名称 |
值 |
用途 |
说明 |
GITHUBTOKEN |
GitHub Personal Access Token |
GitHub 仓库访问权限 |
需要 repo 权限 |
GITHUBUSERNAME |
GitHub 用户名 |
Git 提交信息 |
GitHub 用户名 |
GITHUBEMAIL |
GitHub 邮箱 |
Git 提交信息 |
GitHub 邮箱 |
WEBHOOK_SECRET |
服务器生成的密钥 |
验证 webhook 签名 |
与服务器端 config.js 保持一致 |
WEBHOOK_URL |
http://服务器ip:3000/webhook |
webhook 请求地址 |
替换为服务器 IP |
获取 GitHub Personal Access Token:
- 登录 GitHub,点击右上角头像 → Settings
- 左侧菜单选择 “Developer settings” → “Personal access tokens” → “Tokens (classic)”
- 点击 “Generate new token” → “Generate new token (classic)”
- 设置名称(如:Hexo Blog),选择权限:
repo
(完整仓库权限)
- 点击 “Generate token”,复制生成的 token
配置步骤:
- 进入
ZDBS
仓库
- 点击 Settings → Secrets and variables → Actions
- 点击 “New repository secret”
- 添加上述表格的 Secrets
第六步:初始化部署
启动服务
1 2 3 4 5 6 7 8 9 10 11
| cd /root/data/hexo-webhook docker-compose up -d
docker-compose ps docker-compose logs nginx
pm2 status pm2 logs hexo-webhook
|
验证部署
1 2 3 4 5 6 7 8
| tree /root/data/hexo-webhook/nginx/html/ -L 2
curl http://192.227.206.48:3000/health
curl http://192.227.206.48:8080/health
|
第七步:测试完整流程
本地开发工作流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| cd /path/to/your/ZDBS
hexo new "新文章标题"
hexo generate
hexo server
git add . git commit -m "添加新文章:新文章标题" git push origin main
|
自动化部署流程
GitHub Actions 自动执行:
- 构建 Hexo 静态文件
- 部署到 GitHub Pages 仓库
- 发送 webhook 通知到服务器
服务器自动更新:
- Webhook 服务器接收通知
- 自动拉取最新代码
- Nginx 服务更新内容
验证部署结果:
- GitHub Pages:
https://your-username.github.io
- 服务器:
http://服务器ip:8080
常见问题排查
1 2 3 4 5 6 7 8 9
| ssh -T git@github.com
git config --list
hexo version npm list hexo-deployer-git
|
常用维护命令
注意:以下命令均在项目根目录 /root/data/hexo-webhook
下执行
1
| cd /root/data/hexo-webhook
|
查看服务状态
1 2 3 4 5 6 7 8
| docker-compose ps
pm2 status
pm2 show hexo-webhook
|
查看日志
1 2 3 4 5 6 7 8 9 10 11
| docker-compose logs nginx
pm2 logs hexo-webhook
pm2 logs hexo-webhook --err
pm2 logs hexo-webhook --lines 50
|
重启服务
1 2 3 4 5 6 7 8
| docker-compose restart nginx
pm2 restart hexo-webhook
docker-compose restart && pm2 restart hexo-webhook
|
排查问题
1 2 3 4 5 6 7 8 9 10 11
| curl http://服务器ip:3000/health
curl http://服务器ip:8080/health
ls -la nginx/html/hexo/
cd nginx/html/hexo && git pull origin main
|
更新配置
1 2 3 4 5 6 7 8
| docker-compose exec nginx nginx -s reload
pm2 restart hexo-webhook
cd scripts && npm run config
|