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

204 lines
5.9 KiB
Markdown

# 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 <this-repo-url> nginx-proxy-manager
cd nginx-proxy-manager
```
### 2. Start the stack
```bash
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:
```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
```