Nginx Proxy Manager on Synology
DSM's built-in reverse proxy under Control Panel → Application Portal is fine for a handful of services. Around the tenth container I lost patience with it: no per-host access lists, no custom nginx directives, and no wildcard certificates via DNS validation. Nginx Proxy Manager is a container that gives me all of that, with a clean web UI on top of nginx and certbot.
The catch on a Synology is that DSM's own nginx already holds ports 80 and 443. Below are the two ways around that, then the setup. You'll need Docker on DSM 6.2 with docker-compose over SSH, a domain whose DNS you control (for wildcards, a provider supported by NPM's DNS challenge such as Cloudflare or Route 53), and ports 80 and 443 forwarded on the router.
Getting ports 80 and 443
Option A, move DSM's ports. Control Panel → Network → DSM Settings lets you change 5000/5001, but the nginx listening on 80/443 belongs to Web Station and the HTTP-to-HTTPS redirect. If you do not use Web Station, uninstall it; DSM still binds 80 and 443 for its redirect, though. Fully freeing them means editing /usr/syno/share/nginx/nginx.mustache, which DSM updates overwrite; I do not recommend it.
Option B, which I use: run NPM on high ports and forward the router's 80 and 443 to them. The router maps external 80 to NAS port 8080 and external 443 to NAS port 4443. DSM keeps its ports, nothing in DSM is modified, and from the outside world nothing looks different. The only downside is that on the LAN you must use the high ports or set up split DNS to point the names at the NAS and use the reverse proxy in DSM for internal hostnames, which I do not bother with.
The Compose stack
sudo -i
mkdir -p /volume1/docker/npm/{data,letsencrypt,db}
cd /volume1/docker/npm
vi docker-compose.yml
version: '2.1'
services:
app:
image: jc21/nginx-proxy-manager:2
container_name: npm-app
restart: unless-stopped
ports:
- "8080:80"
- "4443:443"
- "8181:81"
environment:
DB_MYSQL_HOST: db
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: npm
DB_MYSQL_PASSWORD: npm-db-password
DB_MYSQL_NAME: npm
volumes:
- /volume1/docker/npm/data:/data
- /volume1/docker/npm/letsencrypt:/etc/letsencrypt
depends_on:
- db
db:
image: jc21/mariadb-aria:10.4
container_name: npm-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: npm-root-password
MYSQL_DATABASE: npm
MYSQL_USER: npm
MYSQL_PASSWORD: npm-db-password
volumes:
- /volume1/docker/npm/db:/var/lib/mysql
Port 81 is the admin UI, mapped to 8181 here. Start it:
docker-compose up -d
docker-compose logs -f app
Wait until the log shows the "Backend PID" lines and no database connection retries.
First login and a certificate
Open http://<nas-ip>:8181. Default credentials are admin@example.com / changeme; you are forced to change both immediately.
SSL Certificates → Add SSL Certificate → Let's Encrypt. For a wildcard, enter *.example.com and example.com, tick "Use a DNS Challenge", pick the provider and paste the API token in the credentials box (for Cloudflare: dns_cloudflare_api_token = ...). Set the propagation delay to 30 seconds or more. Agree to the terms and save; the certificate appears with an expiry three months out, and NPM renews it on its own.
For single hosts without a DNS API, leave the DNS challenge off; validation then comes in on port 80, so the router forward to 8080 must be in place first.
Proxy hosts
Hosts → Proxy Hosts → Add Proxy Host:
- Domain Names:
cloud.example.com. - Scheme: http. Forward Hostname/IP:
192.168.1.20. Forward Port:8088(the Nextcloud container from an earlier post). Using the NAS LAN IP rather than a container name works regardless of which Docker network the target is on. - Block Common Exploits: on. Websockets Support: on for anything with a live UI.
- SSL tab: pick the wildcard certificate, Force SSL, HTTP/2 Support, HSTS.
- Advanced tab, for Nextcloud specifically:
client_max_body_size 0;
proxy_request_buffering off;
Repeat for each service. Use the Access Lists menu to restrict admin-type UIs (Portainer, Grafana) to your LAN range or to basic-auth users, then attach the list to those hosts.
Finally, in DSM's firewall (Control Panel → Security → Firewall) allow 8080 and 4443 from all, 8181 from the LAN only.
Did it work?
curl -sI https://cloud.example.com | head -5
curl -sI http://cloud.example.com | head -3
The HTTPS request should return HTTP/2 200 with server: openresty, and the HTTP one a 301 to HTTPS. In the browser, check the certificate is the wildcard. Log in to Nextcloud through the new name and upload a large file to confirm the body-size directive is active. In NPM, the Proxy Hosts list shows each host as Online.
Things that bit me
- The admin UI shows "Bad Gateway" for a host: NPM cannot reach the forward address. Usually the target container's port is only bound to
127.0.0.1or the forward port is the container-internal port instead of the published one. Use the port fromdocker ps. - Certificate request fails with "Internal Error" and the log says
Some challenges have failed: for HTTP validation, port 80 is not reaching port 8080 on the NAS; for DNS validation, the API token lacks zone edit permission or propagation delay is too short. - Everything works from outside but the names time out on the LAN: NAT loopback. Either add local DNS overrides pointing the names to the NAS IP and accept using
:4443, or create matching entries in DSM's own reverse proxy on 443 for internal use. - After a DSM update NPM stops answering: the Docker package restarted and
npm-appcame up beforenpm-db.restart: unless-stoppedretries; if it stays down,docker-compose restart app.
Adding a new service is a two-minute job now. I leave DSM's reverse proxy for DSM's own apps.