5.9 KiB
Nginx Proxy Manager
Self-hosted reverse proxy using Nginx Proxy Manager (NPM) via Docker Compose, with Cloudflare real IP restoration configured via a custom Nginx snippet.
Repository Contents
| File | Description |
|---|---|
docker-compose.yml |
NPM service definition with SQLite backend |
http_top.conf |
Custom Nginx snippet - restores real visitor IPs from Cloudflare headers |
Stack
- Nginx Proxy Manager -
jc21/nginx-proxy-manager:latest - SQLite - default embedded database (no separate DB container required)
- Cloudflare - assumed CDN/DNS proxy in front of this server
Prerequisites
- Docker + Docker Compose installed on the host
- Ports
80,443, and81available and open in your firewall - A domain with DNS managed through Cloudflare (for real IP restoration to be meaningful)
Setup
1. Clone / copy this repo onto your server
git clone <this-repo-url> nginx-proxy-manager
cd nginx-proxy-manager
2. Start the stack
docker compose up -d
NPM will be available at http://<host>:81.
3. First login
| Field | Default |
|---|---|
admin@example.com |
|
| Password | changeme |
You will be prompted to change both immediately after first login.
4. Install http_top.conf
This file must be placed where NPM will pick it up as a custom Nginx top-level HTTP block snippet. Copy it into the NPM data directory:
cp http_top.conf ./data/nginx/custom/http_top.conf
NPM loads files from
./data/nginx/custom/automatically. If thecustom/directory doesn't exist yet, start the stack once first so NPM creates its data layout, then copy the file and reload:
docker exec nginxproxymanager nginx -s reload
# or restart the container
docker compose restart app
Verify the snippet is active:
docker exec nginxproxymanager nginx -T | grep set_real_ip_from
Ports
| Port (Host) | Protocol | Purpose |
|---|---|---|
80 |
HTTP | Public HTTP (redirects to HTTPS) |
443 |
HTTPS | Public HTTPS / proxied traffic |
81 |
HTTP | NPM Admin web UI |
The admin UI on port
81should not be exposed to the internet. Restrict it to localhost or a trusted network at your firewall.
Cloudflare Real IP Restoration (http_top.conf)
When traffic passes through Cloudflare, Nginx sees Cloudflare's edge node IP rather than the real visitor IP. http_top.conf corrects this by:
- Trusting all current Cloudflare IP ranges (both IPv4 and IPv6)
- Reading the real client IP from the
CF-Connecting-IPheader Cloudflare injects
This ensures accurate IPs appear in access logs, rate limiting, and upstream application headers.
Keep this file updated. Cloudflare periodically adds new IP ranges. Re-generate this list periodically and reload Nginx.
Current ranges were last updated: Wed May 20 2026
To re-fetch current ranges and regenerate the file:
{
echo "# Cloudflare real IP restoration"
echo "# Auto-generated on $(date)"
for cidr in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do
echo "set_real_ip_from $cidr;"
done
echo "real_ip_header CF-Connecting-IP;"
} > http_top.conf
Cloudflare IP Updater
The http_top.conf file is automatically regenerated by update-cloudflare-ips.sh, which fetches current Cloudflare IP ranges, rewrites the file, and reloads Nginx.
Cron Schedule
Runs at 3:00 AM on the 1st of every month via root's crontab:
0 3 1 * * /usr/local/bin/update-cloudflare-ips.sh >> /var/log/update-cloudflare-ips.log 2>&1
Manual Run
To regenerate http_top.conf immediately without waiting for cron:
/usr/local/bin/update-cloudflare-ips.sh
Notes
- Forces
curl -4for both fetches since the host has no IPv6 egress, but still writes IPv6 CIDR ranges into the config for Nginx to trust. - If
nginx -tfails, the reload is aborted and the existing config remains active. - Logs are written to
/var/log/update-cloudflare-ips.log. - The NPM container is expected to be named
nginx-app-1. Update thedocker execlines in the script if the container name ever changes.
Persistent Data
| Host Path | Container Path | Contents |
|---|---|---|
./data |
/data |
NPM database, Nginx configs, custom snippets, logs |
./letsencrypt |
/etc/letsencrypt |
SSL certificates issued by Let's Encrypt |
Both directories are created automatically on first run.
SSL Certificates
NPM handles Let's Encrypt certificates automatically through the web UI. For domains proxied through Cloudflare, use the DNS Challenge method with your Cloudflare API token - this works even when port 80 is not directly reachable.
- In the NPM UI go to SSL Certificates → Add SSL Certificate
- Select Let's Encrypt and enable Use DNS Challenge
- Select Cloudflare as the provider and enter your API token
Updating NPM
docker compose pull
docker compose up -d
Data and certificates persist in ./data and ./letsencrypt.
Troubleshooting
Admin UI unreachable on port 81
Check that nothing else is bound to port 81 on the host and that your firewall allows it from your management IP.
Real IPs still showing as Cloudflare IPs in logs
Confirm http_top.conf is in ./data/nginx/custom/ and that Nginx reloaded after the file was placed. Check with:
docker exec nginxproxymanager nginx -T | grep set_real_ip_from
Certificate renewal failures
Ensure your Cloudflare API token has Zone:DNS:Edit permissions and that the token hasn't expired. Check NPM logs:
docker logs nginxproxymanager -f
Nginx config errors after editing custom snippets Test the config before reloading:
docker exec nginxproxymanager nginx -t