我的架构:云服务器A,绑定域名www.test.com,并且安装了wireguard,wireguard ip : AWIP,本地虚拟机B,Ubuntu26.04,wirdguard ip:BWIP,现在我想在B中安装ghost博客,然后通过A的域名访问
你的链路本质是:B 上跑 Ghost,A 做七层反向代理并终止 TLS。域名解析到 A 的公网 IP,A 通过 WireGuard 把请求转给 BWIP:2368,B 不直接暴露公网。这是最省事、最容易排查的做法。
浏览器
│ https://www.test.com (443)
▼
云服务器 A nginx + Let's Encrypt 证书
│ proxy_pass http://BWIP:2368 (走 wg0 隧道,明文)
▼
虚拟机 B Ghost (Node,监听 2368)
三个必须守住的前提:
- Ghost 的
url必须写死https://www.test.com,和公开访问地址完全一致,否则后台会重定向循环。 - TLS 只在 A 上做,B 不申请证书,A→B 走隧道内网。
- A 反代必须带
X-Forwarded-Proto: https,否则 Ghost 认为自己在 http 后面,会不停 301。
第 1 步:打通 WireGuard 并放行端口
两端 wg0 的 AllowedIPs 要互相对得上(A 端放 BWIP/32,B 端放 AWIP/32 或整个 10.x.0.0/24),并且 B 在 NAT 后要加 PersistentKeepalive = 25。云厂商安全组放行 51820/udp、80/tcp、443/tcp。
验证(B 上):
wg show # 确认 latest handshake 是几秒前
ping -c 3 AWIP
A 上:
ping -c 3 BWIP
ping 不通就别往下走,先修隧道。
第 2 步:在 B 上安装 Ghost(去掉 nginx 和 SSL)
B 是 Ubuntu 26.04,在 Ghost 官方支持范围内。装 MySQL 8、Node 22、Ghost-CLI:
# MySQL(注意 root 要改成密码认证,Ghost 不支持 socket 认证)
sudo apt-get update && sudo apt-get install -y mysql-server
sudo mysql
# ALTER USER 'root'@'localhost' IDENTIFIED WITH 'caching_sha2_password' BY '<密码>';
# FLUSH PRIVILEGES; exit
# Node 22
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
| sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt-get update && sudo apt-get install -y nodejs
# Ghost-CLI
sudo npm install ghost-cli@latest -g
建目录并安装:
sudo mkdir -p /var/www/ghostblog
sudo chown $USER:$USER /var/www/ghostblog
sudo chmod 775 /var/www/ghostblog
cd /var/www/ghostblog
ghost install
问答按这个填:
| 问题 | 填什么 |
|---|---|
| Blog URL | https://www.test.com(必须带 https) |
| MySQL hostname | 回车(localhost) |
| MySQL 用户/密码 | root + 刚设的密码 |
| 数据库名 | ghostblog |
| 建独立 MySQL 用户 | Yes |
| Set up NGINX? | No(A 上已经有 nginx 了) |
| Set up SSL? | No(证书在 A 上) |
| Set up systemd? | Yes |
| Start Ghost? | Yes |
然后改监听地址,让 A 能连进来。编辑 /var/www/ghostblog/config.production.json:
{
"url": "https://www.test.com",
"server": {
"host": "0.0.0.0",
"port": 2368
}
}
ghost restart
为什么用0.0.0.0而不是BWIP:如果用 BWIP,B 重启时 wg0 还没起来,Ghost bind 会失败并进入重启循环。用0.0.0.0+ 防火墙限制来源更稳。
B 上只放行 A 的隧道 IP:
sudo ufw allow from AWIP to any port 2368 proto tcp
sudo ufw reload
第 3 步:A 上配置 nginx 反代(先 80,后 443)
A 是公网入口,要做四件事:占住 80/443 → 反代到隧道 → 上证书 → 验证。下面按执行顺序给全,命令都写在注释里,你直接照抄;BWIP 换成你实际的隧道内网 IP(如 10.0.0.2)。
说明:下面的路径用/etc/nginx/conf.d/,因为 Ubuntu/Debian 和 RHEL 系(Anolis/CentOS/Alibaba Cloud Linux)都认这个目录,能省掉sites-available的差异。包管理命令两种给全,按cat /etc/os-release的结果选一种。
阶段 0:动手前先取证
在 A 上依次跑,每条都看输出,别跳:
# 1) A 是什么系统、有没有 SELinux
cat /etc/os-release | head -3
getenforce 2>/dev/null || echo "no selinux"
# 2) 域名是否已经解析到 A 的公网 IP(两边要一致)
dig +short www.test.com
curl -4 -s ifconfig.me; echo
# 3) WireGuard 隧道通不通
wg show
ping -c 3 BWIP
# 4) B 上的 Ghost 是否活着(关键前置)
curl -I --max-time 5 http://BWIP:2368
# 5) 80/443 有没有被别的程序占了
sudo ss -lntp | grep -E ':(80|443)\b'
判读标准:
- 第 2 条:
dig返回的 IP 必须等于curl ifconfig.me的结果。不等就先改 DNS,证书签发会卡在这。 - 第 3 条:
wg show里latest handshake要是几十秒内;ping丢包 0。 - 第 4 条:返回
301且 Location 指向https://www.test.com是最理想的结果——说明 Ghost 活着、url也配对。返回Connection refused就先回 B 处理,A 这边做什么都没用。 - 第 5 条:如果 80/443 已被 宝塔/apache/caddy/docker 容器 占用,先决定是停掉还是复用,别硬装。
getenforce如果返回Enforcing,记住,阶段 4 大概率要在 SELinux 上补一脚。
阶段 1:装 nginx 并放行端口
# ---- Debian / Ubuntu ----
sudo apt-get update
sudo apt-get install -y nginx
sudo systemctl enable --now nginx
# ---- RHEL / Anolis / CentOS / Rocky ----
sudo dnf install -y nginx
sudo systemctl enable --now nginx
验证:
nginx -v
curl -I http://127.0.0.1 # 期望 HTTP/1.1 200,欢迎页
防火墙(按你实际启用的那个做,两个都没开就跳过,但云厂商安全组必须单独开):
# ufw
sudo ufw allow 'Nginx Full'
sudo ufw status verbose
# firewalld
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-services
云控制台安全组入方向放行:80/tcp、443/tcp,源 0.0.0.0/0。这一步漏了,后面 certbot 一定失败,而且现象是"卡住等超时",很容易误判成 DNS 问题。
阶段 2:先配 80 反代,跑通再上证书
这一步刻意不碰 443,让排查只有"反代通不通"一个变量。
2.1 全局 upgrade 映射
新建 /etc/nginx/conf.d/00-upgrade-map.conf:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
00- 前缀是让它在按字母序加载时排在站点配置前面。这个 map 必须放在 http 块内(conf.d 里的文件天然就在 http 块内,所以直接放就行)。
2.2 站点配置
新建 /etc/nginx/conf.d/www.test.com.conf:
server {
listen 80;
server_name www.test.com;
access_log /var/log/nginx/www.test.com.access.log;
error_log /var/log/nginx/www.test.com.error.log warn;
client_max_body_size 50m; # 传主题/大图,不够会 413
location / {
proxy_pass http://BWIP:2368; # ← 换成实际隧道 IP
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https; # 关键,别写成 $scheme
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off; # Ghost 后台有 SSE,必须关
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_redirect off;
}
}
X-Forwarded-Proto这里写死https而不是$scheme,是因为这个 80 的 server 块将来会被 certbot 改成"80 只做跳转",真正的业务请求全走 443。写死可以避免 certbot 改配置后语义漂移导致 301 循环。
2.3 加载并验证
sudo nginx -t
sudo systemctl reload nginx
# 本机带 Host 头打一下
curl -I -H 'Host: www.test.com' http://127.0.0.1
期望 HTTP/1.1 200。如果拿到的不是 200:
# 看请求到底进了哪个 server 块、反代有没有报错
sudo tail -n 20 /var/log/nginx/www.test.com.access.log
sudo tail -n 20 /var/log/nginx/www.test.com.error.log
- 如果 access log 里根本没有你这次的记录 → 请求被别的
server(比如sites-enabled/default)抢走了,检查有没有default_server冲突。 - 如果 error log 里是
connect() failed (111: Connection refused)→ B 上 Ghost 没起或没监听0.0.0.0。 - 如果 error log 里是
connect() failed (13: Permission denied)→ 这是 SELinux,跳到下面「阶段 4 的 SELinux 补丁」。
阶段 3:签证书,切到 443
# ---- Debian / Ubuntu ----
sudo apt-get install -y certbot python3-certbot-nginx
# ---- RHEL / Anolis(需要 EPEL)----
sudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx
# 签发(HTTP-01 验证走 80 端口)
sudo certbot --nginx -d www.test.com --redirect --agree-tos -m you@example.com
--redirect 会让 certbot 自动把 80 的 server 块改成 301 跳 https,并新增 443 的 server 块。
签完,务必人工核对生成的 443 配置有没有把 proxy 头带过来(certbot 是复制原 server 块,正常情况下 location 会一起过去,但版本差异下偶尔会丢):
sudo nginx -T | sed -n '/server_name www.test.com/,/^}/p'
对照检查 443 的 location 里这四行必须都在:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
最终 443 块应该长这样(可作参照):
server {
listen 443 ssl;
server_name www.test.com;
ssl_certificate /etc/letsencrypt/live/www.test.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.test.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
access_log /var/log/nginx/www.test.com.access.log;
error_log /var/log/nginx/www.test.com.error.log warn;
client_max_body_size 50m;
location / {
proxy_pass http://BWIP:2368;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_redirect off;
}
}
sudo nginx -t && sudo systemctl reload nginx
续期必须验证一次,否则 90 天后全站证书过期:
sudo certbot renew --dry-run
systemctl list-timers | grep -i certbot
阶段 4:全链路验证 + SELinux 补丁
4.1 验证
# 证书有效期与域名
openssl s_client -connect www.test.com:443 -servername www.test.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -dates
# 首页
curl -I https://www.test.com
# 后台
curl -s -o /dev/null -w '%{http_code}\n' https://www.test.com/ghost
# 80 是否已跳 443
curl -I http://www.test.com
期望:首页 200、/ghost 200(或 302 到登录页)、http:// 返回 301 Location: https://www.test.com。
最后浏览器打开 https://www.test.com/ghost,应该能正常加载登录页。
4.2 SELinux 补丁(RHEL 系必看)
如果 getenforce 是 Enforcing,且 error log 里出现 Permission denied 或者直接 502/403,执行:
# 允许 nginx 主动发起出站连接(反代到 2368 需要)
sudo setsebool -P httpd_can_network_connect 1
# 确认
getsebool httpd_can_network_connect
sudo systemctl restart nginx
这是 RHEL 系反代最经典的坑:nginx 能监听 80/443,但不被允许连出去,现象是 502 而 B 那边完全正常。
4.3 排查对照表
| 现象 | 大概率原因 | 定位命令 |
|---|---|---|
| 502 Bad Gateway | Ghost 没起 / 只听 127.0.0.1 / ufw 挡 / SELinux | curl http://BWIP:2368、ghost log(B 上)、getenforce |
ERR_TOO_MANY_REDIRECTS |
443 块缺 X-Forwarded-Proto https,或 B 的 url 写成 http |
nginx -T 对照 |
| 403 Forbidden | SELinux 未放行出站 | sudo tail /var/log/audit/audit.log |
| certbot 卡住/超时 | 安全组没开 80、DNS 未生效、国内未备案 | dig +short www.test.com、curl -I http://www.test.com |
| 上传主题 413 | client_max_body_size 太小 |
改大后 reload |
| 后台操作卡、事件不刷新 | proxy_buffering 没关 |
看配置 |
| 504 Gateway Timeout | 超时太短 | proxy_read_timeout |
阶段 5:可选加固
按需加,不是必须:
# 放在 443 的 server 块里
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
server_tokens off; # 放 http 块,隐藏版本号
如果 A 前面还套了 Cloudflare 之类的 CDN,要加 real_ip 模块并 set_real_ip_from 对应回源段、real_ip_header CF-Connecting-IP,否则 Ghost 看到的全是 CDN 的 IP,登录限速会误伤。
不建议现在就去限制 /ghost 的访问来源——家庭宽带 IP 会变,容易把自己锁在外面。真要做,等站点稳定后用"仅允许特定 IP + 保留一个后门路径"的方式。
最小执行顺序(TL;DR)
- 阶段 0 五条检查全绿(尤其
curl http://BWIP:2368通) - 装 nginx + 安全组开 80/443
- 写
00-upgrade-map.conf和www.test.com.conf(80 反代)→nginx -t→ reload curl -H 'Host: www.test.com' http://127.0.0.1拿到 200certbot --nginx -d www.test.com --redirect- 核对 443 块四个 proxy 头 → reload
certbot renew --dry-run+ 浏览器/ghost
川公网安备51010802033276号