一台 VPS 上跑了 Uptime Kuma、Jellyfin、n8n、博客和几个 Docker 应用后,最麻烦的通常不是把容器启动起来,而是给每个服务分配域名、申请 HTTPS、处理 WebSocket,再记住几十段 Nginx 配置。
Nginx Proxy Manager,简称 NPM,就是把这部分工作做成网页管理界面。你可以用 status.example.com 指向监控面板,用 media.example.com 指向媒体服务器,再为每个域名单独申请 Let's Encrypt 证书,不需要每次手写完整的 Nginx Server Block。
本文使用当前稳定版 Nginx Proxy Manager 2.15.1 和 MariaDB 11.4.12。需要特别提醒:网上很多旧教程还在使用 [email protected] / changeme,当前版本首次启动默认进入初始化向导,不应该继续依赖这组旧账号。
NPM 比较适合下面这些需求:
- 一台 VPS 上有多个 Web 服务,需要按子域名分流;
- 不想为每个应用手写 Nginx 配置;
- 希望通过网页申请、续期和查看 HTTPS 证书;
- 家庭实验室或小团队需要统一管理反向代理;
- Docker 应用会频繁增加、迁移或更换端口;
- 需要简单的 Basic Auth、Access List 或 WebSocket 支持。
下面几种情况不一定要装 NPM:
- 只有一个网站,Caddy 两三行配置已经够用;
- 配置全部由 Git、Ansible 或 CI/CD 管理,不希望在网页里修改;
- 需要复杂流量治理、动态服务发现或 Kubernetes Ingress;
- 希望它直接替代 Cloudflare WAF、应用鉴权或零信任网关;
- 管理的不是 HTTP/HTTPS 服务,而是大量复杂四层协议。
NPM 的核心仍然是 Nginx。网页界面降低了配置门槛,但不会自动解决应用本身的权限、漏洞、数据库备份和访问控制问题。
| 方案 | 优点 | 缺点 | 更适合谁 |
|---|---|---|---|
| Nginx Proxy Manager | 图形界面、证书管理直观、上手快 | 多一个管理后台和数据库 | 多应用、家庭实验室、小团队 |
| Caddy | 配置短、自动 HTTPS、维护简单 | 图形化管理弱 | 喜欢配置文件、服务数量不多 |
| 原生 Nginx | 灵活、生态成熟、细节可控 | 学习和维护成本更高 | 复杂生产配置和自动化运维 |
如果你习惯配置文件,而且只代理三五个服务,VPS 用 Caddy 反向代理完全指南通常更轻。需要让非运维同事也能新增域名,或者服务经常变化,NPM 会省事很多。
NPM 本身不算重,真正占资源的是它代理的应用。
| 场景 | 推荐配置 | 说明 |
|---|---|---|
| 5 个以内轻量应用 | 1 核 1GB、20GB SSD | 只运行 NPM 和少量低流量服务 |
| 10–30 个应用 | 2 核 2GB、40GB SSD | 给 MariaDB、日志和证书留余量 |
| 同机运行数据库和业务 | 2–4 核、4GB+ | 按业务应用实际负载选配置 |
| 高流量网站 | 独享 CPU、充足带宽 | 重点看上游应用和网络,不只看 NPM |
需要用到三个端口:
80/tcp:HTTP 和 Let's Encrypt HTTP-01 验证;443/tcp:公开 HTTPS 流量;81/tcp:NPM 管理后台。
本文让 80 和 443 对公网开放,但把 81 只绑定到 127.0.0.1。管理后台通过 SSH 隧道访问,避免直接暴露在公网。
上线前检查端口是否已被占用:
sudo ss -lntp | grep -E ':(80|81|443)\b' || true
如果已有 Nginx、Apache、Caddy 或其他代理占用 80/443,先决定由谁接管入口,不要直接启动两个服务抢同一端口。详细排查可以参考 VPS 端口被占用怎么办。
本文使用两个 Docker 网络:
npm-internal:只连接 NPM 和 MariaDB,数据库不暴露到公网;proxy:NPM 与被代理的 Docker 应用共享,通过容器名访问上游。
外部请求路径大致是:
用户浏览器
↓ 80/443
Nginx Proxy Manager
↓ Docker proxy 网络
应用容器:内部端口
如果应用不在 Docker 中,也可以把 Proxy Host 指向 VPS 内网地址或另一台服务器的私网 IP。
先确认 Docker 和 Compose 可用:
docker version
docker compose version
没有安装时,可以先看 Docker 部署实战指南。生产服务器建议使用 Docker 官方仓库安装,不要长期使用发行版里过旧的 Compose。
创建目录和共享网络:
sudo mkdir -p /opt/nginx-proxy-manager/data
sudo mkdir -p /opt/nginx-proxy-manager/letsencrypt
sudo mkdir -p /opt/nginx-proxy-manager/mariadb
sudo chown -R "$USER":"$USER" /opt/nginx-proxy-manager
docker network create proxy
如果 proxy 网络已经存在,Docker 会提示重名,不需要重复创建。可以用下面的命令确认:
docker network inspect proxy
不要把示例里的 npm、password 或域名直接当密码。生成两组随机值:
openssl rand -base64 36
openssl rand -base64 36
在 /opt/nginx-proxy-manager/.env 中保存:
NPM_DB_ROOT_PASSWORD=替换为第一组随机密码
NPM_DB_PASSWORD=替换为第二组随机密码
限制文件权限:
chmod 600 /opt/nginx-proxy-manager/.env
密码中如果包含空格、换行或 Compose 特殊插值字符,可能导致解析失败。使用 openssl rand -hex 32 可以生成更容易放入 .env 的纯十六进制密码。
创建 /opt/nginx-proxy-manager/compose.yaml:
services:
app:
image: jc21/nginx-proxy-manager:2.15.1
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80/tcp"
- "443:443/tcp"
- "127.0.0.1:81:81/tcp"
environment:
TZ: Asia/Shanghai
DB_MYSQL_HOST: db
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: npm
DB_MYSQL_PASSWORD: ${NPM_DB_PASSWORD}
DB_MYSQL_NAME: npm
volumes:
- /opt/nginx-proxy-manager/data:/data
- /opt/nginx-proxy-manager/letsencrypt:/etc/letsencrypt
networks:
- npm-internal
- proxy
depends_on:
db:
condition: service_healthy
db:
image: linuxserver/mariadb:11.4.12
container_name: nginx-proxy-manager-db
restart: unless-stopped
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: ${NPM_DB_ROOT_PASSWORD}
MYSQL_DATABASE: npm
MYSQL_USER: npm
MYSQL_PASSWORD: ${NPM_DB_PASSWORD}
volumes:
- /opt/nginx-proxy-manager/mariadb:/config
networks:
- npm-internal
healthcheck:
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -uroot -p\"$${MYSQL_ROOT_PASSWORD}\" --silent"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
networks:
npm-internal:
internal: true
proxy:
external: true
这里固定了 NPM 和 MariaDB 版本,避免某次 docker compose pull 意外跨大版本升级。官方也支持 SQLite,个人测试确实更简单;但多个代理、证书和长期运行时,独立数据库更方便备份、检查和恢复。
检查最终配置,确保密码已被正确读取:
cd /opt/nginx-proxy-manager
docker compose config --quiet
不要把 docker compose config 的完整输出贴到公开工单,它可能展开 .env 中的数据库密码。
启动服务:
cd /opt/nginx-proxy-manager
docker compose up -d
docker compose ps
第一次启动会生成 JWT 密钥、初始化数据库和表结构,可能需要一两分钟。查看日志:
docker compose logs --tail=150 app
docker compose logs --tail=100 db
确认本机后台端口:
curl -I http://127.0.0.1:81
若刚启动时返回失败,先等数据库健康检查通过,再重新查看日志。不要因为第一次连接失败就删除数据目录重装。
因为 81 只监听本机,需要从自己的电脑建立 SSH 隧道:
ssh -L 8181:127.0.0.1:81 your_user@your_vps_ip
保持终端连接,然后浏览器访问:
http://127.0.0.1:8181
当前版本默认显示初始化向导,让你创建第一个管理员。请使用真实可接收通知的邮箱和独立随机密码。
旧版本文章常见的下面这组信息不再适合作为当前教程的默认流程:
[email protected]
changeme
如果你从旧数据目录升级,原账号仍会保留;如果全新安装却看到旧账号界面,检查是否复用了以前的 /data 或数据库。
假设要把 app.example.com 代理到一个应用:
- 在 DNS 服务商创建 A 记录,指向 VPS 公网 IPv4;
- 有 IPv6 时再创建 AAAA 记录,并确认 VPS 的 80/443 在 IPv6 防火墙中也开放;
- 等待解析生效;
- 在本地检查解析结果。
dig +short app.example.com A
dig +short app.example.com AAAA
如果 AAAA 指向错误服务器,Let's Encrypt 和部分用户可能通过 IPv6 访问失败。暂时不用 IPv6时,删除错误 AAAA 比让它一直指错更安全。
进入 NPM 后选择 Hosts → Proxy Hosts → Add Proxy Host。
基础配置示例:
| 字段 | 示例 | 说明 |
|---|---|---|
| Domain Names | app.example.com | 已解析到 VPS 的域名 |
| Scheme | http | 上游应用未启用 TLS 时使用 |
| Forward Hostname / IP | my-app | 同一 Docker 网络中的容器名 |
| Forward Port | 3000 | 应用容器内部监听端口 |
| Cache Assets | 默认关闭 | 动态应用不要盲目开启 |
| Block Common Exploits | 可开启 | 不是完整 WAF,仍需测试兼容性 |
| Websockets Support | 按应用开启 | 实时面板、终端、通知常需要 |
保存前先在 NPM 容器里测试上游:
docker exec nginx-proxy-manager getent hosts my-app
docker exec nginx-proxy-manager curl -I http://my-app:3000
如果解析不到容器名,说明两个容器没有加入同一个 proxy 网络。
应用自己的 Compose 可以这样写:
services:
my-app:
image: your-app-image:固定版本
restart: unless-stopped
expose:
- "3000"
networks:
- proxy
networks:
proxy:
external: true
expose 只声明容器内部端口,不会把 3000 直接发布到公网。外部用户只能经过 NPM 的 80/443 访问,攻击面更小。
已经运行的容器可以临时加入网络:
docker network connect proxy my-app
但容器重建后,手动连接可能丢失。长期配置应该写进应用的 Compose 文件。
如果应用直接运行在 VPS 宿主机,不在 Docker 中,不要在 NPM 里填写 127.0.0.1。NPM 容器里的 127.0.0.1 指向 NPM 容器自己,不是宿主机。
可以使用以下方案之一:
- 让应用监听宿主机私网地址,再填写该地址;
- 在 Linux Compose 中添加
host-gateway映射; - 使用同一局域网内另一台服务器的私网 IP;
- 把应用也迁入共享 Docker 网络。
host-gateway 示例:
services:
app:
extra_hosts:
- "host.docker.internal:host-gateway"
之后可以尝试使用 host.docker.internal 作为 Forward Hostname。宿主机防火墙和应用监听地址仍需要允许来自 Docker 网桥的连接。
在 Proxy Host 的 SSL 页面:
- 选择
Request a new SSL Certificate; - 开启
Force SSL; - 正常网站可以开启
HTTP/2 Support; - 填写邮箱并同意 Let's Encrypt 条款;
- 保存后检查证书状态。
HTTP-01 验证要求:
- 域名 A/AAAA 正确指向当前 VPS;
- 公网能够访问 80 端口;
- 云防火墙、UFW 和安全组允许 80/443;
- 80 端口没有被另一个反向代理占用;
- Cloudflare 或其他代理没有把请求送到错误源站。
证书申请失败时,不要连续点击十几次。Let's Encrypt 有频率限制,先修复 DNS 和端口,再重新申请。
很多管理面板、聊天应用、在线终端和开发工具需要 WebSocket。最先做的事情是在 Proxy Host 中开启 Websockets Support。
如果仍然断开:
- 确认上游应用的 WebSocket 路径;
- 查看浏览器开发者工具中的 400、403 或 502;
- 检查应用是否要求可信代理或正确的外部 URL;
- 确认 Cloudflare 的 WebSocket 和超时限制;
- 查看 NPM 与应用日志中的 Upgrade 请求。
不要一开始就在 Advanced 里粘贴来源不明的大段 Nginx 配置。NPM 已经会生成常见代理头,重复设置可能造成冲突。
NPM 的 Access List 可以做两类基础限制:
- 用户名和密码的 HTTP Basic Auth;
- 按 IP 或网段允许、拒绝访问。
它适合临时演示站、简单内部工具或第二道访问门槛,但不等于完整身份系统。下面这些场景不要只靠 Basic Auth:
- 管理云账号、密码或支付信息的后台;
- 多人协作且需要审计和单点登录;
- 公开 API、Webhook 或移动客户端;
- 需要 MFA、设备策略和细粒度权限。
更敏感的后台可以放到 WireGuard、Tailscale、Headscale 或 Cloudflare Access 后面。管理 NPM 本身时,SSH 隧道通常比公开 81 端口更直接。
技术上可以把 npm.example.com 反向代理到管理端口,但这样会把登录面暴露到公网。确实需要远程网页登录时,至少同时做到:
- 使用独立子域名;
- 开启 HTTPS;
- 配置 Access List 或外部身份网关;
- 管理员使用唯一长密码;
- 不把 81 端口直接开放;
- 设置登录告警并定期检查日志;
- 限制允许访问的来源地区或 IP 时,先准备恢复方式。
只有自己管理一台 VPS 时,继续使用 SSH 隧道即可,没有必要为了“看起来方便”增加一个公网入口。
接入 Cloudflare 时,建议先用灰云“仅 DNS”完成 NPM 和证书验证,确认源站直连正常,再开启橙云代理。
常见注意点:
- SSL/TLS 模式使用
Full (strict),不要使用 Flexible; - 源站 NPM 仍需要有效 HTTPS 证书;
- Flexible 容易造成 HTTP/HTTPS 重定向循环;
- Cloudflare 521/522 通常表示源站端口、防火墙或连接异常;
- 真实客户端 IP 需要可信代理配置,不能盲目信任任意
X-Forwarded-For; - 大文件上传、长连接和流媒体需要核对 Cloudflare 套餐限制与服务条款。
遇到循环跳转可以看 HTTPS 跳转循环排查;出现 521/522 时参考 Cloudflare 521/522 源站排查。
如果应用支持大文件,但通过 NPM 上传时出现 413 Request Entity Too Large,可以在对应 Proxy Host 的 Advanced 中按需求设置:
client_max_body_size 2g;
proxy_request_buffering off;
不要不加判断地设置成几十 GB。还要同步检查:
- 应用自己的上传限制;
- PHP、Node.js 或后端框架限制;
- Cloudflare 上传上限;
- VPS 磁盘空间和临时目录;
- 请求超时与客户端网络。
修改后用一个可控测试文件验证,不要直接拿唯一的大文件做首次测试。
先看容器输出:
cd /opt/nginx-proxy-manager
docker compose logs --tail=200 app
docker compose logs --tail=100 db
NPM 的持久化日志和生成配置保存在 /opt/nginx-proxy-manager/data。排障时可以检查:
sudo find /opt/nginx-proxy-manager/data -maxdepth 3 -type f | sort | tail -n 50
不要随便手改自动生成的 Nginx 配置文件。下一次在网页保存 Proxy Host 时,手动修改可能被覆盖。长期自定义应放在 NPM 支持的 Advanced 或自定义配置位置,并在升级前验证。
至少备份:
/opt/nginx-proxy-manager/data:用户、配置、JWT 密钥和应用数据;/opt/nginx-proxy-manager/letsencrypt:证书及账户资料;/opt/nginx-proxy-manager/mariadb:数据库文件;compose.yaml和.env;- 当前镜像版本和恢复说明。
数据库运行时直接复制文件并不一定得到一致备份。建议先导出数据库:
cd /opt/nginx-proxy-manager
docker compose exec -T db mariadb-dump \
-unpm \
-p"$NPM_DB_PASSWORD" \
npm > npm-$(date +%F).sql
上面的 Shell 需要先加载 .env:
set -a
. /opt/nginx-proxy-manager/.env
set +a
导出的 SQL、配置目录和 .env 应加密后复制到另一台机器或对象存储。只把备份留在同一块 VPS 磁盘上,不能应对磁盘损坏或账号被停用。
升级前先查看当前镜像并做备份:
cd /opt/nginx-proxy-manager
docker inspect nginx-proxy-manager --format '{{.Config.Image}}'
docker compose config > compose.before-upgrade.yaml
阅读目标版本 Release Notes,确认数据库和架构变化,再修改 compose.yaml 中的镜像标签:
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=200 app
升级后至少测试:
- 管理员登录;
- 一个普通 HTTP 上游;
- 一个 WebSocket 应用;
- HTTPS 证书状态;
- Access List;
- 数据库和证书续期日志。
不要在没有备份时从很老的版本直接跨多个大版本。镜像回退不等于数据库回退,数据库结构已经迁移时,需要同时恢复升级前的数据。
本文故意把 81 绑定到 127.0.0.1,公网访问 http://服务器IP:81 本来就会失败。先建立 SSH 隧道,再访问本机 http://127.0.0.1:8181。
服务端检查:
sudo ss -lntp | grep ':81'
curl -I http://127.0.0.1:81
最常见的原因是上游地址写错、端口写成宿主机端口、容器不在共享网络,或者应用只监听 127.0.0.1。
在 NPM 容器中测试:
docker exec nginx-proxy-manager getent hosts my-app
docker exec nginx-proxy-manager curl -v http://my-app:3000
需要更系统的检查时,参考 VPS 502/504 Bad Gateway 排查清单。
依次确认:
dig +short app.example.com A
dig +short app.example.com AAAA
sudo ss -lntp | grep -E ':(80|443)\b'
docker compose logs --tail=200 app
还要检查云防火墙和 UFW。DNS 刚修改时,可以等 TTL 生效后再申请,避免重复触发失败。
通常不是 NPM 自动清空,而是换了数据库、挂载到了新的空目录,或者 Compose 没有读取原来的 .env。检查容器实际挂载:
docker inspect nginx-proxy-manager --format '{{json .Mounts}}'
docker inspect nginx-proxy-manager-db --format '{{json .Mounts}}'
恢复前先保留当前目录副本,不要直接覆盖唯一数据。
查看 MariaDB 日志和磁盘:
docker compose logs --tail=200 db
df -h
df -i
常见原因包括密码变量不一致、旧数据库目录权限不对、磁盘满、升级跨版本过大。不要不断删除 /config 重试,否则会把可恢复的数据也删掉。
先开启 Websockets Support,然后确认应用的外部 URL、可信代理、心跳和超时配置。如果前面还有 Cloudflare、负载均衡器或另一层 Nginx,需要逐层确认是哪一段关闭连接。
- 80/443 只由一个入口代理占用;
- 81 只绑定本机、VPN 或可信管理网络;
- 管理员使用唯一随机密码,不和数据库密码复用;
- MariaDB 不映射公网端口;
- 应用容器优先使用
expose和共享网络,不直接发布管理端口; .env权限设置为 600,不上传公开仓库;- Proxy Host 只代理自己有权管理的服务;
- Basic Auth 不替代应用自身登录和 MFA;
- Cloudflare 使用 Full (strict),避免 Flexible;
- 定期检查证书、登录、代理和数据库日志;
- 升级前固定版本、备份数据库并测试恢复;
- 关闭已经不用的 Proxy Host、证书和 DNS 记录;
- 不把 NPM 当作 WAF、漏洞扫描器或完整零信任系统。
不是。官方支持 SQLite、MySQL/MariaDB 和 PostgreSQL。个人测试或少量代理可以使用 SQLite;长期运行、需要明确数据库备份和迁移时,独立数据库更容易管理。
可以,但不能同时绑定同一个宿主机 80/443。最清楚的方案是只保留一个公网入口代理,其他面板和应用放到内部端口或 Docker 网络中。
不会。它本身就运行在源站上。需要隐藏源站、身份访问或免开放入口端口时,要另外评估 Cloudflare Tunnel、VPN 或独立边缘代理。站内有 Cloudflare Tunnel 部署指南可供对比。
NPM 会管理 Let's Encrypt 证书续期,但前提是 DNS、端口、证书目录和容器运行状态一直正常。建议配置外部 HTTPS 到期监控,不要只相信“理论上会自动续”。
可以。为每个服务使用独立子域名和 Proxy Host,并根据应用开启 WebSocket、上传限制和可信代理设置。流媒体和大文件还要考虑 VPS 带宽、Cloudflare 限制和服务条款。
第一次部署时,先只添加一个不重要的测试应用。确认 DNS、HTTP、HTTPS、WebSocket 和日志都正常,再迁移正式服务。不要在同一天把十几个域名全部切换到新代理,否则出问题时很难判断是 DNS、NPM、Docker 网络还是应用配置。
对个人和小团队,NPM 最大的价值不是“比 Nginx 性能更强”,而是把域名、证书和代理配置集中管理。真正稳定的关键仍然是:只保留一个入口、上游网络清楚、管理后台不裸奔、版本固定、备份可以恢复。
