Nginx Proxy Manager Docker Compose 部署指南

Nginx Proxy Manager(NPM)v2.16.0 Docker Compose 部署指南:编排文件逐项讲解、首登初始化、代理主机、HTTPS 证书、访问控制与四层转发全链路实测,含等价 docker run 与常见故障实录

Nginx Proxy ManagerNginx Proxy Manager2026/10/18

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/datadatabase.sqlite(全部元数据)、nginx/(生成的配置)、custom_ssl/(自定义证书)、access/(访问控制文件)、logs/(运行日志)、keys.json(API 令牌签名密钥)
./letsencrypt → /etc/letsencrypt/etc/letsencryptLet'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 / PGID0 / 0服务进程运行的用户/组 ID;挂载目录属主非 root 时需显式指定
TZUTC时区,影响日志时间戳,建议设为 Asia/Shanghai
DB_SQLITE_FILE/data/database.sqliteSQLite 数据库文件路径(默认数据库)
DB_MYSQL_HOST / PORT / USER / PASSWORD / NAME无三者(HOST/USER/NAME)齐备时切换到 MySQL,PORT 默认 3306
DB_MYSQL_SSLfalse设 true 后启用 MySQL TLS 连接
DB_MYSQL_SSL_REJECT_UNAUTHORIZED / DB_MYSQL_SSL_VERIFY_IDENTITYtrue / trueMySQL 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_ENABLEDtrue控制是否联网获取 IP 段数据(Access List 的 IP 名单功能使用)
DISABLE_INTERNAL_AUTH未设置设为 true 后关闭内置登录鉴权,交由上层网关做认证(配合零信任场景)

数据库选择逻辑(按优先级):编排/配置文件显式指定 > DB_MYSQL_* 三要素齐备 > DB_POSTGRES_* 三要素齐备 > 默认 SQLite(/data/database.sqlite)。小规模部署直接用默认 SQLite 即可;多实例或需要外部备份体系时再切 MySQL/PostgreSQL。

七、配置参数说明

除编排文件本身,还有三处配置入口需要掌握,避免「改了不生效」。

7.1 编排文件常用修改项

修改项位置生效方式
换端口services.app.portsdocker compose up -d 重建服务
加环境变量services.app.environment同上
关闭 IPv6DISABLE_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 明显更省心。

十二、参考链接

  1. NPM 官方安装文档:https://nginxproxymanager.com/setup/
  2. 官方 GitHub 仓库:https://github.com/NginxProxyManager/nginx-proxy-manager
  3. v2.16.0 发布说明:https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/v2.16.0
  4. 发行仓库标签页:https://hub.docker.com/r/jc21/nginx-proxy-manager/tags
  5. 本组件认知与选型:《Nginx Proxy Manager 2.16.0 介绍与选型指南》