This needs : dockerhost
Compatible with NPM 2.17+ (jc21/nginx-proxy-manager:latest).
Custom rules (kept on purpose):
real_ip_header X-Forwarded-For(patched at start — required behind Cloudflare)- XFF chain via
$realip_remote_addrmap (client, <edge>) - GeoIP2 request headers to upstream
$trust_parent_proxygeo on$realip_remote_addr— lock a host to Cloudflare edge without breaking real client IP- Stock
$forward_host/$x_forwarded_*from current image (not a frozen fullnginx.confmount)
git clone https://github.com/ariadata/dc-nginxproxymanager.git nginx-proxy-manager
cd nginx-proxy-manager
# optional: rm -rf .git
# edit .env if needed
docker compose pullbash init.sh
docker compose up -dBy default Compose uses its own project network — no docker network create needed.
Optional shared external network (other stacks on the same bridge):
docker network create nginx-proxy-managerThen uncomment the networks: blocks in docker-compose.yml.
http://YOUR-IP:8181
On a fresh install, current NPM versions open a setup screen — you create the admin user yourself (no built-in default login anymore).
Older NPM used to ship with:
- User:
admin@example.com - Pass:
changeme
Done!
├── docker-compose.yml
├── .env
├── init.sh
├── update-geolite-db.sh
├── data/ # created by init.sh — mutable NPM state
├── letsencrypt/
└── configs/
├── proxy.conf # only full-file override (GeoIP + custom XFF)
├── scripts/entrypoint.sh # patches real_ip_header on stock nginx.conf
├── custom/ # → /data/nginx/custom
│ ├── root_top.conf # load_module geoip2
│ ├── events.conf
│ ├── http_top.conf # geoip2{} + includes
│ ├── xff-map.conf
│ ├── cloudflare-extra.conf # optional; NPM also fetches CF ranges
│ ├── parent_proxy_trust_access.conf # $trust_parent_proxy geo
│ └── parent_proxy_trust_access_additional.conf # optional extra CIDRs
└── geoip2/ # → /data/nginx/geoip2 (*.mmdb)
After correct real_ip, $remote_addr is the client. NPM Access Lists (allow/deny) use $remote_addr, so an Access List of Cloudflare CIDRs breaks (403 for everyone). Check the TCP peer instead via $trust_parent_proxy (from $realip_remote_addr).
- Remove any Access List that only allows Cloudflare IP ranges from that Proxy Host.
- In Proxy Host → Advanced, add:
# Cloudflare edge only
if ($trust_parent_proxy != 'cloudflare') {
return 403;
}Or allow Cloudflare or private/docker peers:
if ($trust_parent_proxy = 0) {
return 403;
}- Reload / save the host in NPM (or
docker compose exec nginx-proxy-manager nginx -s reload).
Labels: cloudflare, internal (10/8, 172.16/12, 192.168/16). Extra CIDRs → configs/custom/parent_proxy_trust_access_additional.conf.
This does not replace firewalling origin to Cloudflare IPs; it only rejects non-CF peers that reach NPM.
| Piece | Approach |
|---|---|
nginx.conf |
not mounted — image stock + 1-line real_ip_header patch in entrypoint |
| GeoIP modules | configs/custom/root_top.conf (official NPM hook) |
| GeoIP DBs / hooks | separate mounts under configs/ (not mixed into mutable data/) |
proxy.conf |
mounted — only way to add GeoIP/$custom_proxy_add_x_forwarded_for inside location / |
Refresh GeoIP DBs:
bash update-geolite-db.sh
docker compose exec nginx-proxy-manager nginx -s reloadfor being behind another nginx, use these in parent nginx config
proxy_ssl_name $host;
proxy_ssl_server_name on;Get Client Real IP by Header:
x-real-ip
(and X-Forwarded-For chain — first address is the client when recursive real_ip is configured downstream)
Added Headers for GeoIP2:
HTTP_GEOIP2_COUNTRY_CODE
HTTP_GEOIP2_COUNTRY_NAME
HTTP_GEOIP2_CITY_NAME
HTTP_GEOIP2_ASN_CODE
HTTP_GEOIP2_IP_ADDRESS
# Links:
https://download.maxmind.com/app/geoip_download
https://github.com/PrxyHunter/GeoLite2/releases/latest
https://cdn.jsdelivr.net/npm/geolite2-country/
https://cdn.jsdelivr.net/npm/geolite2-city/
https://cdn.jsdelivr.net/npm/geolite2-asn/