背景
家里 homelab 通过 Tailscale 组网,公网入口是一台海外轻量 VPS(约 400MB 内存)。原先在 VPS 上直接跑 Nginx Proxy Manager(NPM) 做 HTTPS 终结和反代,同时还跑着 Tailscale、DERP 中继和 Docker。
症状很典型:SSH 偶发卡顿、DNS 解析慢、容器健康检查超时,最后只能手动重启。根因是 内存长期顶满——NPM 单容器就占近百 MB,再叠加 dockerd、Tailscale,小机器扛不住。
升级内存不可行时的思路:把 NPM 迁到内网大机器,海外 VPS 只做 TCP 透传 + Tailscale 中继。
目标架构
公网用户
│ DNS → 海外 VPS 公网 IP
▼
海外 VPS(~400MB)
│ nginx stream:80/443 原样转发,不解密 TLS
│ tailscale + derper(可选)
▼
内网 NPM 节点(Tailscale IP,记为 HOME)
│ 终结 HTTPS + 按域名反代
├─► HOME 本机服务(Git、CI、对象存储等)
├─► NAS 节点(Tailscale IP,记为 NAS)
└─► 其它 Tailscale 成员(Mac、虚拟机等)| 角色 | 部署内容 | 说明 |
|---|---|---|
| 海外 VPS | nginx-relay(stream)+ Tailscale | 不再跑 NPM |
| 内网 HOME | NPM + MySQL + 大部分业务 | 内存充足,证书与反代规则集中管理 |
| NAS | DSM 反代入口 | 多个子域可共用一个 HTTPS 端口,靠 Host 区分 |
| 其它节点 | 按需 | NPM 里填对应 Tailscale IP 即可 |
DNS:*.your-domain.com 的 A 记录指向海外 VPS 公网 IP。Cloudflare 橙云可按子域单独开关。
为什么用 L4 透传,而不是在海外继续反代
| 方案 | 优点 | 缺点 |
|---|---|---|
| 海外 NPM 反代到 Tailscale | 配置熟悉 | 吃内存;证书在海外维护一份 |
海外 Nginx proxy_pass 到内网 | 可做七层 | 仍要在海外配证书或二次解密 |
| 海外 stream 透传 | 海外进程极轻;证书只在 HOME | 海外不能按域名分流,全部交给内网 NPM |
对小内存 VPS,stream 透传是最省资源的做法:海外 nginx 只做 TCP 转发,TLS 在内网 HOME 终结。
海外 VPS:nginx-relay
目录示例:/srv/nginx-relay/
docker-compose.yml
services:
relay:
image: nginx:alpine
container_name: nginx-relay
restart: unless-stopped
network_mode: host
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ronginx.conf
将 HOME_TAILSCALE_IP 换成内网 NPM 节点的 Tailscale 地址:
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
stream {
upstream npm_https {
server HOME_TAILSCALE_IP:443;
}
upstream npm_http {
server HOME_TAILSCALE_IP:80;
}
server {
listen 443;
listen [::]:443;
proxy_pass npm_https;
proxy_connect_timeout 10s;
}
server {
listen 80;
listen [::]:80;
proxy_pass npm_http;
proxy_connect_timeout 10s;
}
}cd /srv/nginx-relay
docker compose up -d安全提示:NPM 管理口(默认 :81)不要映射到公网;仅通过 Tailscale 访问 http://HOME_TAILSCALE_IP:81。
内网 HOME:NPM
目录示例:/srv/nginx-proxy-manager/
docker-compose.yml(推荐形态)
services:
app:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
container_name: nginx-proxy-manager
network_mode: host
environment:
TZ: Asia/Shanghai
CERTBOT_VERSION: "5.6.0"
DB_MYSQL_HOST: 127.0.0.1
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: root
DB_MYSQL_PASSWORD: ${NPM_DB_PASSWORD}
DB_MYSQL_NAME: npm
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 180sMySQL 可单独 compose,把 3306 发布到宿主机;NPM 用 127.0.0.1 连接。
为何 NPM 建议 network_mode: host
其它业务容器常用自定义 bridge(如 db-bridge)+ 主机名 mysql,但 NPM 有个坑:
- 数据库里若配置了 Cloudflare DNS 验证 的证书,容器每次启动会在线
pip install certbot-dns-cloudflare。 - 在隔离 bridge 网络里,访问 PyPI 容易失败 → Node 后端起不来 → 面板
:81一直 loading(API 502)。
实测稳定组合:
network_mode: hostDB_MYSQL_HOST=127.0.0.1- 显式设置
CERTBOT_VERSION(与镜像内 certbot 大版本一致)
Cloudflare DNS 证书插件(高发故障)
官方镜像不预装 certbot-dns-cloudflare。若面板卡住,进容器用国内镜像源安装(版本号按你设置的 CERTBOT_VERSION 调整):
docker exec nginx-proxy-manager bash -c \
'SETUPTOOLS_USE_DISTUTILS=local . /opt/certbot/bin/activate && \
pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple \
acme==5.6.0 certbot-dns-cloudflare==5.6.0 cloudflare'
cd /srv/nginx-proxy-manager && docker compose restart长期方案:自建 Dockerfile,在构建阶段预装 DNS 插件,避免每次 docker compose recreate 都卡在 pip。
迁移步骤(可照抄流程)
| 步骤 | 操作 |
|---|---|
| 1 | 在海外 VPS 打包 NPM 的 data/、letsencrypt/ |
| 2 | 传到内网 HOME,放到 NPM 目录 |
| 3 | 内网启动 NPM,确认 MySQL 库 npm 中反代记录完整 |
| 4 | 海外 docker compose down 停掉旧 NPM |
| 5 | 海外部署 nginx-relay,透传 80/443 |
| 6 | 公网抽测几个 HTTPS 子域 |
| 7 | 若面板 loading,按上文安装 Cloudflare 插件 |
| 8 | 按需把 NAS 等子域在 NPM 里指到其它 Tailscale IP |
同步示例(在内网机器上拉取,替换 IP 和密码):
# 从海外拉 data + letsencrypt(示例)
ssh user@OVERSEAS_PUBLIC_IP 'tar czf - -C /srv/nginx-proxy-manager data letsencrypt' \
| tar xzf - -C /srv/nginx-proxy-manager多后端分流:一种常见模式
NPM 的 Forward Hostname 填 Tailscale IP,不必所有域名都指 HOME。
模式 A:服务跑在 HOME 本机
| 示例子域 | Forward Host | Forward Port | Scheme |
|---|---|---|---|
git.example.com | 127.0.0.1 或 HOME 的 TS IP | 43000 | http |
ci.example.com | 同上 | 8080 | http |
模式 B:NAS 统一 HTTPS 入口
群晖 DSM 反代常监听一个高端口(如 45001),多个子域都指过去,由 DSM 按 Host 再分发:
| 示例子域 | Forward Host | Forward Port | Scheme |
|---|---|---|---|
files.example.com | NAS_TAILSCALE_IP | 45001 | https |
dsm.example.com | NAS_TAILSCALE_IP | 45001 | https |
SSL 证书在 NPM 选通配符 *.example.com 即可。
模式 C:其它 Tailscale 成员
| 示例子域 | Forward Host | 说明 |
|---|---|---|
ai.example.com | MAC_TAILSCALE_IP | 机器离线则 502 |
lab.example.com | VM_TAILSCALE_IP | 端口按实际服务填写 |
日常新增域名请走 NPM Web UI;直接改 MySQL 或手写 proxy_host/*.conf 仅作应急,容易被 UI 保存覆盖。
海外 VPS 内存优化(仍跑中继时)
vm.swappiness=60(原0时 swap 几乎不用,更容易 OOM)- 给仍运行的容器设
mem_limit - 不在小 VPS 上跑 IDE Remote / 重型 agent
docker image prune定期清理- 确认 只有一处 监听公网 443(relay 与 NPM 不可并存)
故障排查速查
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 海外 SSH 卡、频繁重启 | 内存不足 | 停 NPM,仅保留 relay + tailscale |
| 公网全站 502 | 内网 NPM 或海外 relay 未运行 | docker ps 两边都查 |
:81 一直 loading | pip 装 certbot 插件失败 | 国内源手动安装 + CERTBOT_VERSION |
| 单域名 502 | 目标 Tailscale 节点离线或端口错 | tailscale status + 核对 NPM 记录 |
| 新子域不通 | DNS 未指到海外 IP 或 NAS 反代未配 | Cloudflare A 记录 + DSM 规则 |
recreate 后面板又挂 | 插件未 baked 进镜像 | 重装插件或自定义 Dockerfile |
| NPM stream 与业务抢端口 | data/nginx/stream/*.conf 冲突 | 禁用重复监听或改业务端口 |
证书放在哪
| 项目 | 说明 |
|---|---|
| 文件目录 | NPM 的 letsencrypt/ |
| 数据库 | npm.certificate |
| 公网终结 | 仅内网 HOME 的 NPM |
| DNS 验证 | 若用 Cloudflare,依赖 certbot-dns-cloudflare |
| 续期 | 只在 HOME 的 NPM 面板操作 |
海外 relay 不持有证书,切换内网机器时只要改 stream 上游 IP。
小结
- 小内存海外机不适合跑 NPM,适合做 Tailscale 入口 + L4 透传。
- stream 透传让 TLS 和反代规则集中在内网,运维更简单。
- NPM 迁移主要是
data+letsencrypt搬家,反代记录可整体保留。 network_mode: host+CERTBOT_VERSION能避开 bridge 下 pip 失败导致的面板假死。- 多后端用 Tailscale IP 在 NPM 里分流即可,NAS 类设备适合「同端口 + Host 路由」。
延伸阅读
- Docker docker-compose 笔记
- https 配置
- Tailscale 官方文档:Subnet routers / DERP
本文为个人 homelab 实践脱敏整理,文中 IP、域名、路径均为示例,请按自己的环境替换。

讨论区
欢迎留下想法与补充