Nginx Proxy Manager Docker Compose 部署指南
Nginx Proxy Manager(NPM)v2.16.0 Docker Compose 部署指南:编排文件逐项讲解、首登初始化、代理主机、HTTPS 证书、访问控制与四层转发全链路实测,含等价 docker run 与常见故障实录
Nginx Proxy Manager Docker Compose 部署指南
目标:在 Ubuntu 22.04 服务器上用 Docker Compose 从零部署 Nginx Proxy Manager(NPM)v2.16.0,并完成管理界面初始化、代理主机发布、HTTPS 证书挂载、访问控制与四层转发的全链路验证,全部命令与回显均已在 Ubuntu 22.04 实测通过。
一、环境信息
本章列出本次部署实测所用的软硬件与版本基线,读者按相同版本操作可获得与本文一致的结果。
- 操作系统:Ubuntu 22.04(64 位)
- Docker:29.8.1(
docker --version实测回显:Docker version 29.8.1, build 4a63305) - Docker Compose:v5.5.1(
docker compose version实测回显:Docker Compose version v5.5.1) - NPM 版本:jc21/nginx-proxy-manager:2.16.0(官方最新稳定版,2026-09-24 发布,版本核实过程见《Nginx Proxy Manager 2.16.0 介绍与选型指南》第一章)
- 演示后端:nginx:alpine(静态页面服务,仅用于验证转发链路)
- 服务器出口网卡:ens33(用于部署后网络自查)
- 端口规划:宿主机 8093→服务 80(业务 HTTP)、8094→服务 81(管理界面)、8095→服务 443(业务 HTTPS)、8096→服务 8096(Stream 四层转发示例);本实验环境 80/81/443 已被占用,故映射到 8093~8096,生产环境可直接使用官方默认的 80/81/443
- 数据目录:/opt/npm/data(元数据与生成的 Nginx 配置)、/opt/npm/letsencrypt(证书)、/opt/npm/demo(演示后端页面)
- 编排网络:自定义 bridge 网络,显式子网 10.89.24.0/24
⚠️ 注意:NPM v2.16.0 起官方发行形态即以编排部署为主,单命令运行形式(等价 docker run)在第九章单独给出并做了等价性实测,两种方式二选一即可,不要同时运行两套实例指向同一数据目录。
二、镜像选型说明
选型即选「用哪个标签」。NPM 官方在发行仓库维护的标签分三类(2026-10-18 实查 Docker Hub 标签接口的回显已在选型篇第一章给出),对应关系与选择建议如下:
| 标签 | 含义 | 更新时间(实查 2026-10-18) | 建议 | |---|---|---|---|---| | 2.16.0 | 精确版本,永不自动变更 | 2026-09-24 | 生产环境首选,升级动作可控 | | 2 | 2.x 系列最新,小版本自动跟进 | 2026-09-24 | 追求自动获得安全补丁时可用 | | latest | 最新稳定版,大版本也自动跟进 | 2026-09-24 | 实验环境可用;生产不建议(大版本跳变风险) |
关键差异与选型结论:三个标签当前都指向同一版本 v2.16.0。生产环境建议固定 2.16.0 这类精确标签,把「升级」变成显式的修改编排文件动作,配合第十章的升级命令执行;latest 的自动漂移会让回滚失去基准。本文全部命令使用 jc21/nginx-proxy-manager:2.16.0。
💡 说明:NPM 官方不单独发布发行包,2.16.0 的内置组件组成为 OpenResty 1.31.1.1、Certbot 5.8.0、s6-overlay 3.2.3.0(官方 v2.16.0 发布说明,查询日期 2026-10-18)。
三、前置条件
开始部署前完成三项检查:运行环境、端口占用、后端演示页。
3.1 确认 Docker 与 Compose 可用
docker --version
docker compose version
实测回显:
Docker version 29.8.1, build 4a63305
Docker Compose version v5.5.1
3.2 检查目标端口空闲
ss -ltn | grep -E ':(8093|8094|8095|8096)\b' || echo "端口空闲"
实测回显(部署前执行,端口未被占用):
端口空闲
若回显中已出现 LISTEN 行,说明端口被占用,需更换编排文件中的宿主端口后再继续(处理方法见第十一章 Q5)。
3.3 准备演示后端页面
创建目录结构并放置一个静态页面,作为后续验证转发的后端站点:
mkdir -p /opt/npm/data /opt/npm/letsencrypt /opt/npm/demo
写入演示页面 /opt/npm/demo/index.html,内容如下:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>demo backend</title></head>
<body>
<h1>Hello from demo backend (nginx:alpine)</h1>
<p>This is the upstream behind NPM proxy host demo.local</p>
</body>
</html>
确认落盘内容:
cat /opt/npm/demo/index.html
实测回显与上述写入内容逐行一致,此处不再重复。
四、部署步骤
本章按「编排文件 → 启动 → 状态确认 → 管理界面初始化 → 代理主机 → HTTPS 证书 → 访问控制 → 404 主机 → 四层转发」的顺序完成从零到可用入口的全流程,每一步都有验证手段。
4.1 编写编排文件
创建 /opt/npm/docker-compose.yml,内容如下:
services:
app:
image: jc21/nginx-proxy-manager:2.16.0
container_name: npm-2.16.0-app
restart: unless-stopped
ports:
- "8093:80"
- "8094:81"
- "8095:443"
environment:
DISABLE_IPV6: "true"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- npmnet
demo:
image: nginx:alpine
container_name: npm-2.16.0-demo
restart: unless-stopped
volumes:
- ./demo:/usr/share/nginx/html:ro
networks:
- npmnet
networks:
npmnet:
driver: bridge
ipam:
config:
- subnet: 10.89.24.0/24
逐项说明:
app服务:NPM 主服务;restart: unless-stopped保证服务器重启后自动拉起;ports:三个宿主端口分别对应业务 HTTP、管理界面、业务 HTTPS;生产环境可直接写"80:80"、"81:81"、"443:443";environment: DISABLE_IPV6: "true":内网无 IPv6 时关闭 IPv6 监听,避免生成listen [::]:80配置产生告警;volumes:./data挂到服务内/data(元数据库 + 生成的 Nginx 配置 + 访问控制文件),./letsencrypt挂到/etc/letsencrypt(Let's Encrypt 证书),这两个目录就是全部需要备份的内容;demo服务:静态页后端,不发布宿主端口,只留在编排网络内供 NPM 内部转发;networks.npmnet:显式声明子网10.89.24.0/24。显式子网可避免依赖自动分配落入与物理网段冲突的地址段。
⚠️ 注意:子网不要使用 192.168.0.0/16 与 100.64.0.0/16 段——前者常与物理局域网冲突,一旦网桥路由抢占默认路由会导致服务器失联。
4.2 校验编排文件并启动
先校验编排文件语法与最终配置:
cd /opt/npm && docker compose config
实测回显(输出节选,实测首部):
name: compose
services:
app:
container_name: npm-2.16.0-app
environment:
DISABLE_IPV6: "true"
image: jc21/nginx-proxy-manager:2.16.0
networks:
npmnet: null
ports:
- mode: ingress
target: 80
published: "8093"
protocol: tcp
启动整套服务(首次会自动拉取两个发行包):
cd /opt/npm && docker compose up -d
实测回显:
Network compose_npmnet Created
Network compose_npmnet Created
Container npm-2.16.0-demo Creating
Container npm-2.16.0-app Creating
Container npm-2.16.0-demo Created
Container npm-2.16.0-app Created
Container npm-2.16.0-demo Starting
Container npm-2.16.0-app Starting
Container npm-2.16.0-demo Started
Container npm-2.16.0-app Started
4.3 确认运行状态与网络
cd /opt/npm && docker compose ps
实测回显:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
npm-2.16.0-app jc21/nginx-proxy-manager:2.16.0 "/init" app 5 seconds ago Up 3 seconds 0.0.0.0:8093->80/tcp, [::]:8093->80/tcp, 0.0.0.0:8094->81/tcp, [::]:8094->81/tcp, 0.0.0.0:8095->443/tcp, [::]:8095->443/tcp
npm-2.16.0-demo nginx:alpine "/docker-entrypoint.…" demo 5 seconds ago Up 3 seconds 80/tcp
验证自定义网络的子网确实按声明生效,且未影响服务器默认路由:
docker network inspect compose_npmnet --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
ip route get 192.168.1.62
实测回显:
10.89.24.0/24
192.168.1.62 via 192.168.146.2 dev ens33 src 192.168.146.128 uid 0
cache
两个关键确认点:子网与编排声明一致(10.89.24.0/24);访问外网服务器的路由仍走物理网卡 ens33,未被新建网桥抢占。
4.4 确认管理服务就绪
NPM 启动约需 20~30 秒(内部要完成数据库迁移与 Nginx 生成)。轮询管理接口:
curl -s http://127.0.0.1:8094/api/
实测回显:
{"status":"OK","setup":false,"version":{"major":2,"minor":16,"revision":0}}
setup:false 表示尚未创建管理员;version 即运行的 2.16.0。此接口无需认证,也可作为日常健康检查命令。
4.5 首次登录:创建管理员账号
⚠️ 注意:旧版本教程普遍写着「默认账号 admin@example.com / changeme」。v2.16.0 已不再内置默认账号,首次访问会直接要求创建管理员。以下是实测记录,读者遇到「默认账号登录失败」属正常现象,不是故障。
先用旧默认凭据换取访问令牌,实测确实被拒绝:
curl -s -X POST http://127.0.0.1:8094/api/tokens -H 'Content-Type: application/json' \
--data '{"identity":"admin@example.com","secret":"changeme"}'
实测回显:
{"error":{"code":400,"message":"Invalid email or password"}}
改走初始设置流程:在浏览器访问 http://<服务器IP>:8094,页面会显示创建管理员的表单(姓名、昵称、邮箱、密码)。等价的 API 调用如下(本示例创建 admin@example.com,密码 q1w2e3r4):
curl -s -X POST http://127.0.0.1:8094/api/users -H 'Content-Type: application/json' \
--data '{"name":"Administrator","nickname":"Admin","email":"admin@example.com","auth":{"type":"password","secret":"q1w2e3r4"}}'
实测回显:
{
"id": 1,
"created_on": "2026-10-18 07:16:44",
"modified_on": "2026-10-18 07:16:44",
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
"roles": [
"admin"
],
"permissions": {
"visibility": "all",
"proxy_hosts": "manage",
"redirection_hosts": "manage",
"dead_hosts": "manage",
"streams": "manage",
"access_lists": "manage",
"certificates": "manage"
}
}
创建成功后再次访问网页即可用 admin@example.com / q1w2e3r4 登录管理界面。用同一凭据换取 API 令牌(后续命令行操作都靠它认证):
curl -s -X POST http://127.0.0.1:8094/api/tokens -H 'Content-Type: application/json' \
--data '{"identity":"admin@example.com","secret":"q1w2e3r4"}'
实测回显(输出节选,实测首尾,令牌中间省略):
{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhcGkiLCJhdHRycyI6eyJpZCI6MX0sInNjb3BlIjpbInVzZXIiXSwiZXhwaXJlc0luIjoiMWQiLCJqdGkiOiJlM2JTa1o5c0VjRlc2TTBJIiwiaWF0IjoxNzkyMzA3ODEyLCJleHAiOjE3OTIzOTQyMTJ9.jF4Mlrkw3MozF6AWIzccnNVMssVM20BXRV2ugnxtlf(中间省略)7Z6awB4C89NFdwPKyPgUiHTeedl4EnSlqwQlZPQFv4hYeFECgYAXxepbsgCrpNtGuPJfPR68","expires":"2026-10-19T07:16:52.450Z"}
💡 说明:令牌有效期 1 天(
expiresIn: "1d")。将返回的 token 值保存到临时文件,供本章后续命令使用:echo "<token值>" > /opt/npm/token.txt。
4.6 创建代理主机并验证转发
用 API 创建一条代理主机:域名 demo.local → 编排网络内的演示后端 demo:80(在管理界面中对应「Hosts → Proxy Hosts → Add Proxy Host」,Domain Names 填 demo.local,Forward Hostname 填 demo,Forward Port 填 80):
TOKEN=$(cat /opt/npm/token.txt) && curl -s -X POST http://127.0.0.1:8094/api/nginx/proxy-hosts \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
--data '{"domain_names":["demo.local"],"forward_scheme":"http","forward_host":"demo","forward_port":80,"block_exploits":true,"caching_enabled":false,"ssl_forced":false,"hsts_enabled":false,"hsts_subdomains":false,"http2_support":false,"certificate_id":0,"meta":{"letsencrypt_agree":false,"dns_challenge":false},"locations":[],"access_list_id":0,"advanced_config":"","enabled":true}'
实测回显:
{
"id": 1,
"created_on": "2026-10-18 07:17:16",
"modified_on": "2026-10-18 07:17:16",
"owner_user_id": 1,
"domain_names": [
"demo.local"
],
"forward_host": "demo",
"forward_port": 80,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"block_exploits": true,
"advanced_config": "",
"meta": {
"letsencrypt_agree": false,
"dns_challenge": false
},
"allow_websocket_upgrade": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"certificate": null,
"owner": {
"id": 1,
"created_on": "2026-10-18 07:16:44",
"modified_on": "2026-10-18 07:16:44",
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
"roles": [
"admin"
]
},
"access_list": null
}
查询列表确认已落库:
TOKEN=$(cat /opt/npm/token.txt) && curl -s "http://127.0.0.1:8094/api/nginx/proxy-hosts" \
-H "Authorization: Bearer $TOKEN"
实测回显(输出节选,实测首部):
[
{
"id": 1,
"created_on": "2026-10-18 07:17:16",
"modified_on": "2026-10-18 07:17:16",
"owner_user_id": 1,
"domain_names": [
"demo.local"
],
"forward_host": "demo",
"forward_port": 80,
"access_list_id": 0,
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"block_exploits": true,
"advanced_config": "",
"meta": {
"letsencrypt_agree": false,
"dns_challenge": false,
"nginx_online": true,
"nginx_err": null
},
"allow_websocket_upgrade": false,
"http2_support": false,
"forward_scheme": "http",
"enabled": true,
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false
}
]
验证转发链路:带 Host: demo.local 的请求应返回演示后端页面;不带该头的请求命中默认站点:
curl -H "Host: demo.local" http://127.0.0.1:8093/
实测回显:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>demo backend</title></head>
<body>
<h1>Hello from demo backend (nginx:alpine)</h1>
<p>This is the upstream behind NPM proxy host demo.local</p>
</body>
</html>
curl -s -o /dev/null -w 'HTTP %{http_code}\n' -H "Host: demo.local" http://127.0.0.1:8093/
curl -s http://127.0.0.1:8093/ | head -3
实测回显:
HTTP 200
<!DOCTYPE html>
<html lang="en">
<head>
第一个请求返回了演示后端的完整页面(HTTP 200),转发链路打通;不带 Host 头时返回 NPM 默认欢迎页。
4.7 挂载 HTTPS 证书(自签证书实测)
生产环境用「SSL Certificates → Request a new Certificate」一键申请 Let's Encrypt 证书;本节用自签证书演示「Custom Certificate」完整链路,两种来源在绑定与验证环节完全一致。
第一步,生成自签证书与私钥(365 天,CN 与 SAN 均为 demo.local):
mkdir -p /opt/npm/certs && cd /opt/npm/certs
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout demo.local.key -out demo.local.crt \
-subj "/CN=demo.local" -addext "subjectAltName=DNS:demo.local"
实测回显(RSA 密钥生成的进度点为随机长度,属正常现象):
.+..+...................+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*......+...+....+...........+......+.+..+.+......+..............+.+...........+..........+...+..+....+.....+...+......+.+...+...+..+.+......+...+..+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.....+.........+......+.+........+...+.............+...........+.+...+..+....+...+..............+.......+.....+...................+..+.......+.....+...................+..........+.........+................+..+...+...................+...+...+..+...+........+......+.+.....+....+............+.+..+...+.+..+.......+...........+....+........+......+.....+.+..+.......+..............+......+....+............+.....+.........+...+.+......+...+..+.......+..............+............+.....+.........+................+..+.......+...+...+..+.......+..............+.......+.....+.........+...+.+......+...+..+.......+..............+......+............+.....+.........+...+...+......+.....+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.......+......+..+......+.+.....+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*......+.+..+..........+...+..+....+......+..+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*...+..+.+..+.+..........+..........+.....+.+..+...+...+...................+.....+.+..+....+...+......+.....+....+...+.......+...........+..........+...+..+.+..+.............+...............+..+...+..........+.....+.+..+....+...+......+.....+....+...+.......+...........+..........+...+..+.+..+.............+...............+..+...+..........+.......+...+..+.+...............+...............+......+......+.......+...+.+...............+..+.......+..............+......+......+.......+..+.+...............+......+..........................+...+.+.....+...+......+.+.........+............+............+.+......+............+.+.....+...+......+.+.........+...........+......+......+.+.........+......+......+.......+.....+...+.......+..........+.....+...+.......+..........+.....+...+.......+......+.....+.+.........+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
校验证书内容:
openssl x509 -in demo.local.crt -noout -subject -issuer -dates -ext subjectAltName
实测回显:
subject=CN = demo.local
issuer=CN = demo.local
notBefore=Oct 18 07:18:51 2026 GMT
notAfter=Oct 18 07:18:51 2027 GMT
X509v3 Subject Alternative Name:
DNS:demo.local
第二步,创建自定义证书记录(provider 必须为 other,表示自定义证书):
TOKEN=$(cat /opt/npm/token.txt) && curl -s -X POST http://127.0.0.1:8094/api/nginx/certificates \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
--data '{"provider":"other","nice_name":"demo.local self-signed","domain_names":["demo.local"]}'
实测回显:
{
"provider": "other",
"nice_name": "demo.local self-signed",
"domain_names": [
"demo.local"
],
"owner_user_id": 1,
"created_on": "2026-10-18 07:18:59",
"modified_on": "2026-10-18 07:18:59",
"expires_on": "2026-10-18 07:18:59",
"meta": {},
"id": 1
}
第三步,上传证书与私钥(multipart 表单,字段名固定为 certificate 与 certificate_key):
TOKEN=$(cat /opt/npm/token.txt) && cd /opt/npm/certs && curl -s -X POST http://127.0.0.1:8094/api/nginx/certificates/1/upload \
-H "Authorization: Bearer $TOKEN" \
-F "certificate=@demo.local.crt" -F "certificate_key=@demo.local.key"
实测回显(输出节选,实测首部;响应原样返回了服务端收到的证书与私钥全文):
{
"certificate": "-----BEGIN CERTIFICATE-----\nMIIDIjCCAgqgAwIBAgIUFl/rmHlEIC5B3dHknr7OwOlkOMwwDQYJKoZIhvcNAQEL\nBQAwFTETMBEGA1UEAwwKZGVtby5sb2NhbDAeFw0yNjEwMTgwNzE4NTFaFw0yNzEw\n(中间省略)\nwfWTXLDyF1/zWbON4oUQzn93BA+Ka/wGEKnjcNW8OBIMyeYOX0g=\n-----END CERTIFICATE-----\n",
"certificate_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCnfHuhN+Q5jl6j\n(中间省略)\n89NFdwPKyPgUiHTeedl4EnSlqwQlZPQFv4hYeFECgYAXxepbsgCrpNtGuPJfPR68\nqf5j9Yv+Vm+ifQvuSLRqc3gII4VjIrwiJSHPo4yxO0Y00BXKy1OGyvqSWs8ECdn\nXOzKhMMRWLg5e2Jdy7JBSaxH/nCXrOyW6+cjW6JlUBNE6KDZZ3PeBrq/a0BbGTH4\nxUAfrCQ/kIDnv0+QK62Y5A==\n-----END PRIVATE KEY-----\n"
}
第四步,把证书绑定到 demo.local 代理主机并开启强制 HTTPS:
TOKEN=$(cat /opt/npm/token.txt) && curl -s -X PUT http://127.0.0.1:8094/api/nginx/proxy-hosts/1 \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
--data '{"certificate_id":1,"ssl_forced":true,"hsts_enabled":false,"hsts_subdomains":false,"http2_support":false}'
实测回显(输出节选,实测关键字段):
id: 1 certificate_id: 1 ssl_forced: True certificate: demo.local self-signed
第五步,验证 HTTPS 与强制跳转。注意:测试本机 443 映射端口时必须用 --resolve 把域名解析到 127.0.0.1(TLS 握手按 SNI 匹配证书),直接用 IP + Host 头会握手失败(该现象见第十一章 Q3):
curl -sk --resolve demo.local:8095:127.0.0.1 https://demo.local:8095/
实测回显:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>demo backend</title></head>
<body>
<h1>Hello from demo backend (nginx:alpine)</h1>
<p>This is the upstream behind NPM proxy host demo.local</p>
</body>
</html>
curl -s -o /dev/null -w 'HTTP_CODE=%{http_code} REDIRECT=%{redirect_url}\n' -H "Host: demo.local" http://127.0.0.1:8093/
echo | openssl s_client -servername demo.local -connect 127.0.0.1:8095 2>/dev/null | grep -E 'subject=|issuer=|Verification'
实测回显:
HTTP_CODE=301 REDIRECT=https://demo.local/
subject=CN = demo.local
issuer=CN = demo.local
Verification error: self-signed certificate
三个验证全部符合预期:HTTPS 返回后端页面;HTTP 请求被 301 到 https://demo.local/(强制 HTTPS 生效);握手返回的正是我们上传的自签证书(Verification error: self-signed certificate 是自签证书的预期告警,生产用 Let's Encrypt 证书时该行为验证通过)。
4.8 访问控制(Basic 认证)实测
创建一条访问控制列表,内置一个基础认证账号 admin / q1w2e3r4:
TOKEN=$(cat /opt/npm/token.txt) && curl -s -X POST http://127.0.0.1:8094/api/nginx/access-lists \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
--data '{"name":"demo-acl","satisfy_any":false,"pass_auth":false,"items":[{"username":"admin","password":"q1w2e3r4"}],"clients":[]}'
实测回显(输出节选,实测首部):
{
"id": 1,
"created_on": "2026-10-18 07:20:48",
"modified_on": "2026-10-18 07:20:48",
"owner_user_id": 1,
"name": "demo-acl",
"meta": {},
"satisfy_any": false,
"pass_auth": false,
"proxy_host_count": 0,
"owner": {
"id": 1,
"created_on": "2026-10-18 07:16:44",
"modified_on": "2026-10-18 07:16:44",
"is_deleted": false,
"is_disabled": false,
"email": "admin@example.com",
"name": "Administrator",
"nickname": "Admin",
"avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm",
"roles": [
"admin"
]
},
"items": [
{
"id": 1,
"created_on": "2026-10-18 07:20:48",
"modified_on": "2026-10-18 07:20:48",
"access_list_id": 1,
创建第二条代理主机 acl.local 并挂上该访问控制列表(access_list_id: 1):
TOKEN=$(cat /opt/npm/token.txt) && curl -s -X POST http://127.0.0.1:8094/api/nginx/proxy-hosts \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
--data '{"domain_names":["acl.local"],"forward_scheme":"http","forward_host":"demo","forward_port":80,"block_exploits":false,"caching_enabled":false,"ssl_forced":false,"hsts_enabled":false,"hsts_subdomains":false,"http2_support":false,"certificate_id":0,"meta":{"letsencrypt_agree":false,"dns_challenge":false},"locations":[],"access_list_id":1,"advanced_config":"","enabled":true}'
实测回显(输出节选,实测关键字段):
id: 2 domains: ['acl.local'] access_list: demo-acl
验证:无凭据 401、带正确凭据 200、响应头带 Basic 质询:
sleep 2 && curl -s -o /dev/null -w 'NOAUTH=%{http_code}\n' -H "Host: acl.local" http://127.0.0.1:8093/
curl -s -o /dev/null -w 'AUTH=%{http_code}\n' -u admin:q1w2e3r4 -H "Host: acl.local" http://127.0.0.1:8093/
curl -s -I -H "Host: acl.local" http://127.0.0.1:8093/ | grep -i www-authenticate
实测回显:
NOAUTH=401
AUTH=200
WWW-Authenticate: Basic realm="Authorization required"
4.9 404 Host(默认 404 站点)实测
404 Host 用于把未启用的域名指向一个固定的 404 响应,防止扫描流量命中默认站点:
TOKEN=$(cat /opt/npm/token.txt) && curl -s -X POST http://127.0.0.1:8094/api/nginx/dead-hosts \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
--data '{"domain_names":["notfound.local"],"certificate_id":0,"ssl_forced":false,"hsts_enabled":false,"hsts_subdomains":false,"http2_support":false,"advanced_config":"","meta":{"letsencrypt_agree":false,"dns_challenge":false}}'
实测回显(输出节选,实测关键字段):
id: 1 domains: ['notfound.local'] enabled: True
验证该域名返回 404:
curl -s -o /tmp/dh.html -w 'CODE=%{http_code} ' -H "Host: notfound.local" http://127.0.0.1:8093/ && grep -o '<title>[^<]*</title>' /tmp/dh.html
实测回显:
CODE=404 <title>404 Not Found</title>
4.10 Stream 四层转发(TCP)实测
四层转发用于发布数据库等非 HTTP 服务。先在编排文件的 app.ports 中追加一行 Stream 监听端口(服务内 8096 → 宿主机 8096):
ports:
- "8093:80"
- "8094:81"
- "8095:443"
- "8096:8096"
应用端口变更(会重建 app 服务,数据经挂载目录保留,up -d 实测回显):
cd /opt/npm && docker compose up -d
实测回显:
Container npm-2.16.0-app Recreate
Container npm-2.16.0-app Recreated
Container npm-2.16.0-app Starting
Container npm-2.16.0-app Started
⚠️ 注意:修改
ports/environment后必须docker compose up -d重建服务才能生效;由于元数据与证书都在挂载目录里,重建不会丢失任何主机、证书与账号数据。
创建 TCP 流转发规则(服务内 8096 → 演示后端 demo:80;管理界面中对应「Hosts → Streams」):
TOKEN=$(cat /opt/npm/token.txt) && curl -s -X POST http://127.0.0.1:8094/api/nginx/streams \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
--data '{"incoming_port":8096,"forwarding_host":"demo","forwarding_port":80,"tcp_forwarding":true,"udp_forwarding":false,"certificate_id":0,"meta":{},"domain_names":["tcp-demo.local"]}'
实测回显(输出节选,实测关键字段):
{
"id": 1,
"created_on": "2026-10-18 07:21:52",
"modified_on": "2026-10-18 07:21:52",
"owner_user_id": 1,
"incoming_port": 8096,
"forwarding_host": "demo",
"forwarding_port": 80,
"tcp_forwarding": true,
"udp_forwarding": false,
"meta": {},
"enabled": true,
💡 说明:
domain_names虽然对四层转发没有实际作用,但接口校验要求至少 1 个元素,传一个占位域名即可(为空会报data/domain_names must NOT have fewer than 1 items,实测如此)。
验证 TCP 透传(把 Host 头原样透传给后端,等价于直连后端 80 端口):
sleep 2 && curl -s -H "Host: demo.local" http://127.0.0.1:8096/ | head -3
实测回显:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>demo backend</title></head>
五、数据持久化说明
NPM 的全部状态都在两个挂载目录里,理解它们的内容与落盘位置,是备份迁移和故障恢复的基础。
| 挂载 | 服务内路径 | 内容 |
|---|---|---|
| ./data → /data | /data | database.sqlite(全部元数据)、nginx/(生成的配置)、custom_ssl/(自定义证书)、access/(访问控制文件)、logs/(运行日志)、keys.json(API 令牌签名密钥) |
| ./letsencrypt → /etc/letsencrypt | /etc/letsencrypt | Let's Encrypt 账号与证书(本节自签证书存放于 /data/custom_ssl/,不在此目录) |
验证「重建服务不丢数据」:停掉整套服务(连同网络一起移除),再重新启动:
cd /opt/npm && docker compose down && docker compose up -d
实测回显:
Container npm-2.16.0-app Stopped
Container npm-2.16.0-app Removing
Container npm-2.16.0-app Removed
Network compose_npmnet Removing
Network compose_npmnet Removed
Container npm-2.16.0-app Created
Container npm-2.16.0-app Starting
Container npm-2.16.0-demo Starting
Container npm-2.16.0-app Started
Container npm-2.16.0-demo Started
重建后复核三件事:管理员仍在(setup:true)、代理主机仍生效(HTTP 301 → HTTPS 200)、证书与生成配置仍在磁盘:
sleep 28 && curl -s http://127.0.0.1:8094/api/
curl -s -o /dev/null -w 'HTTP_AFTER_RECREATE=%{http_code}\n' -H "Host: demo.local" http://127.0.0.1:8093/
curl -sk -o /dev/null -w 'HTTPS_AFTER_RECREATE=%{http_code}\n' --resolve demo.local:8095:127.0.0.1 https://demo.local:8095/
docker exec npm-2.16.0-app sh -c "ls /data/custom_ssl/npm-1/ && ls /data/nginx/proxy_host/"
实测回显:
{"status":"OK","setup":true,"version":{"major":2,"minor":16,"revision":0}}
HTTP_AFTER_RECREATE=301
HTTPS_AFTER_RECREATE=200
fullchain.pem
privkey.pem
1.conf
2.conf
结论:down 会移除服务与网络,但挂载目录原样保留在宿主机;up -d 后全部配置原样恢复。日常备份只需备份 /opt/npm/data 与 /opt/npm/letsencrypt 两个目录(备份与恢复的完整流程见《Nginx Proxy Manager 日常运维手册》)。
⚠️ 注意:/data/keys.json 是 API 令牌的签名密钥,恢复备份时必须连同该文件一起恢复,否则旧令牌全部失效(影响有限,重新登录即可);两台服务器共用同一份 data 目录会导致数据库损坏,切勿多人同时指向同一目录。
六、环境变量表
以下变量在编排文件 environment: 中注入。表格中的默认值取自 v2.16.0 运行实例内的配置加载逻辑与初始化脚本实读,均在本文实测环境中核实。
| 变量 | 默认值 | 说明 |
|---|---|---|
| DISABLE_IPV6 | 未设置(启用 IPv6 监听) | 设为 true 后生成的 Nginx 配置注释掉 listen [::] 行 |
| PUID / PGID | 0 / 0 | 服务进程运行的用户/组 ID;挂载目录属主非 root 时需显式指定 |
| TZ | UTC | 时区,影响日志时间戳,建议设为 Asia/Shanghai |
| DB_SQLITE_FILE | /data/database.sqlite | SQLite 数据库文件路径(默认数据库) |
| DB_MYSQL_HOST / PORT / USER / PASSWORD / NAME | 无 | 三者(HOST/USER/NAME)齐备时切换到 MySQL,PORT 默认 3306 |
| DB_MYSQL_SSL | false | 设 true 后启用 MySQL TLS 连接 |
| DB_MYSQL_SSL_REJECT_UNAUTHORIZED / DB_MYSQL_SSL_VERIFY_IDENTITY | true / true | MySQL TLS 校验策略,仅 DB_MYSQL_SSL=true 时生效 |
| DB_POSTGRES_HOST / PORT / USER / PASSWORD / NAME | 无 | 三者(HOST/USER/NAME)齐备时切换到 PostgreSQL,PORT 默认 5432 |
| LE_STAGING | 未设置 | 设为 true 时 Certbot 走 Let's Encrypt 测试环境(频繁调试签发时避免触发限额) |
| LE_SERVER | 未设置 | 覆盖 ACME 服务端地址(私有 ACME 场景) |
| DEBUG | 未设置 | 设为 true 输出调试日志 |
| IP_RANGES_FETCH_ENABLED | true | 控制是否联网获取 IP 段数据(Access List 的 IP 名单功能使用) |
| DISABLE_INTERNAL_AUTH | 未设置 | 设为 true 后关闭内置登录鉴权,交由上层网关做认证(配合零信任场景) |
数据库选择逻辑(按优先级):编排/配置文件显式指定 > DB_MYSQL_* 三要素齐备 > DB_POSTGRES_* 三要素齐备 > 默认 SQLite(/data/database.sqlite)。小规模部署直接用默认 SQLite 即可;多实例或需要外部备份体系时再切 MySQL/PostgreSQL。
七、配置参数说明
除编排文件本身,还有三处配置入口需要掌握,避免「改了不生效」。
7.1 编排文件常用修改项
| 修改项 | 位置 | 生效方式 |
|---|---|---|
| 换端口 | services.app.ports | docker compose up -d 重建服务 |
| 加环境变量 | services.app.environment | 同上 |
| 关闭 IPv6 | DISABLE_IPV6=true | 同上 |
| 换数据库 | 环境变量 DB_MYSQL_/DB_POSTGRES_ | 同上(切换前先备份 /data) |
| 升级版本 | services.app.image 标签 | 同上 |
统一原则:编排文件改动后执行 docker compose up -d,Compose 只重建发生变化的服务,数据目录不受影响。
7.2 自定义 Nginx 配置注入
每个自动生成的 server 块末尾都有一行 include /data/nginx/custom/server_proxy[.]conf;(按主机类型分别为 server_proxy / server_dead / server_stream 等后缀)。在该目录创建对应文件名的配置片段,即可在不改动生成配置的前提下追加自定义指令,重建服务也不会丢失。该目录默认不存在,需要时手动创建:mkdir -p /opt/npm/data/nginx/custom。
7.3 单条代理主机的高级配置
管理界面中每条代理主机都有 Advanced Config 输入框(API 字段 advanced_config),内容会原样注入该主机的 server 块,适合追加 client_max_body_size、proxy_read_timeout 等指令。保存后 NPM 会做 Nginx 语法校验,校验失败配置不会生效且会保留 .conf.err 文件便于排查。
八、配置文件挂载
汇总本部署中宿主机目录与服务内路径的映射关系,便于迁移时按图索骥:
| 宿主机路径 | 服务内路径 | 读/写 | 用途 |
|---|---|---|---|
| /opt/npm/data | /data | 读写 | 元数据、生成配置、日志、自定义证书、访问控制 |
| /opt/npm/letsencrypt | /etc/letsencrypt | 读写 | Let's Encrypt 证书与账号 |
| /opt/npm/demo | /usr/share/nginx/html | 只读(:ro) | 演示后端静态页 |
| /opt/npm/docker-compose.yml | — | — | 编排文件(不入服务) |
验证挂载关系(生成配置确实落到了宿主机):
ls /opt/npm/data/nginx/proxy_host/
实测回显:
1.conf
2.conf
两条代理主机各对应一个自动生成的 .conf 文件;直接查看其中一个可以看到 NPM 生成的完整 server 块(配置内容详解见配置详解篇)。
九、等价 docker run 形式
不用编排文件时,可用单命令运行与上述部署完全等价的实例。先停掉编排形态释放端口,再以相同端口、相同挂载运行:
cd /opt/npm && docker compose stop app
docker run -d --name npm-2.16.0-standalone --restart unless-stopped \
-p 8093:80 -p 8094:81 -p 8095:443 \
-v /opt/npm/data:/data -v /opt/npm/letsencrypt:/etc/letsencrypt \
jc21/nginx-proxy-manager:2.16.0
实测回显(首行为 run 返回的服务 ID):
a820a140232fe84b56622c2347ad3d41b3d7012d629d559efcc64cdd79eaddcd
确认运行状态与管理服务可用:
docker ps --filter name=npm-2.16.0-standalone --format '{{.Names}} {{.Status}}'
curl -s http://127.0.0.1:8094/api/
实测回显:
npm-2.16.0-standalone Up 40 seconds
{"status":"OK","setup":true,"version":{"major":2,"minor":16,"revision":0}}
setup:true 说明账号、主机、证书全部沿用(同一份数据目录),单命令形态与编排形态对 NPM 本身完全等价;差异仅在服务发现——编排形态里后端可用服务名互访,单命令形态需保证上游地址可达(本次验证时演示后端未接入同一网络,代理请求返回 502 属预期,见第十一章 Q7)。
验证完毕切回编排形态:
docker stop npm-2.16.0-standalone && docker rm npm-2.16.0-standalone
cd /opt/npm && docker compose start app
实测回显:
npm-2.16.0-standalone
npm-2.16.0-standalone
Container npm-2.16.0-app Started
十、常用运维命令
以下命令均在 /opt/npm 目录内执行(cd /opt/npm),全部实测。
docker compose ps # 查看服务状态
docker compose logs app --tail 20 # 查看主服务最近 20 行日志
docker compose restart app # 重启主服务
docker compose stop app # 停止主服务(保留数据与网络)
docker compose start app # 启动主服务
实测回显:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
npm-2.16.0-app jc21/nginx-proxy-manager:2.16.0 "/init" app 5 minutes ago Up 3 minutes 0.0.0.0:8093->80/tcp, [::]:8093->80/tcp, 0.0.0.0:8094->81/tcp, [::]:8094->81/tcp, 0.0.0.0:8095->443/tcp, [::]:8095->443/tcp, 0.0.0.0:8096->8096/tcp, [::]:8096->8096/tcp
npm-2.16.0-demo nginx:alpine "/docker-entrypoint.…" demo 5 minutes ago Up 3 minutes 80/tcp
npm-2.16.0-app | [10/18/2026] [7:40:19 AM] [IP Ranges] › ℹ info Fetching https://www.cloudflare.com/ips-v6
npm-2.16.0-app | [10/18/2026] [7:40:20 AM] [SSL ] › ℹ info Let's Encrypt Renewal Timer initialized
npm-2.16.0-app | [10/18/2026] [7:40:20 AM] [SSL ] › ℹ info Renewing SSL certs expiring within 30 days ...
npm-2.16.0-app | [10/18/2026] [7:40:20 AM] [IP Ranges] › ℹ info IP Ranges Renewal Timer initialized
npm-2.16.0-app | [10/18/2026] [7:40:20 AM] [SSL ] › ℹ info Completed SSL cert renew process
npm-2.16.0-app | [10/18/2026] [7:40:20 AM] [Global ] › ℹ info Backend PID 243 listening on port 3000 ...
日志里的关键信息:Let's Encrypt Renewal Timer initialized 与 Renewing SSL certs expiring within 30 days ... 表明证书自动续期机制随服务启动而激活;Backend PID 243 listening on port 3000 说明管理后端在服务内 3000 端口监听,宿主机访问的 81 端口由数据面转交给它。
升级到新版本三步(以 2.16.0 → 未来版本为例,把编排文件中 image 标签改成目标版本后):
cd /opt/npm && docker compose pull app
docker compose up -d
实测回显(当前已是最新版,pull 确认无更新):
Image jc21/nginx-proxy-manager:2.16.0 Pulling
Image jc21/nginx-proxy-manager:2.16.0 Pulled
⚠️ 注意:
docker compose down会连网络一起移除(数据目录保留),属于影响面最大的停止方式,日常停机用docker compose stop即可;两者的差异与恢复方法见第五章实测。升级前务必先备份数据目录(流程见《Nginx Proxy Manager 日常运维手册》)。
十一、常见问题
以下问题均为本文实测中真实发生并记录的故障,按「现象 → 原因 → 解决」整理。
Q1:用 admin@example.com / changeme 登录提示 Invalid email or password? 原因:v2.16.0 起官方移除了内置默认账号,首次访问必须先创建管理员(实测回显见 4.5 节)。解决:按 4.5 节初始设置流程创建管理员后再登录。
Q2:访问代理主机返回 502 Bad Gateway? 原因:上游后端不可达。实测把演示后端停掉后,HTTPS 请求返回 502,同时错误日志记录了根因(后端主机名解析失败):
tail -3 /data/logs/proxy-host-1_error.log
实测回显:
2026/10/18 07:19:28 [warn] 469#469: *38 using uninitialized "server" variable while logging request, client: 10.89.24.1, server: demo.local
2026/10/18 07:24:27 [error] 273#273: *11 demo could not be resolved (2: Server failure), client: 10.89.24.1, server: demo.local, request: "GET / HTTP/1.1", host: "demo.local:8095"
解决:恢复后端服务即可(实测恢复后 502 立即变回 200)。若日志显示 connect() failed 则是端口不通,检查上游 IP/端口与防火墙。
Q3:用 IP + Host 头测试 HTTPS 报 SSL 错误? 现象:
curl -sk https://127.0.0.1:8095/ -H "Host: demo.local"
echo "EXIT=$?"
实测回显:
EXIT=35
加 -v 可见完整原因:
* TLSv1.3 (IN), TLS alert, unrecognized name (624):
* error:0A000458:SSL routines::tlsv1 unrecognized name
原因:TLS 握手阶段按 SNI(而不是 HTTP 的 Host 头)选择证书与 server 块,用 IP 访问时 SNI 是 127.0.0.1,Nginx 找不到对应 TLS 站点直接拒绝握手。解决:测试本机时用 --resolve 模拟域名解析(如 4.7 节),或用 openssl s_client -servername 指定 SNI。
Q4:Stream 四层转发建好后端口不通?
原因:Stream 监听的是服务内端口,必须在编排文件 ports 中显式映射到宿主机(4.10 节的追加 8096 行)。遗漏映射时宿主机上该端口无人监听。解决:追加映射后 docker compose up -d 重建(重建不丢数据,实测见 4.10 节回显)。
Q5:启动时报 port is already allocated? 实测现象(8093 已被编排形态占用时再用单命令绑定同端口):
docker run -d --name npm-2.16.0-conflict-test -p 8093:80 jc21/nginx-proxy-manager:2.16.0
实测回显:
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint npm-2.16.0-conflict-test (e5df39508cb956517381bd46afefc663b3a34b628ade7ecc08581c316acb463d): Bind for 0.0.0.0:8093 failed: port is already allocated
解决:ss -ltnp | grep 8093 找到占用方,停掉旧实例或改用其他宿主端口;部署前用 ss -ltn 预检(3.2 节)可避免。
Q6:申请 Let's Encrypt 证书失败(Internal Error)? 实测现象(用无效域名 certfail.invalid + 邮箱 admin@example.com 走 HTTP 校验):
{"error":{"code":500,"message":"Internal Error"},"debug":{"stack":["Error: Saving debug log to /data/logs/letsencrypt.log","Unable to register an account with ACME server. The ACME server believes admin@example.com is an invalid email address. Please ensure it is a valid email and attempt registration again.",...(节选)]}}
原因有两个,需同时满足才能签发成功:一是 ACME 账号邮箱必须真实有效(example.com 假邮箱会被 Let's Encrypt 拒绝);二是 HTTP 校验要求域名可公网解析且 80 端口可达,内网假域名走不通(改用 DNS 校验可解决域名解析限制,但仍需真实域名)。详细错误可看日志文件:tail -20 /data/logs/letsencrypt.log(实测该文件位于挂载目录 /opt/npm/data/logs/letsencrypt.log,服务内路径为 /data/logs/letsencrypt.log)。
Q7:单命令形态下代理报 502 / Nginx 反复重启报 host not found in upstream? 实测现象:单命令运行时(第九章)若元数据里已有 Stream/代理主机指向编排网络内的服务名(如 demo),Nginx 启动即失败并循环重试:
❯ Starting nginx ...
nginx: [emerg] host not found in upstream "demo" in /data/nginx/stream/1.conf:10
原因:服务名只在编排网络内可解析,单命令形态未接入该网络。解决:上游地址改用可达的 IP/域名;或在该形态下先停用引用不可达上游的主机。这也是生产选型的参考点之一——同一台机器上多服务互联时,编排形态的内部 DNS 明显更省心。
十二、参考链接
- NPM 官方安装文档:https://nginxproxymanager.com/setup/
- 官方 GitHub 仓库:https://github.com/NginxProxyManager/nginx-proxy-manager
- v2.16.0 发布说明:https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/v2.16.0
- 发行仓库标签页:https://hub.docker.com/r/jc21/nginx-proxy-manager/tags
- 本组件认知与选型:《Nginx Proxy Manager 2.16.0 介绍与选型指南》