步骤/目录： 1.背景介绍 2.docker安装NPM 3.通配符SSL证书的申请 4.NPM的使用 （1）使用docker网络 （2）Access List通用密码 5.NPM + frp 6.NPM + v2ray 7.其它问题 （1）登录时卡在502 （2）端口开放 （3）ipv6+DDNS时80、443封禁 （4）无法申请证书本文首发于个人博客https://lisper517.top/index.php/archives/107/，转载请注明出处。本文的目的是介绍Nginx Proxy Manager，一款可通过Web UI管理Nginx反代的工具。本文写作日期为2023年12月23日。主要受up主 我不是咕咕鸽 的启发。他还分享了很多其它有趣的内容，讲解也很详细、基础，很适合小白学习。1.背景介绍在日常管理服务器的过程中，一些工具能大大减轻工作量。之前在up主 我不是咕咕鸽 的空间看到了很多有趣的分享，也尝试自己搭建一下。其中NPM是搭建个人网页服务比较基础的工具。之前笔者曾经介绍过frp，一款很常用的开源反向代理软件。理论上nginx也可以用来做反向代理，但是nginx的配置笔者经常出问题，太麻烦了。NPM则是一款方便的、容易上手的反代工具，还能很方便地申请免费的SSL证书。不过需要注意，frp是端口对端口、可开放内网机器到公网，nginx则是端口对域名、更多用于在服务器上搭建各种子域名网页（换一个子域名就能切换到该服务器的不同网页，很方便）。frp + NPM，就可以通过子域名访问内网机器，不需要背端口号。NPM另外一个好处则是，如果用NPM，很多其它服务的网页端口也能走80、443，提升了安全性。2.docker安装NPM在安装之前需要提醒，由于接下来会用到80、81、443端口，如果有服务占用的话需要先关掉。已经搭建了自己的网页时，也不用慌，后面访问时加个子域名就行，现在可以安心关掉。访问 NPM官方设置 可以看到官方给出的yml文件示例。在服务器上如下操作：mkdir -p /docker/NPM/data/NPM mkdir -p /docker/NPM/data/mysql mkdir -p /docker/NPM/letsencrypt cd /docker/NPM vim docker-compose.yml写入以下内容：version: '3.8' services: NPM: image: 'jc21/nginx-proxy-manager:latest' ports: - '80:80' - '443:443' #仅在第一次配置时映射81端口 - '81:81' environment: DB_MYSQL_HOST: "mariadb" DB_MYSQL_PORT: 3306 DB_MYSQL_USER: "npm_user" DB_MYSQL_PASSWORD: "123456" DB_MYSQL_NAME: "npm" volumes: - /docker/NPM/data/NPM:/data - /docker/NPM/letsencrypt:/etc/letsencrypt depends_on: - mariadb restart: unless-stopped mariadb: image: 'jc21/mariadb-aria:latest' environment: MYSQL_ROOT_PASSWORD: '999999' MYSQL_DATABASE: 'npm' MYSQL_USER: 'npm_user' MYSQL_PASSWORD: '123456' volumes: - /docker/NPM/data/mysql:/var/lib/mysql restart: unless-stopped数据库的密码可以自己修改一下，这个数据库不会开放到公网，理论上来说没有风险，但是docker毕竟比较透风。运行一下：docker-compose config docker-compose up容器开始运行，先不要退出，使用过程中观察有无异常输出。最后，记得在ufw和厂商的服务器防火墙控制台把80、443、81端口打开。docker-compose up 后稍微等待十几秒没有输出后，访问 服务器ip:81 端口，默认的邮箱、密码是 admin@example.com 和 changeme ，马上修改一下。3.通配符SSL证书的申请在正式使用NPM之前，还需要一些小小的工作。如果没有为这台服务器申请域名，可参考 Docker实践——如何查阅wiki百科及V2Ray 。总之，买好域名、配置好DNS解析（必须把域名解析到本服务器上）后，到你的DNS解析厂商的控制台，以阿里云为例，在头像处的下拉菜单中找到AccessKey（各家厂商可能有一些细小的区别，其它厂商可能叫token令牌、API之类的。总之它会给你一串只出现一次，但可以重复创建的字符作为令牌，用令牌可以对接认证到该厂商进行一些服务。权限最高的称为全局令牌，只拥有某些权限的是局部令牌或者子令牌，一般来说尽量用局部令牌，并且令牌要严防泄露，如果不小心泄露了要及时重新创建），创建一个，保存好（忘记保存就再创建一个），AccessKey ID相当于用户名，AccessKey Secret相当于密码。回到NPM的主页面，点击 SSL Certificates - Add SSL Certificate - Let's Encrypt ，填入一些信息：Domain Names * 填入 *.你的域名 和 你的域名 ，比如 *.lisper517.top 和 lisper517.top （下面一行是说这个域名必须绑定到安装NPM的这台服务器上）；勾选上 Use a DNS Challenge 和 I Agree to the Let's Encrypt Terms of Service ；在下拉菜单中，填好邮箱，DNS厂商，令牌（下面的警告是说这个令牌会明文存到mariadb数据库里。由于数据库不对外开放，这个没什么影响）。Propagation Seconds是传播时间（DNS服务器对一条DNS解析是需要传播的，比如 lisper517.top -> 47.98.39.236 这一条解析关系，会先在阿里的DNS服务器上，然后传到谷歌的8.8.8.8、8.8.4.4和其它DNS服务器。这个传播过程可能会花几分钟）。申请SSL证书时，NPM会使用这台服务器的80端口向提供证书的厂商发送信息，以验证该公网ip是证书申请者所有，厂商还会查询DNS服务器的解析记录。平常申请免费证书时要求添加TXT类型的 _dnsauth.你的域名 来进行验证，也是这个目的。等待的时候，回到你的DNS厂商后台，添加一条解析记录，类型为 A ，主机记录为 *.你的域名 ，也就是一个泛解析。最后验证一下，访问 gqhjgqzn.你的域名 （子域名随便乱填），可以看到NPM的 Congratulations 页面，说明泛解析配置成功。如果需要更新证书，在证书旁边的下拉菜单中更新，并且docker restart NPM即可。4.NPM的使用页面上的 Proxy Hosts 就是前面介绍的子域名反代。另外还有 Redirection Hosts （重定向，就是HTTP码300那个）、 Streams （把服务器本机的端口连接到 其它机器/本机 的端口，端口转发）、 404 Hosts （404提供）。添加一个 Proxy Hosts ，比如想用 npm.你的域名 来访问npm服务，就如下填写：在 Details 页面， Domain Names * 填 npm.你的域名 ；Forward Hostname / IP* ，因为是本机+NPM容器内部，填 localhost 即可。如果是其它IP、域名，或者是本服务器的其它容器，注意要打开该机器的对应端口，或者填 ip addr show docker0 显示的docker网络ip；Forward Port * ，npm是开放在81端口，就填81；Block Common Exploits （堵住常见漏洞）和 Websockets Support （网络套接字支持。是一种让网络客户端向服务器端发送信息的通信协议）打开，也可以不打开；到 SSL 页面，选择刚才申请的通配符证书，把 Force SSL 打开。这样就ok了。浏览器访问 npm.你的域名 ，出现NPM管理页面，并且也是有HTTPS证书的。最后，如果想要更强的安全性，可以在ufw和服务器控制台，把81端口关掉。还可把NPM的docker-compose.yml中的 81->81 映射删掉，删除NPM容器并重新构建。还能节省宿主机的81端口。经过笔者的实验，按照以上的设置，只有NPM自己可以节省81端口。其它不在同一个yml的容器，比如用NPM反代Portainer，那么必须要把9000:9000加上，否则NPM找不到该容器（因为不在同一个yml），也不能用 docker inspect 看到的portainer的IP；而且在vultr服务器上实验时，必须在控制台、ufw都把9000打开，这样实际上也没有把端口隐藏，不过这也可能是vultr的锅。不管怎么说，如此一来安全性虽然仍是不错，但也大打折扣。可能的替代方法是每个新容器都在NPM的yml里加，但这样总会遇到冲突，比如两个stack都需要用mysql；NPM官方推荐的做法则是使用docker网络。（1）使用docker网络在 NPM官网 的进阶配置中提到，可以通过把NPM与后端服务加入同一个docker网络来提升安全性（不用暴露端口、占用宿主机的端口）。docker网络是可以把不同的容器放在同一个网络下以利于互相沟通交互的技术，docker-compose以后用的大大减少，笔者都快忘了。在服务器上先创建一个docker网络：docker network create NPM_group之后在NPM和所有其它需要NPM反代的yml里，末尾都加上：networks: default: external: true name: NPM_group即可。这样就把NPM和其它容器组放在同一网络下，反代的时候也不需要填 ip addr show docker0 显示的ip，而是直接填localhost或者容器名即可；还能在ufw、控制台把端口关掉，只需要22、80、443端口，非常的安全，非常的新鲜，非常的美味。最后，有些容器的网络会用host，比如frps的dashboard。把这些容器加到NPM_group网络下没有什么意义，此时NPM反代就要填 ip addr show docker0 显示的ip（一般是172.17.0.1），这个ip在容器没有使用 network_mode:host 又想要联系到宿主机的其它服务时都可以使用，从定义域上说是 容器外 ∩ 宿主机内 。（2）Access List通用密码为了安全起见，任何网页都应该尽量用HTTPS访问，因为HTTP在这一层一般都是不加密的。如果还想安全一点，或者某些网页服务没有自带的密码验证，或者不想别人看到某个子域名下的服务，就可以使用NPM的Access List。选中 Access Lists ， Name * 填 Global 或者自己想填什么都行。勾选 Satisify Any ；在 Authorization 里，填用户名密码。最后Save即可。现在测试一下。回到刚才为npm添加的 Proxy Host ，编辑一下，把 Details 页面的 Access List 选到刚才创建的这个 Global ，保存，刷新一下页面，就能看到页面是白色的，先弹出一个登录框，登录后才能看到网页内容。可惜的是，此时就算输入对的用户名密码也无法显示NPM页面了，这是因为一般的密码验证是每次请求时在请求头里添加 Authorization 来实现的，而Access List也占用了这个 Authorization 字段。所以对于自带登录验证的网页服务就可以不用 Access List ，或者如果实在想用 Access List 你也可以试试，有些应用不走 Authorization 。对这个问题的解答见 NPM-FAQ 。现在，访问 你的域名:81 ，把npm的 Proxy Host 的 Access List 改回去。如果还是无法登录到 npm.你的域名 ，就删除、再重新创建一下NPM的 Proxy Host 。5.NPM + frp网页服务都可以代理，而且服务器不需要打开多余的端口，proxy host填172.17.0.1即可。如果是非网页服务如ssh，比如想通过 ssh.你的域名:80 来访问内网机器的ssh，笔者从NPM端试了一下没有成功，应该从frp配置上下功夫。需要钻研的可参考 frp官方文档 ，总的来说是使用tcpmux，只需开放frps的一个端口（tcpmux端口），就可以通过不同的子域名访问所有ssh。目前docker-frp更新到了0.53.2，笔者使用frps:0.53.2和toml配置时发现dashboard没开放（使用旧的ini语法就可以）。经过与frps的一番搏斗，在 这个问题下 找到了解决方法，即使用 ifconfig 查看服务器的eth0的inet的ip，然后在toml里写 webServer.addr = "inet的ip" ，并且在NPM也把ip改成这个。所以说还能跑的东西就不应该更新。6.NPM + v2ray在之前的 Docker实践——如何查阅wiki百科及V2Ray 中，笔者介绍过 VLESS + TCP + TLS + WS 的配置。 v2ray + 网页 时也需要使用80、443端口，与NPM冲突。要把NPM反代的网页作为v2ray的网页，关键在于把：location /mysecretpath { proxy_redirect off; proxy_pass http://127.0.0.1:40000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }类似这样的一段配置加到NPM的nginx中，但笔者暂时没能成功。好在x-ui可以帮助解决这个问题，x-ui是一种服务器监控面板，集成了xray（xray可以说是下一代v2ray）。x-ui的高阶玩法参考 x-ui-github 及 节点配置 。以下搭建过程参考了 咕咕鸽的文章 。首先安装x-ui（这个脚本同样还能更新x-ui）：bash <(curl -Ls https://raw.githubusercontent.com/FranzKafkaYu/x-ui/master/install.sh)跟着安装步骤走即可，设置用户名、密码、x-ui端口，打开端口（在ufw和控制台防火墙里都打开）。先不要用NPM反代x-ui，到x-ui的web ui的 面板配置 里，会自动设置一个 面板 url 根路径 并跳转，记住这个x-ui路径（也可以自己修改）。然后到NPM随便选一个proxy-host，在 Custom locations 里添加子路径：location 填 x-ui路径； IP 填 172.17.0.1 ，即docker0的ip； Forward Port 填 x-ui端口。通过这个设置，访问 该proxy-host域名/x-ui路径 就能看到x-ui的网页。现在可以到控制台防火墙关掉x-ui端口，但ufw里要一直打开。接下来到x-ui，在 系统状态 里有一个 切换版本 ，换到比较新的xray版本；然后在 入站列表 里 添加入站，以 vmess + ws 为例，只需更改5个地方：备注，随便写； 协议，选vmess； 端口，可以不改，但是要记住，称为xray端口； 网络，选ws； 路径，随便写一个路径，这个路径称为xray路径； 如果是比较新的版本，可能需要添加一个用户，记住id。添加以后，到 面板设置 里 重启面板 。同时，在ufw和控制台防火墙里打开xray端口。最后到NPM里，刚才的proxy-host，在 Advanced 下添加几行个性化配置：location /xray路径 { proxy_redirect off; proxy_pass http://172.17.0.1:xray端口; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; proxy_read_timeout 300s; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }至此设置完成。此时访问 该proxy-host域名/xray路径 ，出现熟悉的 Bad Request ，这并不是错误，而是说明xray接收到了入站请求，伪装成功。x-ui在 入站列表 里，每条配置左边的 操作 处，可以分享二维码直接给客户端使用。以上使用的是vmess，vmess要求客户端与服务器端时间一致，如果发现客户端无法使用，经常是时区问题，可以通过 timedatectl set-timezone Asia/Shanghai 修改服务器的时区。vmess实际上是比较老的技术了，可以改用vless，但是vless没有加密，所以必须搭配tls等加密手段使用。笔者不管如何尝试，tls使用的证书都与NPM冲突，所以vless的加密需要从客户端更改。客户端扫码后，稍微修改一下：把xray端口改成443； 打开tls； ws中，填 该proxy-host域名。这样虽然服务器端没有加tls，但通过修改客户端配置，实际上也是通过NPM的tls加密了。7.其它问题（1）登录时卡在502NPM 2.9.14 版本，安装好后过了几天无法通过https登录（502 bad gateway），但是除了NPM的其它子域名都正常。重新打开NPM的81端口后能看到登录界面，但是输入用户名密码后仍然显示 Bad Gateway 。切换到 docker-compose up 观察输出，过一会儿显示用pip更新 certbot-dns-cloudflare 时连不上。参考 这个问题 ，把mysql数据库移出来：cd /docker/NPM docker-compose down mv /docker/NPM/data/mysql /docker/NPM/mysql vim docker-compose.yml把yml中的挂载也改一下，重新build容器，无法解决问题。该问题的场景是使用了NPM标准的yml文件，里面把mariadb的data目录放到了NPM的data里面，导致NPM容器能修改mariadb的data目录。笔者的yml不是这种情况。把NPM的版本改到最新的2.10.4，未能解决问题。根据 这个问题 ，把mariadb的镜像换到官方：image: 'mariadb' ，出现一大堆报错。根据 这个问题 的提示，进入NPM容器手动 pip install certbot-dns-cloudflare ，显示连不上pypi.org。美国太可恶了。最后的解决方法是手动修改 nginx/proxy host 中的conf。每过3个月证书过期，重装一次NPM。github上也有人说这个问题等几天就会消失。10天后果然可以登录了，目前的版本是2.9.14，没有其它问题。值得注意的是，笔者在云服务器上没有出现这种问题，也许因为云服务器是CentOS，家里的服务器是Ubuntu？（2）端口开放笔者遇到了这样的情况：NPM映射部分端口，加入docker网络（容器a）；其它一些容器使用 network_mode:host ，也加入相同的docker网络（容器b）；或者宿主机上一些没有使用docker的其它服务（服务c）。这时容器a要联络到容器b、服务c，要通过docker0的ip（ ip addr show docker0 ），同时要在ufw中打开该端口。如果你的宿主机不对公网开放，这没什么问题，但是对公网开放时，经常被人扫端口。笔者目前的解决方法是在ufw中指定来源ip，比如：ufw allow from 192.168.1.0/24 to any port 22允许 192.168.1.* 来源的ip访问22端口（ 24 是掩码，一种特定的写法）。这样端口虽然开放了，但外网无法访问。更多ufw的使用方法，请自行搜索，比如参考 这个问题 。（3）ipv6+DDNS时80、443封禁笔者使用IPv6+DDNS+NPM，发现内网可以访问域名，但是外网无法访问。具体来说是在 https://mtool.chinaz.com/port 这里查询域名下的端口是否开放，22、80、443都关闭，但是一些非常规端口却是开放的，并且ping域名也能解析到正确的ipv6地址。这种情况说明配置的没问题，但是网络运营商封了22、80、443这些端口。笔者在NPM中暂时解决了这个问题，就是使用nginx的端口转发功能（NPM中在 Streams 设置），将其它端口（如40443）的流量转发到443，并且打开40443端口。这样的话外网需要访问 https://npm.你的域名:40443 ，虽然多了个端口号，但也能使用。有文章说cloudflare的Origin Rules能达到更好的效果（外网访问不用加端口），笔者试用了一下还是不太会，最终放弃。另外，80、443被封了，申请免费证书可能会出问题。暂时没有着手解决这个问题，等下一次换证书的时候再说。申请的通配符证书是3个月的，笔者自动续签了一次，暂时没有问题。（4）无法申请证书在2.9.14版本，笔者申请通配符证书时出现错误，错误信息说执行 pip install certbot-dns-cloudflare==$(certbot --version | grep -Eo '[0-9](\.[0-9]+)+') cloudflare 这条命令时出错，还提示 ModuleNotFoundError: No module named 'CloudFlare' 。参考 github问题 ，进行如下操作：docker exec -it NPM bash #进入容器 python3 -m pip install --upgrade pip pip install --upgrade cloudflare==2.19.*重新申请通配符证书即可。中间因为网络问题，需要多尝试几次。