From c8ebcfc9f7c0a556bbfdea5e435246c7c9c39fce Mon Sep 17 00:00:00 2001 From: Christopher Berger Date: Fri, 29 May 2026 02:01:07 +0000 Subject: [PATCH] Add README.md --- README.md | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9136b6 --- /dev/null +++ b/README.md @@ -0,0 +1,211 @@ +# 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 +``` + +To restore after accidental removal: + +```bash +crontab -e +# add the line above +``` + +### 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 +``` \ No newline at end of file