docker compose up -d 只负责在后台启动 Compose 中定义的容器,不会自动创建域名、申请证书或写入反向代理规则。Nginx Proxy Manager(NPM)把域名转发、SSL 证书申请与导入等操作放在 Web 管理界面中完成,所以命令执行成功、容器处于运行状态,与“域名已经可以访问”是两回事。
处理顺序应是:先确认 Compose 和容器正常,再打开 81 管理页创建 Proxy Host,随后检查 DNS、代理目标和证书签发条件。如果执行 docker compose up -d 后容器反复退出,先处理端口、数据库或网络问题;如果容器正常但域名没有反应,则应从管理页配置、DNS 和上游连通性继续查,而不是反复重启容器。
先确认是 Compose v2
先检查 docker compose 是否为新版插件,并让 Compose 解析当前配置。新版命令中间有空格,旧版命令通常写作 docker-compose,两者的行为和报错不能混在一起看。
docker compose version
docker compose config
docker compose config 主要用于检查 YAML、变量替换、端口和服务关系。它也可能显示数据库口令,因此输出不要直接贴到公开的工单或聊天记录里。
如果提示找不到 docker compose,应先按 Docker 当前文档安装 Compose v2 插件,而不是把旧版 docker-compose 的排错结果套到新命令上。
先把网络和持久化写对
NPM 的配置方式不同于“普通 Nginx 镜像加一份 nginx.conf”。普通 Nginx 的 Compose 方案通常会挂载 nginx.conf 和证书目录;NPM 则通过自己的管理界面生成代理配置。下面这份结构按官方仓库安装模板整理,使用 MariaDB 保存 NPM 数据。数据库类型、镜像标签和首次初始化行为在正式发布前仍应对照当前官方文档及配置源码。
先准备目录:
mkdir -p ~/nginx-proxy-manager/data/database ~/nginx-proxy-manager/letsencrypt
cd ~/nginx-proxy-manager
生成两个不同的随机口令:
openssl rand -hex 24
openssl rand -hex 24
将结果写入 .env:
MYSQL_ROOT_PASSWORD=replace_with_first_random_value
MYSQL_DATABASE=nginxproxymanager
MYSQL_USER=npm
MYSQL_PASSWORD=replace_with_second_random_value
不要保留示例中的 replace_with_...,并限制文件权限:
chmod 600 .env
docker-compose.yml 可以写成:
services:
app:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
environment:
DISABLE_IPV6: "1"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- nginx-proxy
db:
image: mariadb:10.6
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD is required}
MYSQL_DATABASE: ${MYSQL_DATABASE:-nginxproxymanager}
MYSQL_USER: ${MYSQL_USER:-npm}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD is required}
volumes:
- ./data/database:/var/lib/mysql
networks:
- nginx-proxy
networks:
nginx-proxy:
name: nginx-proxy
external: true
这里的 80、443、81 分别用于 HTTP、HTTPS 和管理页面。./data 保存应用数据,./data/database 保存数据库文件,./letsencrypt 保存证书相关文件,都是后续重建容器时不能随意删除的内容。
这份文件把 nginx-proxy 声明成了 external 网络,因此首次启动前需要创建它:
docker network create nginx-proxy
如果网络已经存在,先检查即可,不必重复创建:
docker network inspect nginx-proxy
如果希望由 Compose 自己管理网络,则删除 external: true,保留下面这种写法,并不要再手工执行 docker network create:
networks:
nginx-proxy:
name: nginx-proxy
容器反复退出先看日志
docker compose up -d 返回成功,只说明 Compose 已创建并启动容器,不能据此判断 NPM 已经可用。启动后依次检查状态和日志:
docker compose up -d
docker compose ps -a
docker compose logs --tail=100 app db
容器停在 Restarting 或 Exited 时,先沿着日志里的根因排查,而不是先删容器。external 网络不存在时,应执行前面的网络创建命令;数据库认证失败时,要核对 .env 中的凭据与数据库中已经初始化的账号凭据是否一致。
尤其是数据库第一次初始化以后再修改 MYSQL_ROOT_PASSWORD 或 MYSQL_PASSWORD,新环境变量不会自动修改已有数据库账号。此时应恢复正确口令,或在数据库中修改对应的账号密码或权限。不要为了让容器启动就直接删除 ./data/database,那会丢掉已有数据。
如果日志显示数据目录权限错误,应核对镜像要求的目录属主和权限,再做针对性修改。把目录权限直接放开虽然偶尔能绕过问题,却会扩大宿主机上的文件暴露面。
如果应用只是比数据库晚一点启动,日志中可能短暂出现数据库连接失败;数据库持续不可用时,单纯重启应用没有意义,应先确认 db 服务已经完成初始化。
80 和 443 被占用
80 承载 HTTP 和常见的证书验证流量,443 承载 HTTPS。已有 Nginx、Caddy、Traefik 或其他 Docker 容器占用端口时,NPM 容器可能因此无法启动。先确认占用者:
sudo ss -ltnp | grep -E ':(80|443|81)([[:space:]]|$)'
docker ps --format 'table {{.Names}}\t{{.Ports}}'
如果旧代理已准备下线,可以在确认实际容器名称后停止它:
docker stop old-proxy-container
不要根据模糊的进程名直接终止未知服务,机器上可能还有其他业务依赖这些端口。
另一种做法是修改宿主机映射,例如:
ports:
- "8080:80"
- "8443:443"
- "81:81"
但改映射不只是改 Compose 文件。路由器或负载均衡仍需把公网 80 转发到宿主机 8080,把公网 443 转发到宿主机 8443,防火墙规则和证书验证请求经过的路径也要同步调整。如果公网 80 没有转发到容器 80,基于 HTTP 验证的证书申请通常无法完成。
81 只是管理入口,可以改用不冲突的宿主机端口,也可以只允许指定来源访问管理页。
81 管理页不通
先在本机检查管理端口:
curl -I http://127.0.0.1:81
docker compose port app 81
本机访问失败时,问题通常还在容器或端口发布这一层,应重新检查 docker compose ps -a、应用日志和 ports 映射。本机能够返回正常 HTTP 响应,而其他机器访问失败时,再检查云安全组、宿主机防火墙、Docker 防火墙规则、路由器端口转发和上级网络限制。
排查时要把本机访问和外部访问分开:本机请求不经过云安全组,也不经过路由器 NAT,因此“本机能开、外面不能开”不能归因于 DNS。更常见的情况是管理端口确实已经发布,但入站规则只允许了 80 和 443,没有允许 81。
管理页面也不适合直接向整个公网开放。完成首次初始化后应立即修改默认管理密码,并通过防火墙来源白名单、VPN 或 SSH 隧道限制访问。例如通过 SSH 隧道访问:
ssh -L 8081:127.0.0.1:81 user@server-ip
随后在本机打开:
http://127.0.0.1:8081
管理页可用后,在 Proxy Host 中填写域名和转发目标,再为该域名申请或导入证书。NPM 通过这一界面管理反向代理、域名转发和 SSL 证书。
域名没有进入服务器
以 app.example.com 为例,先分别查看 A 和 AAAA 记录:
dig +short A app.example.com
dig +short AAAA app.example.com
curl -I http://app.example.com
A 记录应指向能够接收请求的公网入口。在家庭宽带、NAS 或内网服务器场景下,还要确认路由器已经把公网 80 和 443 转发到这台机器。运营商 CGNAT 环境通常不能依靠普通端口映射获得公网入口。
AAAA 记录不能只看“有没有”。如果记录存在,但服务器没有可用的 IPv6 路由、入站规则或监听,客户端仍可能优先尝试 IPv6,随后超时。应删除无效的 AAAA 记录,或者把整套 IPv6 入站链路补齐,而不是只检查 IPv4。
使用 CDN 时,DNS 返回的是 CDN 地址,不是源站地址。此时需要确认 CDN 的回源流量确实到达 NPM,并确认证书验证请求没有被其他代理、访问控制或重定向规则截断。
多个内网服务可以分别使用 app.example.com 和 api.example.com,让外部用户通过统一域名入口访问,而不必记住每个服务的 IP 和端口。但 DNS 只负责把请求送到入口,Proxy Host 中的域名规则才决定后续转发。
代理目标要看它在哪
代理目标不能一律填写 127.0.0.1。在 NPM 容器里,127.0.0.1 指向 NPM 容器自身,不是宿主机,也不是其他业务容器。
先检查 NPM 所在网络:
docker network inspect nginx-proxy
如果后端与 NPM 位于同一个 Compose 项目,优先使用 Compose 服务名和容器内监听端口,例如 backend:8080。这里的 8080 应当是后端容器实际监听的端口,不是宿主机上随意映射的端口。
如果后端位于另一个 Compose 项目,可以把它接入同一个 external 网络:
docker network connect nginx-proxy backend-container
随后使用能够被 DNS 解析的容器名或服务别名,例如 backend-container:8080。
如果后端是宿主机上的进程,Linux Docker Engine 支持 host-gateway 时,可以给 NPM 服务增加:
extra_hosts:
- "host.docker.internal:host-gateway"
重新应用配置后,代理目标可填写 host.docker.internal:8080。Docker Desktop 通常已经提供 host.docker.internal。使用前仍应从 NPM 所在容器测试该地址是否可达。
宿主机进程如果只监听宿主机的 127.0.0.1,即使通过 host.docker.internal 访问,NPM 容器仍然无法连接。进程需要监听容器可达的宿主机接口。后端位于局域网其他机器时,则填写那台机器的局域网 IP,并确认它监听对应接口且防火墙允许访问。
应用容器也有同样的问题:如果应用只监听自身的 127.0.0.1,其他容器无法访问它。排查时应同时确认应用监听了哪个地址、哪个端口。
502 和 504 要逐段测上游
502 和 504 只是链路中的现象,不是根因。连接被拒绝、端口写错、协议不匹配、后端异常退出或上游迟迟不返回,都可能表现为代理错误。连接拒绝通常更接近 502,等待上游超时通常更接近 504,但最终仍要看 NPM 日志和直连测试。
如果 NPM 镜像包含 curl,可以从 NPM 容器直接测试:
docker compose exec app curl -I http://backend:8080
镜像没有诊断工具时,使用同一网络中的临时容器:
docker run --rm --network nginx-proxy curlimages/curl -sS -I http://backend:8080
能得到任何 HTTP 响应,都说明 DNS、TCP 和 HTTP 已经走通;具体状态码是否符合业务预期是另一层问题。
若临时容器也无法连接,问题在容器网络、后端监听地址或端口本身。若直连正常而通过域名仍然失败,则重新核对 Proxy Host 中的目标主机、端口、协议以及是否填错服务名,并查看 NPM 日志:
docker compose logs --tail=200 app
真实业务结构复杂时,可以先加入一个 whoami 测试服务用于定位故障。把下面的片段合并到 Compose 文件的 services 下:
whoami:
image: traefik/whoami
restart: "no"
networks:
- nginx-proxy
启动并创建临时 Proxy Host:
docker compose up -d whoami
代理目标填写 whoami:80。whoami 能正常返回,说明 NPM、监听端口和网络链路基本正常,再切回真实业务服务即可。
证书签发失败先查验证路径
使用 Let’s Encrypt 的 HTTP 验证时,域名必须已经指向正确入口,公网 80 端口也要能够到达 NPM。常见失败原因包括 DNS 尚未生效、路由器没有转发 80、错误 AAAA 记录、CDN 截断了验证请求,或者域名实际进入了另一台服务器。
DNS 验证不依赖公网 80,但需要所选 DNS 服务商的 API 权限,并且当前 NPM 版本确实支持该服务商对应的实现。配置前应按当前官方文档确认支持范围,不要直接照搬旧文章里的服务商参数。
导入已有证书时,要检查证书与私钥是否匹配、证书域名是否覆盖目标域名,以及证书链是否包含完整的中间证书。可以先在文件层面比较公钥:
openssl x509 -in fullchain.pem -noout -subject -issuer -dates
openssl pkey -in privkey.pem -pubout | openssl sha256
openssl x509 -in fullchain.pem -pubkey -noout | openssl sha256
后两条命令针对叶子证书计算出的结果应一致。私钥不要放进公开仓库,也不要贴进排障日志。
证书申请失败时,重启 NPM 通常不会改变 DNS、端口或 API 权限。应先修验证路径,再重新发起申请。
强制 HTTPS 会改变客户端行为
强制 HTTPS 后,HTTP 请求会被重定向到 443。浏览器通常能够正常跟随,但调用 API 的客户端、定时任务、Webhook 或只允许有限次跳转的客户端可能失败或把重定向当成业务错误。
可以分别检查 HTTP 和 HTTPS:
curl -I http://app.example.com
curl -Iv https://app.example.com
openssl s_client -connect app.example.com:443 -servername app.example.com -verify_return_error
HTTP 响应应指向同一主名的 HTTPS 地址,HTTPS 请求应通过域名和证书校验。curl -k 可用于临时区分“证书校验失败”和“其他链路错误”,但它会跳过证书验证,不能作为最终验收结果。
如果业务中有不跟随重定向的客户端,应在上线前逐个检查,而不是等回调开始失败后再处理。
重建前先保住数据
代理配置、证书和数据库分别依赖对应的数据目录。升级或重建前,先停止相关容器,再备份文件,能避免数据库复制时仍在写入:
docker compose stop app db
tar -czf "nginx-proxy-manager-$(date +%F).tar.gz" data letsencrypt
docker compose start db
docker compose start app
.env 包含数据库凭据,应单独安全备份,不要随意加入公开归档。
更新镜像前先查看官方升级说明,再拉取并重新应用配置:
docker compose pull
docker compose up -d
docker compose ps -a
docker compose logs --tail=100 app db
docker compose down 会删除容器和 Compose 管理的项目网络,但不会删除当前文件中的 bind mount 目录。docker compose down -v 会删除 Compose 管理的 named volume;当前示例使用的是 bind mount,所以宿主机上的 ./data 和 ./letsencrypt 仍然保留。若以后改用 named volume,-v 才会直接删除其中的数据。配置持久化和升级注意事项也应按当前部署结构核对。
按完整请求链验收
最后不要只看管理页是否能打开,而要按“DNS → 80/443 → NPM → 反代目标”的顺序检查真实响应:
dig +short A app.example.com
dig +short AAAA app.example.com
curl -I http://app.example.com
curl -Iv https://app.example.com
curl -fsS https://app.example.com -o /tmp/npm-final.html
head -n 20 /tmp/npm-final.html
docker compose logs --tail=100 app
验收需要同时满足以下条件:HTTP 请求被正确跳转到同一主机的 HTTPS,TLS 证书和域名校验通过,最终响应正文来自目标应用,而不是 NPM 默认页、错误页或其他服务。











暂无评论内容