204 lines
5.9 KiB
Markdown
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
|
|
``` |