a fork of nginx-proxy-manager
See the codeIf you don't need the web GUI of NPMplus, you may also have a look at caddy: https://caddyserver.com
Note: this fork is distributed under the GNU Affero General Public License version 3 or any later version. It is based on the MIT licensed nginx-proxy-manager.
Note: by running NPMplus you agree to the ToS of Let's Encrypt/your custom CA.
Note: remember to expose udp/quic for the https port (443/udp).
Note: remember to add your domain to the hsts preload list if you enabled hsts for your domain.
Note: please report issues first to this fork before reporting them to the upstream repository.
docker exec -it npmplus password-reset.js USER_EMAIL [PASSWORD] [--disable-mfa]/lib/ld-linux-x86-64.so.2 --help, plain x86-64 is not supported only v2 and up) and aarch64/arm64 (other archs (including 64-bit ones) and any 32-bit arch (like armhf/armv7 (dropped), armel/armv6) are not supported, because of the duration to compile).certbot-dns-he, certbot-dns-dnspod, certbot-dns-online, certbot-dns-powerdns and certbot-dns-do (certbot-dns-do was replaced in upstream with v2.12.4 and then merged into NPMplus)docker compose up -d
https:// on port 81.cp -a should be enough)/etc/letsencrypt mount, since it was moved to /data while migration, then redeploy the compose fileNote: If you don't disable sharing in crowdsec, you may need to mention that this is sent to crowdsec in your privacy policy.
COLLECTIONS env:
crowdsecurity/crs in step 3true in your compose.yaml and redeploy/opt/crowdsec/conf/acquis.d/npmplus.yaml (path may be different depending how you installed crowdsec) and fill it with:filenames:
- /opt/npmplus/nginx/logs/access.log
labels:
type: npmplus
---
listen_addr: 0.0.0.0:7422
appsec_configs:
- crowdsecurity/appsec-default
# - crowdsecurity/crs # only if you installed crowdsecurity/appsec-crs
# - crowdsecurity/appsec-bot-* # only if you set up bot detection, see below
name: appsec
source: appsec
labels:
type: appsec
network_mode: host in your compose file for the NPMplus containerdocker exec crowdsec cscli bouncers add npmplus and save the api key of the output/opt/npmplus/crowdsec/crowdsec.confENABLED to trueAPI_KEYcompose.yamlproxy_(request_)buffering to off will not workBot detection challenges every request without a valid cookie, so clients which can not solve it, like apps, get blocked. Full documentation: https://doc.crowdsec.net/docs/next/appsec/bot_detection/enable, the short version:
crowdsecurity/appsec-bot-challenge, crowdsecurity/appsec-bot-challenge-strict or crowdsecurity/appsec-bot-challenge-permissive to the COLLECTIONS env, depending on your wanted thresholdcrowdsecurity/appsec-bot-* in the acquisition file from step 3, the wildcard only matches appsec-configs you actually installedIf you added rate limiting (limit_req_zone/limit_req) through the advanced config or /data/custom_nginx, crowdsecurity/nginx-req-limit-exceeded from the ZoeyVid/npmplus collection needs the error log, since nginx only writes the limiting requests, excess lines there. Add it to the acquisition file from step 3:
filenames:
- /opt/npmplus/nginx/logs/access.log
- /opt/npmplus/nginx/logs/error.log
labels:
type: npmplus
nginx writes the client IP itself, so bans can not be forged. The request line however is written out as the client sent it, a crafted one can put any value into target_fqdn, so do not build whitelists on the domain of an error log alert.
To set it per location: press the gear button, set the scheme to path, put in the path and paste the following in the new text field at the bottom, you need to adjust the last line:
location ~* [^/]\.php(?:$|/) {
fastcgi_split_path_info ^(.*\.php)(/.*)$;
try_files $fastcgi_script_name =404;
fastcgi_pass ...; # set this to the address of your php-fpm (socket/tcp): https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_pass
}
127.0.0.1/path to proxy to a subpath.
GET /cdf/abc to a custom location /cdf which proxies to 127.0.0.1/abc will proxy to 127.0.0.1/abc/abc, a custom location /cdf/ which proxies to 127.0.0.1/ will proxy to 127.0.0.1/abc and a custom location /cdf which proxies to 127.0.0.1 will proxy to 127.0.0.1/cdf/abcpath, a path ending with a / will be searched relative to the custom location (is uses nginx alias) and a path ending without a / will be searched relative to the main / location (it uses nginx root)pathNote: The upstream URL for an auth request provider can be overridden in the UI; a main location's override takes precedence over any custom location's override.
status_codes:
CHALLENGE: 401
DENY: 403
/opt/npmplus/custom_nginx/http_top.conf (or /opt/npmplus/custom_nginx/stream_top.conf for streams), if you changed /opt/npmplus to a different path make sure to change the path to fitupstream cu_mybackend {
zone cu_mybackend 128k;
server 127.0.0.1:44 resolve;
server 127.0.0.1:33 resolve;
server 127.0.0.1:22 resolve;
server 192.168.1.11:44 backup resolve;
}
cu_mybackend) and leave the forward port field empty (ports are defined inside the upstream block)
cu_ prefix (short for custom upstream) is required: NPMplus uses it to detect that the hostname refers to a custom upstream block and skips generating its own upstream block for ithttp://cu_myauth), SNI and Host header are then set to the requested host like for proxy hosts, so the servers inside the upstream block need to point directly to the auth service and not to NPMplus itself/opt/npmplus/tls/ech/cron.sh. You need to fill this file with a script to handle your ECH keys.cron.sh, use the built-in ech.sh command to generate your keys. The syntax is: ech.sh <public-name> <identifier> [max-name-length (default 64)]./opt/npmplus/tls/ech/ (saving the current and previous keys) and outputs the Base64-encoded ECH config list to standard output, which you can capture to update your DNS records.cron.sh must push the newly generated config to your DNS provider.ech-cron-cloudflare-example.sh. You can adapt this script, add your API tokens, define your zones/records, and place its contents into /opt/npmplus/tls/ech/cron.sh.cron.sh script and reload nginx on container start and then every hour after container start. You can change this interval by setting the ECH_ROTATION_INTERVAL environment variable in your compose.yaml.NGINX_LOAD_GEOIP2_MODULE env to true and redeploy NPMplus/opt/npmplus/custom_nginx/http_top.conf, if you changed /opt/npmplus to a different path make sure to change the path to fitgeoip2 /data/goaccess/geoip/GeoLite2-Country.mmdb {
auto_reload 60m;
$geoip2_country_iso_code country iso_code;
}
# whitelist example, you can add as many country codes as you want, country code list: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#XY
#map $geoip2_country_iso_code $geoip2_country_rule {
# default no;
# AA yes;
# XY yes;
# '' yes; # if you want to allow IPs with unknown country codes, if you don't do this make sure to allow private IPs
#}
# blacklist example, you can add as many country codes as you want, country code list: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#XY
#map $geoip2_country_iso_code $geoip2_country_rule {
# default yes;
# AA no;
# XY no;
# '' no; # if you want to block IPs with unknown country codes, if you do this make sure to allow private IPs
#}
# uncomment if you block/don't allow IPs with unknown country codes
#geo $is_private_ip {
# default no;
# 127.0.0.0/8 yes;
# 10.0.0.0/8 yes;
# 172.16.0.0/12 yes;
# 192.168.0.0/16 yes;
# 169.254.0.0/16 yes;
# ::1/128 yes;
# fc00::/7 yes;
# fec0::/10 yes;
#}
4a. to set it per location: press the gear button (on the tab itself, not the tab selection) on the details tab (or on the custom location you want to use), set your proxy settings and paste the following in the new text field at the bottom, you may want to adjust the last lines (do not use the advanced tab with this example as it may break cert renewals):
# uncomment if you block/don't allow IPs with unknown country codes
#if ($is_private_ip = yes) {
# set $geoip2_country_rule yes;
#}
if ($geoip2_country_rule = no) {
return 444; # this rejects the connection, but you can also return 403 to tell the client that it was denied
}
4b. to set it for an entire host: put this in the advanced tab:
# uncomment if you block/don't allow IPs with unknown country codes
#if ($is_private_ip = yes) {
# set $geoip2_country_rule yes;
#}
if ($request_uri ~* "^/\.well-known/acme-challenge/") {
set $geoip2_country_rule yes;
}
if ($geoip2_country_rule = no) {
return 444; # this rejects the connection, but you can also return 403 to tell the client that it was denied
}
4c. to set it for all http hosts of them same type: put this in the custom_nginx/server_proxy.conf / custom_nginx/server_redirect.conf / custom_nginx/server_dead.conf file(s):
# uncomment if you block/don't allow IPs with unknown country codes
#if ($is_private_ip = yes) {
# set $geoip2_country_rule yes;
#}
if ($request_uri ~* "^/\.well-known/acme-challenge/") {
set $geoip2_country_rule yes;
}
if ($geoip2_country_rule = no) {
return 444; # this rejects the connection, but you can also return 403 to tell the client that it was denied
}
4d. to set it for all http hosts: put this in the custom_nginx/server_http.conf file:
# uncomment if you block/don't allow IPs with unknown country codes
#if ($is_private_ip = yes) {
# set $geoip2_country_rule yes;
#}
if ($request_uri ~* "^/\.well-known/acme-challenge/") {
set $geoip2_country_rule yes;
}
if ($geoip2_country_rule = no) {
return 444; # this rejects the connection, but you can also return 403 to tell the client that it was denied
}
$geoip2_country_rule for each rule list (you need the unique name also in the custom locations)If you need to run scripts before NPMplus launches put them under: /opt/npmplus/prerun/*.sh (please add #!/usr/bin/env sh / #!/usr/bin/env bash to the top of the script) you need to create this folder yourself, also set the ENABLE_PRERUN env to true
users <=> cloudflare <=> NPMplus)your domain => SSL/TLS => SSL/TLS encryption => Current encryption mode => Configure to "Full (strict)"Note: This is not legal advice. The following points are intended to give you hints and help you identify areas that may be relevant to your privacy policy. This list may not be complete or correct.
LOGROTATE the access and error (also level “warn”), logs will be written to your disk and rotated daily and deleted based on your set number of set rotations. The access logs use these formats: http and stream. These include user information (like IPs), so make sure to also mention that these exist and what you are doing with them.When using multiple Access Lists on a Proxy Host or a Proxy Location, they are evaluated in a top-down order from the UI.
The Satisfy Any or Pass Auth to Upstream only get applied if they are set on the first Access List assigned to a proxy host/location
All are welcome to create pull requests for this project, but this does not mean that they will be merged, so better ask if your PR would be merged before creating one (via Discussion), typos and translations are excluded from this.
461 followers · starred Jan 2026
220 followers · starred May 2026
476 followers · starred Aug 2025
364 followers · starred Mar 2026
a fork of nginx-proxy-manager
See the codeIf you don't need the web GUI of NPMplus, you may also have a look at caddy: https://caddyserver.com
Note: this fork is distributed under the GNU Affero General Public License version 3 or any later version. It is based on the MIT licensed nginx-proxy-manager.
Note: by running NPMplus you agree to the ToS of Let's Encrypt/your custom CA.
Note: remember to expose udp/quic for the https port (443/udp).
Note: remember to add your domain to the hsts preload list if you enabled hsts for your domain.
Note: please report issues first to this fork before reporting them to the upstream repository.
docker exec -it npmplus password-reset.js USER_EMAIL [PASSWORD] [--disable-mfa]/lib/ld-linux-x86-64.so.2 --help, plain x86-64 is not supported only v2 and up) and aarch64/arm64 (other archs (including 64-bit ones) and any 32-bit arch (like armhf/armv7 (dropped), armel/armv6) are not supported, because of the duration to compile).certbot-dns-he, certbot-dns-dnspod, certbot-dns-online, certbot-dns-powerdns and certbot-dns-do (certbot-dns-do was replaced in upstream with v2.12.4 and then merged into NPMplus)docker compose up -d
https:// on port 81.cp -a should be enough)/etc/letsencrypt mount, since it was moved to /data while migration, then redeploy the compose fileNote: If you don't disable sharing in crowdsec, you may need to mention that this is sent to crowdsec in your privacy policy.
COLLECTIONS env:
crowdsecurity/crs in step 3true in your compose.yaml and redeploy/opt/crowdsec/conf/acquis.d/npmplus.yaml (path may be different depending how you installed crowdsec) and fill it with:filenames:
- /opt/npmplus/nginx/logs/access.log
labels:
type: npmplus
---
listen_addr: 0.0.0.0:7422
appsec_configs:
- crowdsecurity/appsec-default
# - crowdsecurity/crs # only if you installed crowdsecurity/appsec-crs
# - crowdsecurity/appsec-bot-* # only if you set up bot detection, see below
name: appsec
source: appsec
labels:
type: appsec
network_mode: host in your compose file for the NPMplus containerdocker exec crowdsec cscli bouncers add npmplus and save the api key of the output/opt/npmplus/crowdsec/crowdsec.confENABLED to trueAPI_KEYcompose.yamlproxy_(request_)buffering to off will not workBot detection challenges every request without a valid cookie, so clients which can not solve it, like apps, get blocked. Full documentation: https://doc.crowdsec.net/docs/next/appsec/bot_detection/enable, the short version:
crowdsecurity/appsec-bot-challenge, crowdsecurity/appsec-bot-challenge-strict or crowdsecurity/appsec-bot-challenge-permissive to the COLLECTIONS env, depending on your wanted thresholdcrowdsecurity/appsec-bot-* in the acquisition file from step 3, the wildcard only matches appsec-configs you actually installedIf you added rate limiting (limit_req_zone/limit_req) through the advanced config or /data/custom_nginx, crowdsecurity/nginx-req-limit-exceeded from the ZoeyVid/npmplus collection needs the error log, since nginx only writes the limiting requests, excess lines there. Add it to the acquisition file from step 3:
filenames:
- /opt/npmplus/nginx/logs/access.log
- /opt/npmplus/nginx/logs/error.log
labels:
type: npmplus
nginx writes the client IP itself, so bans can not be forged. The request line however is written out as the client sent it, a crafted one can put any value into target_fqdn, so do not build whitelists on the domain of an error log alert.
To set it per location: press the gear button, set the scheme to path, put in the path and paste the following in the new text field at the bottom, you need to adjust the last line:
location ~* [^/]\.php(?:$|/) {
fastcgi_split_path_info ^(.*\.php)(/.*)$;
try_files $fastcgi_script_name =404;
fastcgi_pass ...; # set this to the address of your php-fpm (socket/tcp): https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_pass
}
127.0.0.1/path to proxy to a subpath.
GET /cdf/abc to a custom location /cdf which proxies to 127.0.0.1/abc will proxy to 127.0.0.1/abc/abc, a custom location /cdf/ which proxies to 127.0.0.1/ will proxy to 127.0.0.1/abc and a custom location /cdf which proxies to 127.0.0.1 will proxy to 127.0.0.1/cdf/abcpath, a path ending with a / will be searched relative to the custom location (is uses nginx alias) and a path ending without a / will be searched relative to the main / location (it uses nginx root)pathNote: The upstream URL for an auth request provider can be overridden in the UI; a main location's override takes precedence over any custom location's override.
status_codes:
CHALLENGE: 401
DENY: 403
/opt/npmplus/custom_nginx/http_top.conf (or /opt/npmplus/custom_nginx/stream_top.conf for streams), if you changed /opt/npmplus to a different path make sure to change the path to fitupstream cu_mybackend {
zone cu_mybackend 128k;
server 127.0.0.1:44 resolve;
server 127.0.0.1:33 resolve;
server 127.0.0.1:22 resolve;
server 192.168.1.11:44 backup resolve;
}
cu_mybackend) and leave the forward port field empty (ports are defined inside the upstream block)
cu_ prefix (short for custom upstream) is required: NPMplus uses it to detect that the hostname refers to a custom upstream block and skips generating its own upstream block for ithttp://cu_myauth), SNI and Host header are then set to the requested host like for proxy hosts, so the servers inside the upstream block need to point directly to the auth service and not to NPMplus itself/opt/npmplus/tls/ech/cron.sh. You need to fill this file with a script to handle your ECH keys.cron.sh, use the built-in ech.sh command to generate your keys. The syntax is: ech.sh <public-name> <identifier> [max-name-length (default 64)]./opt/npmplus/tls/ech/ (saving the current and previous keys) and outputs the Base64-encoded ECH config list to standard output, which you can capture to update your DNS records.cron.sh must push the newly generated config to your DNS provider.ech-cron-cloudflare-example.sh. You can adapt this script, add your API tokens, define your zones/records, and place its contents into /opt/npmplus/tls/ech/cron.sh.cron.sh script and reload nginx on container start and then every hour after container start. You can change this interval by setting the ECH_ROTATION_INTERVAL environment variable in your compose.yaml.NGINX_LOAD_GEOIP2_MODULE env to true and redeploy NPMplus/opt/npmplus/custom_nginx/http_top.conf, if you changed /opt/npmplus to a different path make sure to change the path to fitgeoip2 /data/goaccess/geoip/GeoLite2-Country.mmdb {
auto_reload 60m;
$geoip2_country_iso_code country iso_code;
}
# whitelist example, you can add as many country codes as you want, country code list: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#XY
#map $geoip2_country_iso_code $geoip2_country_rule {
# default no;
# AA yes;
# XY yes;
# '' yes; # if you want to allow IPs with unknown country codes, if you don't do this make sure to allow private IPs
#}
# blacklist example, you can add as many country codes as you want, country code list: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#XY
#map $geoip2_country_iso_code $geoip2_country_rule {
# default yes;
# AA no;
# XY no;
# '' no; # if you want to block IPs with unknown country codes, if you do this make sure to allow private IPs
#}
# uncomment if you block/don't allow IPs with unknown country codes
#geo $is_private_ip {
# default no;
# 127.0.0.0/8 yes;
# 10.0.0.0/8 yes;
# 172.16.0.0/12 yes;
# 192.168.0.0/16 yes;
# 169.254.0.0/16 yes;
# ::1/128 yes;
# fc00::/7 yes;
# fec0::/10 yes;
#}
4a. to set it per location: press the gear button (on the tab itself, not the tab selection) on the details tab (or on the custom location you want to use), set your proxy settings and paste the following in the new text field at the bottom, you may want to adjust the last lines (do not use the advanced tab with this example as it may break cert renewals):
# uncomment if you block/don't allow IPs with unknown country codes
#if ($is_private_ip = yes) {
# set $geoip2_country_rule yes;
#}
if ($geoip2_country_rule = no) {
return 444; # this rejects the connection, but you can also return 403 to tell the client that it was denied
}
4b. to set it for an entire host: put this in the advanced tab:
# uncomment if you block/don't allow IPs with unknown country codes
#if ($is_private_ip = yes) {
# set $geoip2_country_rule yes;
#}
if ($request_uri ~* "^/\.well-known/acme-challenge/") {
set $geoip2_country_rule yes;
}
if ($geoip2_country_rule = no) {
return 444; # this rejects the connection, but you can also return 403 to tell the client that it was denied
}
4c. to set it for all http hosts of them same type: put this in the custom_nginx/server_proxy.conf / custom_nginx/server_redirect.conf / custom_nginx/server_dead.conf file(s):
# uncomment if you block/don't allow IPs with unknown country codes
#if ($is_private_ip = yes) {
# set $geoip2_country_rule yes;
#}
if ($request_uri ~* "^/\.well-known/acme-challenge/") {
set $geoip2_country_rule yes;
}
if ($geoip2_country_rule = no) {
return 444; # this rejects the connection, but you can also return 403 to tell the client that it was denied
}
4d. to set it for all http hosts: put this in the custom_nginx/server_http.conf file:
# uncomment if you block/don't allow IPs with unknown country codes
#if ($is_private_ip = yes) {
# set $geoip2_country_rule yes;
#}
if ($request_uri ~* "^/\.well-known/acme-challenge/") {
set $geoip2_country_rule yes;
}
if ($geoip2_country_rule = no) {
return 444; # this rejects the connection, but you can also return 403 to tell the client that it was denied
}
$geoip2_country_rule for each rule list (you need the unique name also in the custom locations)If you need to run scripts before NPMplus launches put them under: /opt/npmplus/prerun/*.sh (please add #!/usr/bin/env sh / #!/usr/bin/env bash to the top of the script) you need to create this folder yourself, also set the ENABLE_PRERUN env to true
users <=> cloudflare <=> NPMplus)your domain => SSL/TLS => SSL/TLS encryption => Current encryption mode => Configure to "Full (strict)"Note: This is not legal advice. The following points are intended to give you hints and help you identify areas that may be relevant to your privacy policy. This list may not be complete or correct.
LOGROTATE the access and error (also level “warn”), logs will be written to your disk and rotated daily and deleted based on your set number of set rotations. The access logs use these formats: http and stream. These include user information (like IPs), so make sure to also mention that these exist and what you are doing with them.When using multiple Access Lists on a Proxy Host or a Proxy Location, they are evaluated in a top-down order from the UI.
The Satisfy Any or Pass Auth to Upstream only get applied if they are set on the first Access List assigned to a proxy host/location
All are welcome to create pull requests for this project, but this does not mean that they will be merged, so better ask if your PR would be merged before creating one (via Discussion), typos and translations are excluded from this.
461 followers · starred Jan 2026
220 followers · starred May 2026
476 followers · starred Aug 2025
364 followers · starred Mar 2026