← 返回文章列表
技术

用 Tailscale 和双层 Nginx 把本地 Boker 发布到公网

云服务器负责域名与 HTTPS,通过 Tailscale 直连本机;两层 Nginx 正确传递主机和协议头,让 Boker 同时支持公网域名与 Tailnet 地址访问。

Boker 不一定要连数据库和上传目录一起搬到云上。这次发布里,云服务器只负责域名、HTTPS 和公网流量入口;Boker、PostgreSQL 与上传文件仍然由本机承载。两端通过 Tailscale 直连,不必把家庭宽带的入站端口暴露到公网。

这篇记录最终链路、两层 Nginx 如何传递主机与协议,以及一次证书路径看似合理却导致 Nginx 无法启动的故障。其中 Tailnet 地址都用语义化占位符表示,复用配置时应替换为自己的地址。

最终链路:云端管入口,本机管应用

公网浏览器
  -> 云服务器 Nginx :443
  -> Tailscale 加密链路
  -> 本机 Nginx :80
  -> Boker 127.0.0.1:3000

云端 Nginx 终止 TLS,把请求发到本机的 Tailscale 地址。本机 Nginx 是内部边界:它只允许云服务器、本机 Tailnet 地址与 loopback 进入,然后转发给只监听 127.0.0.1:3000 的 Boker。

这种拆分保留了本机数据库和上传文件的现有运行方式,也让云服务器只需维护 DNS、证书和反向代理。代价是两端都要正确配置,而且本机与 Tailnet 必须保持在线。

云端 Nginx:统一入口并覆盖代理头

云端同时接收根域名与 www 域名。所有 HTTP 请求、以及根域名的 HTTPS 请求,都跳转到 https://www.jinzhao.net.cn;只有 www 的 HTTPS 入口代理到本机。

server {
    listen 80;
    listen [::]:80;
    server_name jinzhao.net.cn www.jinzhao.net.cn;

    return 301 https://www.jinzhao.net.cn$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name jinzhao.net.cn;

    ssl_certificate <证书地址路径>/*.pem;
    ssl_certificate_key <证书地址路径>/*.pem;

    return 301 https://www.jinzhao.net.cn$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.jinzhao.net.cn;

    ssl_certificate <证书地址路径>/*.pem;
    ssl_certificate_key <证书地址路径>/*.pem;

    client_max_body_size 11m;

    location / {
        proxy_pass http://<本机 Tailscale IP>:80;
        proxy_http_version 1.1;

        proxy_set_header Host www.jinzhao.net.cn;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
    }
}

proxy_set_header 在公网边界上是覆盖写入,不盲信访客自带的同名请求头。Host 固定为规范域名,X-Real-IP 记录公网客户端,X-Forwarded-For 也用 $remote_addr 覆盖来访头,只向下一跳传递 Nginx 实际看到的客户端地址;X-Forwarded-Proto 则明确表示浏览器使用 HTTPS。11m 比应用允许的 10 MiB 图片略大,给 multipart 边界和其他表单字段留出余量。

本机 Nginx:限制来源并保留外部协议

第二层代理不能用自己看到的 $scheme 无条件覆盖协议。云端到本机这一段是 HTTP,但浏览器到云端是 HTTPS;如果覆盖成 http,Boker 会还原出错误的外部来源。

map $http_x_forwarded_proto $boker_forwarded_proto {
    default $scheme;
    http    http;
    https   https;
}

server {
    listen 80;
    listen [::]:80;
    server_name _;

    allow <云服务器 Tailscale IP>;
    allow <本机 Tailscale IP>;
    allow 127.0.0.1;
    allow ::1;
    deny all;

    client_max_body_size 11m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $boker_forwarded_proto;
    }
}

map 只接受明确的 http 或 https,其他值和缺失值都回退到当前连接的 $scheme。因为入口已用 allow/deny 限制为可信的 Tailnet 节点和 loopback,云端写入的 X-Real-IP 与 X-Forwarded-For 会由 Nginx 继续传给上游,不再被本机地址替换。

Boker 环境:规范地址与本机管理入口

Boker 的规范站点地址是公网 HTTPS 域名,用于 canonical、sitemap 等对外链接。站长仍可在 Tailnet 内通过本机 HTTP 地址管理站点,因此把该来源单独列入写入白名单:

SITE_URL=https://www.jinzhao.net.cn
ADMIN_HTTP_ORIGINS=http://<本机 Tailscale IP>

SITE_URL 不是“所有后台请求只能来自这里”的开关。后台保存、登录与图片上传还会将浏览器的 Origin 与代理还原出的 Host 和 X-Forwarded-Proto 比较。这正是两层代理必须保留规范主机、并把外部 HTTPS 传到应用层的原因。

故障:证书域名不等于证书目录名

第一版配置把证书路径写成了 <证书地址路径>/*.pem。这个名字看起来与公网主机匹配,但它并不存在,nginx -t 因此报 BIO_new_file() failed。实际有效目录是 <证书地址路径>。

先用只读命令对照 Nginx 引用与 Certbot 实际创建的目录,再修正配置:

sudo grep -RIn '<证书地址路径>' /etc/nginx
sudo ls -la <证书地址路径>
sudo nginx -t

不要为了迎合错误配置,将私钥复制到一个新目录。证书的续期和符号链接都由 Certbot 管理;手工副本不会随续期更新,还会扩大私钥暴露面。应修正 Nginx 路径,并且每次 reload 前都先执行 nginx -t。

分层验收:从上游到真实写入

验收按数据流逐层向外推进,这样出错时能立即知道问题在哪一段:

  1. 在云端通过 Tailnet 请求本机 Nginx,确认上游可达、本机访问规则命中,并能获得 Boker 响应。
  2. 从公网请求根域名和 HTTP 入口,确认它们都跳转到 https://www.jinzhao.net.cn,证书链和公开文章页正常。
  3. 在公网 HTTPS 入口完成后台登录,编辑并保存一篇测试文章,确认主机、协议、来源校验和 Cookie 都正常。
  4. 分别上传 1 MiB 与 10 MiB 图片,再打开正文中的图片,确认两层 client_max_body_size 和媒体读取链路都通过。
  5. 重启 Boker、本机 Nginx 与云端 Nginx,必要时重启本机,再重复公开文章、后台登录、保存和图片读取,确认服务会自动恢复。

只检查首页的 200 状态码不能代替这些步骤。公开读取、受保护写入、大请求体与重启恢复覆盖的是不同故障面。

上线后还要守住的边界

Tailscale 让两台机器之间的传输加密,但它不会自动代替所有访问控制。本机 Nginx 仍应只允许需要的 Tailnet 节点,Boker 继续只监听 loopback,云服务器的公网防火墙只开放必要端口。如果 Tailnet 还有其他成员,再用 Tailscale ACL 限制谁能访问本机 80 端口。

数据仍在本机,因此 PostgreSQL 与上传目录的定期备份比迁移前更重要,且备份要实际演练恢复。监控也不应只盯云端 Nginx:还要观察 Tailnet 连通性、本机 Nginx、Boker 进程、数据库与磁盘空间。双层代理的目标是把责任分开,验收和日常运维也要按同样的层次进行。