# Nginx Proxy Manager Self-hosted reverse proxy using [Nginx Proxy Manager](https://nginxproxymanager.com/) (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 ```bash git clone nginx-proxy-manager cd nginx-proxy-manager ``` ### 2. Start the stack ```bash docker compose up -d ``` NPM will be available at `http://: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: ```bash 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: ```bash docker exec nginxproxymanager nginx -s reload # or restart the container docker compose restart app ``` Verify the snippet is active: ```bash 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](https://www.cloudflare.com/ips/) (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: ```bash { 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: ```bash /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 ```bash 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: ```bash 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: ```bash docker logs nginxproxymanager -f ``` **Nginx config errors after editing custom snippets** Test the config before reloading: ```bash docker exec nginxproxymanager nginx -t ```