Nginx Proxy Manager 部署与踩坑
把内网几台机器的 HTTPS 反代统一收到 Nginx Proxy Manager (NPM)。之前手写 nginx + acme.sh 续期用了四年,撑到 5+ 域名开始漏续期,换成 NPM。
部署
- 主机:Debian 12 + Docker,数据根
/vol1/1000/docker/npm/ - 镜像:
jc21/nginx-proxy-manager:2.15.1 - 端口:HTTP {http-port}→80 / HTTPS {https-port}→443 / Admin {admin-port}→81
- 数据库:外部 PostgreSQL(专用账号 npm/库 npm)
- 证书:DNSPod DNS-01 签 wildcard
端口偏移 {offset} 是有意的,80/443 留给将来可能要直连的服务,混部的时候一眼分得清。
1 | services: |
几点说明:
- 外部 postgres 比容器自带的 SQLite/MariaDB 好用。密码走密码管理工具,备份走统一 postgres 流程,跨机器迁移不用倒 sqlite 文件。
- 镜像必须 2.15.1+,旧版配
DB_POSTGRES_*会回退到 SQLite。这是 #4389,2.15.1 修的。 - 持久化目录 owner 用
1000:1001(app uid),否则容器起来写不进数据。
初始化
建库建用户(一次性):
1 | CREATE ROLE npm LOGIN PASSWORD '{generated}'; |
不要图省事直接用 postgres admin 账号连库,专用账号权限有限,被拖库损失小,审计也清晰。
首次登录 Admin Web:http://{host}:{admin-port},默认账号 admin@example.com / changeme,登录后马上改密码和邮箱。
踩坑
下面这几个坑是这次实际部署花时间最多的,坑 1 和坑 2 最容易被新手踩。
坑 1:跨 docker 网络反代超时
最早在 proxy host 里填后端地址 {host-ip}:{backend-port},报 502。换容器 IP 也 502,换 127.0.0.1:{backend-port} 也不行。
三个地址都不通,原因不一样:
- 宿主机 IP:docker-proxy NAT 在宿主机本机会回环,容器访问走不通
- 后端容器 IP:默认情况下不同 docker bridge 网络间容器 IP 不可达
- 127.0.0.1:是 NPM 容器自己,不是被反代服务
后来换成 docker 主机网关 172.28.0.1(被反代服务所在 bridge 网络的 gateway IP),通了。
排查可以用这个命令起临时容器共享目标网络:
1 | docker run --rm --network {后端bridge} alpine:3.20 \ |
如果这个通,NPM 用同样的 172.28.0.1:{backend-port} 就稳。
坑 2:/api/ 502 + IP Ranges 卡死
WebUI 起来后所有 API 返回 502,日志最后一行停在 Fetching https://ip-ranges.amazonaws.com/ip-ranges.json,backend 进程根本没 listen 3000。
翻了下源码和 issue,NPM 启动时从 AWS 拉 IP ranges 段(用于访问控制),https.get 没设 timeout,出网挂起后 Promise 永不 resolve,app.listen(3000) 永不执行。前端 nginx 起来了,后面的 node 没起来。
compose 里加 IP_RANGES_FETCH_ENABLED: 'false'(NPM 官方支持这个开关)就好。
判断启动完成:
1 | docker logs {npm容器名} 2>&1 | grep "Backend PID" |
坑 3:NPM 容器访问宿主物理 IP
proxy host 配 {host-ip}:端口 也是超时。
和坑 1 是同一个 docker 网络问题,但容易误以为是网络通的问题。ping {host-ip} 通,但 TCP 业务层不通。
正解还是 172.28.0.1。
坑 4:DB_POSTGRES_* 配了却用 SQLite(Issue #4389)
明明在 compose 填了 PG 变量,日志看却是 SQLite。
NPM 旧版 bug,识别不到 DB_POSTGRES_* 时 fallback 到 SQLite,升到 2.15.1+ 就好。
坑 5:Let’s Encrypt HTTP-01 失败
UI 里点 “Issue Certificate” 选 HTTP-01,签名失败。
NPM 容器对外暴露的 80 端口({http-port})只有走到公网时才被 Let’s Encrypt 流量找到。如果机器不在公网、或者 DNSPod 配的记录还没生效,HTTP-01 一定失败。
wildcard 证书走 DNS-01。腾讯云 DNSPod 配 dns_tencentcloud_secret_id / secret_key,签 *.{yourdomain}.com,一次覆盖所有子域,不需要 80 端口。
坑 6:端口冲突
docker compose up 报 bind: address already in use。见上面的端口偏移约定,{http-port}/{https-port}/{admin-port} 这套基本不和别的服务撞。
502 排障速查
排 502 一般几步:
curl http://{host}:{admin-port}/api/看返回啥。502 是 backend 没起来(nginx 起来但 node 没 listen 3000),200 是 backend 正常,问题在前端反代配置- 看日志最后一行。停在
Fetching https://ip-ranges.amazonaws.com/ip-ranges.json是坑 2,加IP_RANGES_FETCH_ENABLED=false。停在Error: connect ECONNREFUSED是坑 1,检查后端地址是不是172.28.0.1:端口 - 正常标志:
Backend PID xxx listening on port 3000+/api/返回{"status":"OK","setup":true,...}
DNS-01 证书配置(DNSPod 实践)
UI 里建证书的步骤:
- Add SSL Certificate → Let’s Encrypt
- Domain Names:
*.{yourdomain}.com(泛域) - DNS Challenge: 勾选
- DNS Provider: 选
tencentcloud - Credentials:
1
2dns_tencentcloud_secret_id = {SecretId}
dns_tencentcloud_secret_key = {SecretKey} - Agree to Let’s Encrypt ToS + Email(用于过期提醒)
签发成功后自动带 SAN 覆盖所有子域。续期 90 天,NPM 自动跑。
反代配置(API 方式)
UI 点点点也能配,但 API 更适合批量或自动化。
创建 proxy host:
1 | POST /api/nginx/proxy-hosts |
证书 ID 从 GET /api/nginx/certificates 拿。
注意:API 创建证书有时会报 socket.timeout,但点 UI 一定能成功。签证书走 UI,配 proxy host 走 API。
验收
1 | # 1. 后端服务起来 |
三条都通过才算真上线。
收尾
坑 1 和坑 2 是这次最大的坎。前一个是 docker 网络模型没想清楚,跨 bridge 网络时容器之间靠宿主机网关走;后一个是 NPM 启动顺序里 AWS 那次 fetch 没设超时,扯出一串 promise 链全挂死。没踩过这两个的人第一次配估计得一晚上。
接的是密码管理、DNSPod、外部 postgres。从部署到今天差不多半年,没出过问题。