Synology Reverse Proxy for Multiple Services
I got tired of typing nas:8080, nas:8096 and nas:3000 from memory, and forwarding every one of those ports on the router felt wrong. DSM 6.0 and later have a reverse proxy screen that drives the built-in nginx, so I now expose only port 443 and route by hostname, all under one Let's Encrypt certificate.
I run it in front of Docker containers on the same box, but the backend can be any host on the LAN. You'll need DSM 6.0+, a domain or DDNS name (several hostnames like git.example.com and media.example.com), and ports 80 and 443 forwarded to the NAS.
Get one certificate that covers every hostname
Control Panel → Security → Certificate → Add → Add a new certificate → Get a certificate from Let's Encrypt. Enter the primary name and list the others under Subject Alternative Name, separated by semicolons:
Domain name: nas.example.com
Email: admin@example.com
Subject Alternative Name: git.example.com;media.example.com;wiki.example.com
Let's Encrypt validates each name over port 80, so all of them must resolve to the NAS at the moment you click Apply. Once issued, click Configure and assign this certificate to each reverse proxy entry you create below (they appear in the list after you create them).
One rule per hostname
Control Panel → Application Portal → Reverse Proxy → Create. One rule per hostname:
Description: Gitea
Source
Protocol: HTTPS
Hostname: git.example.com
Port: 443
Enable HSTS: yes
Destination
Protocol: HTTP
Hostname: localhost
Port: 3000
Repeat for media.example.com → localhost:8096 and so on. Leave Source hostname empty only if you want a catch-all; with several services, every rule needs its hostname.
WebSocket headers
Applications like Home Assistant, Rocket.Chat or anything with a live-updating UI use WebSockets and will half-work behind the proxy without two extra headers. On the rule, open Custom Header → Create → WebSocket. DSM 6.1 fills in the two entries for you:
Upgrade $http_upgrade
Connection $connection_upgrade
If your DSM build lacks the WebSocket shortcut, add both as custom headers manually with exactly those values.
Peek at the generated nginx config
I like to see what the GUI actually produced. It all lands in a file you can read over SSH:
sudo cat /etc/nginx/app.d/server.ReverseProxy.conf
You will see one server block per rule with proxy_pass http://localhost:3000; and the headers you added. Do not edit this file; DSM regenerates it whenever you save in the GUI. Reload the web server after checking syntax:
sudo nginx -t
sudo synoservicecfg --reload nginx
Tell the applications about the proxy
Some backends need to know their public URL. For Gitea set ROOT_URL = https://git.example.com/ in app.ini. For WordPress set WP_HOME and WP_SITEURL. Most containers also want X-Forwarded-For to log the real client IP; DSM's proxy sends X-Real-IP and X-Forwarded-For by default, so the application only has to be told to trust them.
Close the direct ports
Easy to forget: with the proxy in place, remove the router forwards for 3000, 8096 and friends. On the NAS, Control Panel → Security → Firewall → Edit Rules: allow 80 and 443 from anywhere, allow the application ports from the LAN subnet only, deny the rest.
Checking from outside
From outside the network:
curl -sI https://git.example.com/ | head -n 5
curl -sI https://media.example.com/ | grep -i strict-transport
Both should return HTTP/1.1 200 OK (or a 302 to a login page) with the Let's Encrypt certificate and, if HSTS is on, a Strict-Transport-Security header. Check the certificate names with:
openssl s_client -connect nas.example.com:443 -servername git.example.com </dev/null 2>/dev/null | openssl x509 -noout -text | grep DNS
Gotchas
- Browser shows the DSM login page instead of the application: the hostname did not match any rule and fell through to the default server. Check for a typo in Source hostname or a missing DNS record.
- Application loads but live updates stop, and the browser console says "WebSocket connection to 'wss://...' failed: Error during WebSocket handshake: Unexpected response code: 400": the Upgrade/Connection headers from the WebSocket section are missing.
- Uploads over a few megabytes fail with "413 Request Entity Too Large": DSM's nginx has a default
client_max_body_size. Add a custom header will not help; instead place a file in/usr/local/etc/nginx/conf.d/or, simpler, adjust the size limit inside the application if it offers one, because DSM overwrites the main config on updates. - Certificate warning for one hostname only: that name was not in the SAN list when the certificate was issued. Edit the certificate and re-request it.
Adding a container now means one rule, one DNS name, and one more entry on the certificate. I'd never go back to forwarding ports one by one.