适用读者:第一次自己买服务器建站、被"三口锅四张表"折腾过的人。
前置:S01(开发环境与工具链)、S15(Linux、Docker 与运维交付)、S26(nginx 与证书基础)。
覆盖旧文:3218(Github 博客粉丝转移)、3219(GitHub 被攻击后 Jekyll 修复与重建)、3220(Netty + JavaFx 仿微信上云)、3221(自建云盘 + PDF 预览)、3222(一天建 4 个博客)、3223(Cloudreve 云盘实践)、3224(码农云服务:环境/端口/域名/SSL/博客)、3225(vuepress 迁移踩坑)、3226(webhooks 自动部署)、3227(docsify 所见即所得)、3228(免费服务器部署 ChatGPT 网页版)、3229(搭博客展示经历)→ 12 篇 / 约 4 万字符。
扩展点:E-S27-021(四层自动化阶梯:手动→脚本→Git 钩子→CI)、E-S27-022(webhook 部署的四个安全坑与最小安全实现)、E-S27-023(静态站工具三选一:Jekyll / docsify / VitePress 的判据)。
下一篇:S27-X02 · 学成在线附录源码详解归档。
1. 一句话结论
自建站点的分水岭不是"用哪个框架",是"部署靠手还是靠机":手传 FTP → 脚本拉取 → Git 钩子 → CI 流水线。每上一层,省的是"忘记传某台机器"这类事故;同时每上一层都新增一个可被攻击的入口(3226 的 webhook 就是典型)。
出处:
A-3224(云服务全流程)、A-3226(webhooks)、A-3225(迁移)、A-3223(Cloudreve)。
2. 四层自动化阶梯(E-S27-021)
| 层 | 做法 | 省掉什么 | 新增风险 |
|---|---|---|---|
| 0 手动 | FTP/SFTP 逐台传 | — | 漏传、版本不一致(3226 的用户就是这样) |
| 1 脚本 | rsync -avz --delete dist/ host:/var/www/ |
传输重复 | 无版本可回滚 |
| 2 Git 钩子 | webhook → 服务器 git pull + 构建 |
人工触发 | 公网可达的部署入口(见 §3) |
| 3 CI | push → CI 构建 → 产物发布(含回滚) | 人工判断 | CI 凭据与流水线权限 |
判据:站点 ≤3 台、内容更新 <每周一次 → 第 2 层足够;多环境/多机房/需要回滚审计 → 第 3 层。不要一步跳到 Kubernetes。
3. webhook 部署的四个安全坑(E-S27-022)
3226 的最小实现是 webhooks.php 里 shell_exec("git pull origin main 2>&1") —— 能跑,但把公网请求直接变成命令执行入口。四个坑按严重度排序:
① 无来源校验:任何人 POST 你的 URL 都能触发部署(甚至注入参数)
→ 校验签名:HMAC-SHA256(body, secret) 与请求头 X-Hub-Signature-256 比对(定时安全比较)
② 参数拼进命令:shell_exec("git pull " . $_POST['branch'])
→ 命令注入。分支名必须白名单(如仅 main),且用数组形式执行、不拼字符串
③ 部署以 root 运行:一旦被利用即整机失守
→ 专用低权限用户 + 只对站点目录有写权限
④ 无并发/无幂等:重复推送并发执行 git pull → 索引锁、构建产物互相覆盖
→ 文件锁(flock)+ 部署队列串行化
最小安全实现(PHP 伪代码,可移植到任意语言):
$secret = getenv('HOOK_SECRET');
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
$expect = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($expect, $sig)) { http_response_code(403); exit; }
$payload = json_decode($body, true);
if (($payload['ref'] ?? '') !== 'refs/heads/main') { http_response_code(204); exit; }
$lock = fopen('/tmp/deploy.lock', 'c');
if (!flock($lock, LOCK_EX | LOCK_NB)) { http_response_code(409); exit; } // 已在部署
// 用数组形式、固定命令,不拼接任何外部输入
exec('sudo -u deploy /opt/deploy/pull_and_build.sh 2>&1', $out, $rc);
flock($lock, LOCK_UN);
http_response_code($rc === 0 ? 200 : 500);
# /opt/deploy/pull_and_build.sh(由 deploy 用户执行,固定分支)
#!/usr/bin/env bash
set -euo pipefail
cd /srv/site
git fetch --prune origin main
git reset --hard origin/main # 产物可重建,故用 reset 而非 merge
npm ci && npm run build
rsync -a --delete dist/ /var/www/site/
判据:"能否被重放"决定要不要签名——签名防的是伪造请求,flock 防的是并发,低权限防的是被利用后的爆炸半径。三者缺一不可。
4. 静态站工具三选一(E-S27-023)
| 工具 | 构建 | 适合 | 坑(旧文实测) |
|---|---|---|---|
| Jekyll | Ruby + Liquid | 老牌、GitHub Pages 原生 | 依赖 Ruby 环境(3219 重建时踩 gem 源、版本);主题生态旧 |
| docsify | 运行时渲染(无构建) | 文档型、极简、改完即生效(3227) |
SEO 差(内容由 JS 渲染)、首屏慢;不适合长文博客 |
| VuePress / VitePress | Node 构建为静态 HTML | 中文技术博客、Markdown 友好 | 迁移期配置与主题适配最痛(3225:300 篇从 jekyll 迁 vuepress 差点拉胯) |
迁移三原则(3225 的教训):
① 先迁「内容」再迁「样式」:Markdown 与 front-matter 先跑通,主题后调
② 保留旧 URL 映射:生成 301 表(旧路径→新路径),否则外链与搜索收录全废
③ 一次性全量迁移前,先迁 10 篇做端到端验证(构建、图片、代码高亮、锚点、RSS)
判据:"内容要 SEO / 要长文"就别选 docsify;"只要内部文档、改完立刻看"选 docsify 最省。
5. 私有服务的三类最小实现
| 服务 | 最小实现 | 关键配置 | 风险点 |
|---|---|---|---|
云盘(3221/3223) |
Cloudreve(Go 单体 + SQLite/MySQL) | 存储策略(本地/对象存储)、分享链接有效期、限速 | 目录遍历与分享越权;默认管理员口令 |
| PDF 预览 | 前端 pdf.js 或后端转图 | 大文件分片、Range 请求 | 内存爆(整本加载) |
多媒体/AI 网页(3228) |
反向代理 + 密钥放服务端 | nginx proxy_pass + 超时 |
把 API Key 写进前端 JS(等于公开) |
判据:"任何密钥出现在浏览器可见处即视为泄露"——前端只能持短期票据,长期密钥留在服务端或网关。
6. 域名、端口、证书与备案(3224 的可执行清单)
□ 域名:注册 → 实名 → (国内服务器)备案 → DNS 解析到服务器公网 IP
□ 端口:只开 22/80/443;应用端口(如 8080)只监听 127.0.0.1,由 nginx 反代
□ 证书:Let's Encrypt(certbot --nginx)或云厂商免费证书;配置自动续期与到期告警
□ nginx:80 全量 301 到 443;HSTS 谨慎开启(先确认全站 HTTPS 可用)
□ 防火墙:ufw/firewalld 默认拒绝入站;云安全组与系统防火墙两处都要开
□ 备份:站点目录 + 数据库每日备份,**备份文件不在同一台机器**
验证命令(只读,安全):
ss -lntp | grep -E ':(22|80|443|8080)' # 确认监听地址
curl -sI https://DOMAIN | head -5 # 状态码与 HSTS/CSP 头
echo | openssl s_client -connect DOMAIN:443 -servername DOMAIN 2>/dev/null | openssl x509 -noout -dates
7. 反例:七种"建完就后悔"
| 做法 | 后果 |
|---|---|
| FTP 逐台传 | 三台机器版本不一致(3226 用户原话) |
| webhook 无签名、root 执行 | 公网可触发命令执行 |
| 应用端口直接暴露公网 | 绕过 nginx 的限流与鉴权 |
| 密钥写进前端 | 一夜之间被刷爆额度 |
| docsify 做长文博客 | 搜索引擎收录差、首屏慢 |
| 迁移不做 301 | 旧外链全 404,流量归零 |
| 备份与站点同机 | 机器挂了备份一起没 |
8. 自测
- 四层自动化阶梯各层省什么、新增什么风险?
- webhook 部署的四个安全坑与对应措施?
- Jekyll / docsify / VuePress 的判据?
- 迁移三原则?
- 域名端口证书清单里,哪两项最容易被漏?
答案
- 手动→脚本→Git 钩子→CI;省人工与不一致,新增公网入口与凭据风险。
- 无签名(HMAC+定时安全比较)、命令注入(白名单+数组执行)、root 运行(低权限用户)、并发(flock)。
- 要 SEO 与长文用 VuePress/VitePress,内部文档用 docsify,GitHub Pages 生态用 Jekyll。
- 先内容后样式、保留 301 映射、先迁 10 篇端到端验证。
- 应用端口只监听 127.0.0.1、备份不在同一台机器。
9. 下一篇
S27-X02 · 学成在线附录源码详解归档 —— A–L 十个附录(185 万字符)的模块划分、与 S07/S08/S11/S17 主讲篇的映射表,以及为什么这批教材附录不做点位级吸收。
本篇属于系列《S27 · 杂项归档与工具实测》。旧文来源:A-3218–3229(12 篇自建站实操,作者 小傅哥 / bugstack.cn);扩展点 E-S27-021/022/023。文中命令与配置在 Ubuntu 24.04 + nginx 1.25 口径下核对,具体版本差异需按环境验证。