2026-05-29 02:03:05 +00:00
2026-05-29 02:03:05 +00:00

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, and 81 available 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
Email 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 the custom/ 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 81 should 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:

  1. Trusting all current Cloudflare IP ranges (both IPv4 and IPv6)
  2. Reading the real client IP from the CF-Connecting-IP header 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 -4 for both fetches since the host has no IPv6 egress, but still writes IPv6 CIDR ranges into the config for Nginx to trust.
  • If nginx -t fails, 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 the docker exec lines 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.

  1. In the NPM UI go to SSL Certificates → Add SSL Certificate
  2. Select Let's Encrypt and enable Use DNS Challenge
  3. 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
S
Description
Nginx reverse proxy with Docker Compose
Readme 34 KiB
Languages
Shell 100%