Skip to content

小内存海外 VPS 跑 NPM 卡死:内网迁移与 L4 透传实践

仲灏2026-06-10约 1 分钟

背景

家里 homelab 通过 Tailscale 组网,公网入口是一台海外轻量 VPS(约 400MB 内存)。原先在 VPS 上直接跑 Nginx Proxy Manager(NPM) 做 HTTPS 终结和反代,同时还跑着 Tailscale、DERP 中继和 Docker。

症状很典型:SSH 偶发卡顿、DNS 解析慢、容器健康检查超时,最后只能手动重启。根因是 内存长期顶满——NPM 单容器就占近百 MB,再叠加 dockerd、Tailscale,小机器扛不住。

升级内存不可行时的思路:把 NPM 迁到内网大机器,海外 VPS 只做 TCP 透传 + Tailscale 中继


目标架构

text
公网用户
  │  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、虚拟机等)
角色部署内容说明
海外 VPSnginx-relay(stream)+ Tailscale不再跑 NPM
内网 HOMENPM + MySQL + 大部分业务内存充足,证书与反代规则集中管理
NASDSM 反代入口多个子域可共用一个 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

yaml
services:
  relay:
    image: nginx:alpine
    container_name: nginx-relay
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

nginx.conf

HOME_TAILSCALE_IP 换成内网 NPM 节点的 Tailscale 地址:

nginx
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;
    }
}
bash
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(推荐形态)

yaml
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: 180s

MySQL 可单独 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: host
  • DB_MYSQL_HOST=127.0.0.1
  • 显式设置 CERTBOT_VERSION(与镜像内 certbot 大版本一致)

Cloudflare DNS 证书插件(高发故障)

官方镜像不预装 certbot-dns-cloudflare。若面板卡住,进容器用国内镜像源安装(版本号按你设置的 CERTBOT_VERSION 调整):

bash
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 和密码):

bash
# 从海外拉 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 HostForward PortScheme
git.example.com127.0.0.1 或 HOME 的 TS IP43000http
ci.example.com同上8080http

模式 B:NAS 统一 HTTPS 入口

群晖 DSM 反代常监听一个高端口(如 45001),多个子域都指过去,由 DSM 按 Host 再分发:

示例子域Forward HostForward PortScheme
files.example.comNAS_TAILSCALE_IP45001https
dsm.example.comNAS_TAILSCALE_IP45001https

SSL 证书在 NPM 选通配符 *.example.com 即可。

模式 C:其它 Tailscale 成员

示例子域Forward Host说明
ai.example.comMAC_TAILSCALE_IP机器离线则 502
lab.example.comVM_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 一直 loadingpip 装 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。


小结

  1. 小内存海外机不适合跑 NPM,适合做 Tailscale 入口 + L4 透传。
  2. stream 透传让 TLS 和反代规则集中在内网,运维更简单。
  3. NPM 迁移主要是 data + letsencrypt 搬家,反代记录可整体保留。
  4. network_mode: host + CERTBOT_VERSION 能避开 bridge 下 pip 失败导致的面板假死。
  5. 多后端用 Tailscale IP 在 NPM 里分流即可,NAS 类设备适合「同端口 + Host 路由」。

延伸阅读

本文为个人 homelab 实践脱敏整理,文中 IP、域名、路径均为示例,请按自己的环境替换。

讨论区

欢迎留下想法与补充