Reverse-proxy settings for 20 self-hosted apps
You already run nginx, Nginx Proxy Manager, Caddy or Traefik, and one app misbehaves behind it: uploads die with 413, the page loads but never updates, the app logs every visitor as the proxy. For each of these twenty apps, here is what its own documentation says the proxy and the app need, in all four proxies' terms, each line marked with the page it comes from and the day that page was read.
What it cannot do: it does not test your running proxy, and it only knows what each project wrote down. Projects change their docs and defaults between versions, so every block shows the date its sources were checked; when your version is newer, compare with the linked page.
- Immich
- Nextcloud
- Home Assistant
- Vaultwarden
- Jellyfin
- Paperless-ngx
- Uptime Kuma
- Audiobookshelf
- Navidrome
- PhotoPrism
- Syncthing
- n8n
- Open WebUI
- Grafana
- Forgejo
- Frigate
- Portainer CE
- AdGuard Home
- ntfy
- Gotify
The defaults that cause most of it
- nginx accepts request bodies up to
client_max_body_size 1mand answers 413 above that; it waitsproxy_read_timeout 60sfor the app; and before nginx 1.29.7 it spoke HTTP/1.0 to the app, so WebSockets needproxy_http_version 1.1plus the Upgrade and Connection headers, which nginx never forwards by itself. [nginx docs below] - Nginx Proxy Manager raises the body limit to
2000mand the timeouts to90sfor every host, sends Host, X-Forwarded-Proto, X-Forwarded-For and X-Real-IP, and adds the WebSocket headers only when Websockets Support is on. Text in the Advanced tab lands in the server block. [NPM source below] - Caddy proxies WebSockets and sets X-Forwarded-For, -Proto and -Host on its own, and sets no body-size limit unless you add one, which is why most Caddy blocks here are two lines. [Caddy docs below]
- Traefik also passes WebSockets and the forwarded headers by default, but its entrypoint read timeout is 60 seconds, which is what cuts long uploads (Immich documents error 499). [Traefik docs below]
- The app usually has to be told to trust the proxy (Home Assistant, Nextcloud, Jellyfin, ntfy) or told its public https address (Nextcloud, Paperless-ngx, n8n, Grafana, Forgejo): that half is under “In app itself” in each section.
Immich app page
Upstream immich-server:2283 · WebSockets: required [1] · Upload limit: 50000M [1] · Timeouts: 600s [1] · docs checked 2026-09-26
# Immich: nginx for photos.example.org
# App-specific lines are checked against Immich's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name photos.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name photos.example.org;
ssl_certificate /etc/letsencrypt/live/photos.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/photos.example.org/privkey.pem;
client_max_body_size 50000M; # [1]
proxy_read_timeout 600s; # [1]
proxy_send_timeout 600s; # [1]
proxy_request_buffering off; # [1]
client_body_buffer_size 1024k; # [1]
send_timeout 600s; # [1]
proxy_redirect off; # [1]
location / {
proxy_pass http://immich-server:2283;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection "upgrade"; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Immich # Checked against Immich's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... photos.example.org Scheme ................ http Forward Hostname / IP . immich-server Forward Port .......... 2283 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 50000M; # [1] proxy_read_timeout 600s; # [1] proxy_send_timeout 600s; # [1] proxy_request_buffering off; # [1] client_body_buffer_size 1024k; # [1] send_timeout 600s; # [1] proxy_redirect off; # [1]
# Immich: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Immich's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
photos.example.org {
reverse_proxy immich-server:2283
}
# Immich: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Immich's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
immich-server:
labels:
traefik.enable: "true"
traefik.http.routers.immich.rule: "Host(`photos.example.org`)"
traefik.http.routers.immich.entrypoints: "websecure"
traefik.http.routers.immich.tls: "true"
traefik.http.routers.immich.tls.certresolver: "letsencrypt"
traefik.http.routers.immich.service: "immich"
traefik.http.services.immich.loadbalancer.server.port: "2283"
# [1] Immich's docs: Traefik's entrypoint readTimeout defaults to 60s, which stops video
# uploads after a minute (error 499). Raise it in Traefik's static configuration:
# entryPoints.websecure.transport.respondingTimeouts.readTimeout: 600s and idleTimeout:
# 600s. (The dankhost builder sets readTimeout to 0, no limit.)
In Immich itself
Optional: list your proxy's address in IMMICH_TRUSTED_PROXIES (comma-separated IPs) on the immich-server container, so Immich takes the visitor's address from the forwarded headers. [2]
IMMICH_TRUSTED_PROXIES=<proxy IP>
- Immich must be served on the root of a (sub)domain; a sub-path such as /immich is not supported. [1]
- If the proxy gets certificates with Let's Encrypt's http-01 challenge, make sure /.well-known/immich still reaches Immich, or the mobile app may fail to connect. [1]
- Immich docs: Reverse Proxy (nginx, Caddy and Traefik examples) checked 2026-09-26
- Immich docs: Environment Variables (IMMICH_TRUSTED_PROXIES) checked 2026-09-26
Nextcloud app page
Upstream app:80 · WebSockets: not in the docs · Upload limit: 512M [3] · Timeouts: defaults · docs checked 2026-09-26
# Nextcloud: nginx for cloud.example.org
# App-specific lines are checked against Nextcloud's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name cloud.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name cloud.example.org;
ssl_certificate /etc/letsencrypt/live/cloud.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cloud.example.org/privkey.pem;
client_max_body_size 512M; # [3]
proxy_buffering off; # [2]
location / {
proxy_pass http://app:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /.well-known/carddav { # [1]
return 301 $scheme://$host/remote.php/dav;
}
location /.well-known/caldav { # [1]
return 301 $scheme://$host/remote.php/dav;
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Nextcloud # Checked against Nextcloud's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... cloud.example.org Scheme ................ http Forward Hostname / IP . app Forward Port .......... 80 Cache Assets .......... off Block Common Exploits . on Websockets Support .... off (the docs ask for no WebSocket headers) SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 512M; # [3]
proxy_buffering off; # [2]
proxy_hide_header Upgrade; # [1]
location /.well-known/carddav { # [1]
return 301 $scheme://$host/remote.php/dav;
}
location /.well-known/caldav { # [1]
return 301 $scheme://$host/remote.php/dav;
}
- Nextcloud's manual: with Nginx Proxy Manager, add proxy_hide_header Upgrade; to the Custom Nginx Configuration, otherwise iPhones and iPads get 'Connection Closed'. [1]
# Nextcloud: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Nextcloud's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
cloud.example.org {
# [1]
redir /.well-known/carddav /remote.php/dav/ 301
# [1]
redir /.well-known/caldav /remote.php/dav/ 301
reverse_proxy app:80
}
# Nextcloud: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Nextcloud's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
app:
labels:
traefik.enable: "true"
traefik.http.routers.nextcloud.rule: "Host(`cloud.example.org`)"
traefik.http.routers.nextcloud.entrypoints: "websecure"
traefik.http.routers.nextcloud.tls: "true"
traefik.http.routers.nextcloud.tls.certresolver: "letsencrypt"
traefik.http.routers.nextcloud.service: "nextcloud"
traefik.http.routers.nextcloud.middlewares: "nextcloud-dav-redirect"
traefik.http.services.nextcloud.loadbalancer.server.port: "80"
traefik.http.middlewares.nextcloud-dav-redirect.redirectregex.regex: "^https://cloud\\.example\\.org/\\.well-known/(?:card|cal)dav$$"
traefik.http.middlewares.nextcloud-dav-redirect.redirectregex.replacement: "https://cloud.example.org/remote.php/dav/"
traefik.http.middlewares.nextcloud-dav-redirect.redirectregex.permanent: "true"
# [1] The dav-redirect middleware is the Traefik 2+ redirectregex the manual gives for
# /.well-known/carddav and caldav, pointed at /remote.php/dav/.
In Nextcloud itself
In config/config.php: trust the proxy's address, and fix the protocol and the command-line URL so links and notifications use https. [1]
'trusted_proxies' => ['<proxy IP>'], 'overwriteprotocol' => 'https', 'overwrite.cli.url' => 'https://cloud.example.org',
With the official nextcloud Docker image the same settings are environment variables; APACHE_DISABLE_REWRITE_IP=1 lets Nextcloud read the forwarded headers from the trusted proxy. [4]
APACHE_DISABLE_REWRITE_IP=1 TRUSTED_PROXIES=<proxy IP> OVERWRITEPROTOCOL=https OVERWRITECLIURL=https://cloud.example.org
- 512M is the value in Nextcloud's own nginx configuration. Browser uploads are sent in 100 MiB chunks by default (files.chunked_upload.max_size), so this limit is not the maximum file size. [2]
- The manual's nginx example also has a catch-all 'location ^~ /.well-known' redirect to /index.php; it is left out here because it would also catch Let's Encrypt's /.well-known/acme-challenge/ on the same host. The two DAV redirects are the ones the Caddy example has too. [1]
- Nextcloud admin manual: Reverse proxy (trusted_proxies, overwrite parameters, CalDAV/CardDAV redirects, the NPM proxy_hide_header note) checked 2026-09-26
- Nextcloud admin manual: Uploading big files (client_max_body_size; downloads over 1 GB need proxy_buffering off on an nginx frontend; 100 MiB upload chunks) checked 2026-09-26
- Nextcloud admin manual: NGINX configuration (client_max_body_size 512M) checked 2026-09-26
- nextcloud/docker README: behind a reverse proxy (TRUSTED_PROXIES, OVERWRITEPROTOCOL, OVERWRITECLIURL, APACHE_DISABLE_REWRITE_IP) checked 2026-09-26
Home Assistant app page
Upstream host.docker.internal:8123 · WebSockets: required [2] · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26
# Home Assistant: nginx for home.example.org
# App-specific lines are checked against Home Assistant's docs (2026-09-26); [n] = source n below.
# Home Assistant runs with host networking: host.docker.internal:8123 from nginx in a container
# (with an extra_hosts host-gateway entry), 127.0.0.1:8123 from nginx on the same machine.
upstream home-assistant {
server host.docker.internal:8123;
}
# WebSocket map [2]. It belongs in the http {} context, which conf.d files are.
# Leave it out if your nginx already defines $connection_upgrade.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name home.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name home.example.org;
ssl_certificate /etc/letsencrypt/live/home.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/home.example.org/privkey.pem;
proxy_buffering off; # [2]
location / {
proxy_pass http://home-assistant;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; # [2]
proxy_set_header Upgrade $http_upgrade; # [2]
proxy_set_header Connection $connection_upgrade; # [2]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Home Assistant # Checked against Home Assistant's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... home.example.org Scheme ................ http Forward Hostname / IP . host.docker.internal Forward Port .......... 8123 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [2] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
proxy_buffering off; # [2]
# Home Assistant: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Home Assistant's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
home.example.org {
reverse_proxy host.docker.internal:8123
}
# Home Assistant: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Home Assistant's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
# Home Assistant uses host networking: Traefik reaches it at host.docker.internal:8123.
services:
homeassistant:
labels:
traefik.enable: "true"
traefik.http.routers.home-assistant.rule: "Host(`home.example.org`)"
traefik.http.routers.home-assistant.entrypoints: "websecure"
traefik.http.routers.home-assistant.tls: "true"
traefik.http.routers.home-assistant.tls.certresolver: "letsencrypt"
traefik.http.routers.home-assistant.service: "home-assistant"
traefik.http.services.home-assistant.loadbalancer.server.port: "8123"
In Home Assistant itself
Home Assistant 2026.8 and newer: Settings > System > Network > HTTP server, turn on Trust X-Forwarded-For and add the proxy to Trusted proxies. Requests from a reverse proxy are blocked until both are set. For a range, give the network address (192.168.1.0/24, not 192.168.1.50/24). [1]
Trust X-Forwarded-For: on Trusted proxies: <proxy IP>
Before 2026.8 the same two settings lived in configuration.yaml. On upgrade Home Assistant imports them and raises a repair issue until you delete the http: block. [1]
http: use_x_forwarded_for: true trusted_proxies: - <proxy IP>
- Home Assistant Container keeps port 8123 (the 2026.8 default of 80 applies to Home Assistant OS). Its documented compose file uses host networking, so the upstream is the host: host.docker.internal:8123 from a proxy container with an extra_hosts host-gateway entry, or 127.0.0.1:8123 for a proxy installed on the same machine. [1]
Vaultwarden app page
Upstream vaultwarden:80 · WebSockets: required [1] · Upload limit: 525M [1] · Timeouts: defaults · docs checked 2026-09-26
# Vaultwarden: nginx for vault.example.org
# App-specific lines are checked against Vaultwarden's docs (2026-09-26); [n] = source n below.
# WebSocket map [1]. It belongs in the http {} context, which conf.d files are.
# Leave it out if your nginx already defines $connection_upgrade.
map $http_upgrade $connection_upgrade {
default upgrade;
'' "";
}
server {
listen 80;
listen [::]:80;
server_name vault.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name vault.example.org;
ssl_certificate /etc/letsencrypt/live/vault.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vault.example.org/privkey.pem;
client_max_body_size 525M; # [1]
location / {
proxy_pass http://vaultwarden:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection $connection_upgrade; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Vaultwarden # Checked against Vaultwarden's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... vault.example.org Scheme ................ http Forward Hostname / IP . vaultwarden Forward Port .......... 80 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 525M; # [1]
# Vaultwarden: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Vaultwarden's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
vault.example.org {
reverse_proxy vaultwarden:80 {
# [1]
header_up X-Real-IP {remote_host}
}
}
# Vaultwarden: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Vaultwarden's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
vaultwarden:
labels:
traefik.enable: "true"
traefik.http.routers.vaultwarden.rule: "Host(`vault.example.org`)"
traefik.http.routers.vaultwarden.entrypoints: "websecure"
traefik.http.routers.vaultwarden.tls: "true"
traefik.http.routers.vaultwarden.tls.certresolver: "letsencrypt"
traefik.http.routers.vaultwarden.service: "vaultwarden"
traefik.http.services.vaultwarden.loadbalancer.server.port: "80"
In Vaultwarden itself
No app-side setting for a reverse proxy. The project documents nothing more.
- The wiki's map sends an empty Connection value instead of 'close' when there is no upgrade, so keepalive connections to Vaultwarden keep working. [1]
- On 504 Gateway Timeout the wiki suggests longer timeouts in the server block: proxy_connect_timeout, proxy_send_timeout, proxy_read_timeout and send_timeout 777. [1]
Jellyfin app page
Upstream jellyfin:8096 · WebSockets: required on /socket [1] · Upload limit: 20M [1] · Timeouts: defaults · docs checked 2026-09-26
# Jellyfin: nginx for media.example.org
# App-specific lines are checked against Jellyfin's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name media.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name media.example.org;
ssl_certificate /etc/letsencrypt/live/media.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/media.example.org/privkey.pem;
client_max_body_size 20M; # [1]
location / {
proxy_pass http://jellyfin:8096;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme; # [1]
proxy_set_header X-Forwarded-Host $http_host; # [1]
proxy_buffering off; # [1]
}
# WebSocket traffic [1]
location /socket {
proxy_pass http://jellyfin:8096;
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection "upgrade"; # [1]
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme; # [1]
proxy_set_header X-Forwarded-Host $http_host; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Jellyfin # Checked against Jellyfin's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... media.example.org Scheme ................ http Forward Hostname / IP . jellyfin Forward Port .......... 8096 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 20M; # [1]
proxy_buffering off; # [1]
# NPM puts this text inside the server block, where proxy_set_header does not reach its own
# location /. So this location / replaces NPM's (an Access List on this host no longer applies).
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
include conf.d/include/proxy.conf;
proxy_set_header X-Forwarded-Protocol $scheme; # [1]
proxy_set_header X-Forwarded-Host $http_host; # [1]
}
# Jellyfin: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Jellyfin's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
media.example.org {
reverse_proxy jellyfin:8096
}
# Jellyfin: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Jellyfin's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
jellyfin:
labels:
traefik.enable: "true"
traefik.http.routers.jellyfin.rule: "Host(`media.example.org`)"
traefik.http.routers.jellyfin.entrypoints: "websecure"
traefik.http.routers.jellyfin.tls: "true"
traefik.http.routers.jellyfin.tls.certresolver: "letsencrypt"
traefik.http.routers.jellyfin.service: "jellyfin"
traefik.http.services.jellyfin.loadbalancer.server.port: "8096"
In Jellyfin itself
Dashboard > Networking: add the proxy's IP address to Known Proxies. Jellyfin ignores forwarded-for headers from any other address, so without it every visitor looks like the proxy and remote-access limits do not work. [2]
Known Proxies: <proxy IP>
- The docs' comment: nginx's default client_max_body_size of 1M 'might not be enough for some posters'. [1]
Paperless-ngx app page
Upstream webserver:8000 · WebSockets: required [1] · Upload limit: 10M [1] · Timeouts: defaults · docs checked 2026-09-26
# Paperless-ngx: nginx for docs.example.org
# App-specific lines are checked against Paperless-ngx's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name docs.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name docs.example.org;
ssl_certificate /etc/letsencrypt/live/docs.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/docs.example.org/privkey.pem;
client_max_body_size 10M; # [1]
location / {
proxy_pass http://webserver:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name; # [1]
proxy_redirect off; # [1]
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection "upgrade"; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Paperless-ngx # Checked against Paperless-ngx's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... docs.example.org Scheme ................ http Forward Hostname / IP . webserver Forward Port .......... 8000 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 10M; # [1]
proxy_redirect off; # [1]
proxy_send_timeout 600m; # [1]
proxy_read_timeout 600m; # [1]
# NPM puts this text inside the server block, where proxy_set_header does not reach its own
# location /. So this location / replaces NPM's (an Access List on this host no longer applies).
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
include conf.d/include/proxy.conf;
proxy_set_header X-Forwarded-Host $server_name; # [1]
}
- The wiki: NPM's 90-second default timeout cuts the WebSocket when nothing changes for 90 seconds; raise it in the Custom Nginx Configuration. [1]
# Paperless-ngx: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Paperless-ngx's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
docs.example.org {
reverse_proxy webserver:8000
}
# Paperless-ngx: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Paperless-ngx's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
webserver:
labels:
traefik.enable: "true"
traefik.http.routers.paperless.rule: "Host(`docs.example.org`)"
traefik.http.routers.paperless.entrypoints: "websecure"
traefik.http.routers.paperless.tls: "true"
traefik.http.routers.paperless.tls.certresolver: "letsencrypt"
traefik.http.routers.paperless.service: "paperless"
traefik.http.services.paperless.loadbalancer.server.port: "8000"
In Paperless-ngx itself
Set PAPERLESS_URL to the public address (no trailing slash, no path); the wiki says it is required behind a reverse proxy. It fills ALLOWED_HOSTS, CORS_ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS. [2]
PAPERLESS_URL=https://docs.example.org
The wiki also suggests these three when the proxy forwards host, port and protocol. [1]
PAPERLESS_USE_X_FORWARD_HOST=true PAPERLESS_USE_X_FORWARD_PORT=true PAPERLESS_PROXY_SSL_HEADER='["HTTP_X_FORWARDED_PROTO", "https"]'
- 10M is the wiki's example ('Adjust as required. This is the maximum size for file uploads.'); raise it if you upload larger scans. [1]
- Paperless-ngx wiki: Using a Reverse Proxy with Paperless-ngx (nginx, Nginx Proxy Manager) checked 2026-09-26
- Paperless-ngx docs source: configuration.md (PAPERLESS_URL) checked 2026-09-26
Uptime Kuma app page
Upstream uptime-kuma:3001 · WebSockets: required [1] · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26
# Uptime Kuma: nginx for status.example.org
# App-specific lines are checked against Uptime Kuma's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name status.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name status.example.org;
ssl_certificate /etc/letsencrypt/live/status.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/status.example.org/privkey.pem;
location / {
proxy_pass http://uptime-kuma:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection "upgrade"; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Uptime Kuma # Checked against Uptime Kuma's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... status.example.org Scheme ................ http Forward Hostname / IP . uptime-kuma Forward Port .......... 3001 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
# Nothing to add: Uptime Kuma's docs give no setting beyond NPM's defaults. The project documents nothing more.
# Uptime Kuma: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Uptime Kuma's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
status.example.org {
reverse_proxy uptime-kuma:3001
}
# Uptime Kuma: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Uptime Kuma's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
uptime-kuma:
labels:
traefik.enable: "true"
traefik.http.routers.uptime-kuma.rule: "Host(`status.example.org`)"
traefik.http.routers.uptime-kuma.entrypoints: "websecure"
traefik.http.routers.uptime-kuma.tls: "true"
traefik.http.routers.uptime-kuma.tls.certresolver: "letsencrypt"
traefik.http.routers.uptime-kuma.service: "uptime-kuma"
traefik.http.services.uptime-kuma.loadbalancer.server.port: "3001"
In Uptime Kuma itself
If Uptime Kuma is reachable only through the proxy, turn on trusted proxy headers so logs show the visitor's address. [1]
Settings > Reverse Proxy > HTTP Headers > Trust Proxy: Yes
- The wiki: 'Unlike other web apps, Uptime Kuma is based on WebSocket.' A sub-directory such as /uptimekuma is not supported. [1]
- Uptime Kuma wiki: Reverse Proxy (nginx, Nginx Proxy Manager, Trust Proxy) checked 2026-09-26
Audiobookshelf app page
Upstream audiobookshelf:80 · WebSockets: required [1] · Upload limit: 10240M [2] · Timeouts: defaults · docs checked 2026-09-26
# Audiobookshelf: nginx for books.example.org
# App-specific lines are checked against Audiobookshelf's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name books.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name books.example.org;
ssl_certificate /etc/letsencrypt/live/books.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/books.example.org/privkey.pem;
client_max_body_size 10240M; # [2]
location / {
proxy_pass http://audiobookshelf:80;
proxy_set_header Host $http_host; # [2]
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect http:// https://; # [2]
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection "upgrade"; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Audiobookshelf # Checked against Audiobookshelf's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... books.example.org Scheme ................ http Forward Hostname / IP . audiobookshelf Forward Port .......... 80 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 10240M; # [2] proxy_redirect http:// https://; # [2] # Host: NPM already sends Host $host, the same value as $http_host while NPM listens on 443.
- The docs: only if NPM listens on a non-standard port (not 443) does the Host header need changing, with a custom location '/' carrying proxy_set_header Host $http_host. On 443 NPM's own Host $host is the same value. [3]
# Audiobookshelf: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Audiobookshelf's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
books.example.org {
reverse_proxy audiobookshelf:80
}
# Audiobookshelf: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Audiobookshelf's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
audiobookshelf:
labels:
traefik.enable: "true"
traefik.http.routers.audiobookshelf.rule: "Host(`books.example.org`)"
traefik.http.routers.audiobookshelf.entrypoints: "websecure"
traefik.http.routers.audiobookshelf.tls: "true"
traefik.http.routers.audiobookshelf.tls.certresolver: "letsencrypt"
traefik.http.routers.audiobookshelf.service: "audiobookshelf"
traefik.http.services.audiobookshelf.loadbalancer.server.port: "80"
In Audiobookshelf itself
No app-side setting for a reverse proxy. The project documents nothing more.
- 10240M is the docs' value, there to 'prevent 413 Request Entity Too Large' on uploads. [2]
- Audiobookshelf README: Reverse Proxy Set Up ('Audiobookshelf requires a websocket connection') checked 2026-09-26
- Audiobookshelf docs: Reverse proxy > NGINX (client_max_body_size 10240M, Host $http_host, proxy_redirect) checked 2026-09-26
- Audiobookshelf docs: Reverse proxy > NPM (websockets on; Host header for non-standard ports) checked 2026-09-26
PhotoPrism app page
Upstream photoprism:2342 · WebSockets: required [1] · Upload limit: 512M [1] · Timeouts: 600s [1] · docs checked 2026-09-26
# PhotoPrism: nginx for gallery.example.org
# App-specific lines are checked against PhotoPrism's docs (2026-09-26); [n] = source n below.
# WebSocket map [1]. It belongs in the http {} context, which conf.d files are.
# Leave it out if your nginx already defines $connection_upgrade.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name gallery.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name gallery.example.org;
ssl_certificate /etc/letsencrypt/live/gallery.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gallery.example.org/privkey.pem;
client_max_body_size 512M; # [1]
proxy_read_timeout 600s; # [1]
proxy_send_timeout 600s; # [1]
proxy_buffering off; # [1]
location / {
proxy_pass http://photoprism:2342;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host; # [1]
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection $connection_upgrade; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for PhotoPrism # Checked against PhotoPrism's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... gallery.example.org Scheme ................ http Forward Hostname / IP . photoprism Forward Port .......... 2342 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 512M; # [1]
proxy_read_timeout 600s; # [1]
proxy_send_timeout 600s; # [1]
proxy_buffering off; # [1]
# NPM puts this text inside the server block, where proxy_set_header does not reach its own
# location /. So this location / replaces NPM's (an Access List on this host no longer applies).
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
include conf.d/include/proxy.conf;
proxy_set_header X-Forwarded-Host $host; # [1]
}
# PhotoPrism: Caddyfile site block, the same one the dankhost builder writes.
# Checked against PhotoPrism's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
gallery.example.org {
# [2]
encode zstd gzip
reverse_proxy photoprism:2342 {
# [2]
flush_interval -1
}
}
# PhotoPrism: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against PhotoPrism's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
photoprism:
labels:
traefik.enable: "true"
traefik.http.routers.photoprism.rule: "Host(`gallery.example.org`)"
traefik.http.routers.photoprism.entrypoints: "websecure"
traefik.http.routers.photoprism.tls: "true"
traefik.http.routers.photoprism.tls.certresolver: "letsencrypt"
traefik.http.routers.photoprism.service: "photoprism"
traefik.http.routers.photoprism.middlewares: "photoprism-compress"
traefik.http.services.photoprism.loadbalancer.server.port: "2342"
traefik.http.services.photoprism.loadbalancer.responseforwarding.flushinterval: "-1ms"
traefik.http.middlewares.photoprism-compress.compress.encodings: "zstd,gzip"
# [2] The compress middleware (zstd, gzip) and responseForwarding.flushInterval=-1ms are
# Traefik's form of the Caddy example's encode and flush_interval -1.
In PhotoPrism itself
Keep PhotoPrism's own TLS off so the proxy handles certificates, and set the public site URL. If the proxy reaches PhotoPrism from outside Docker's default internal range, add its IP or CIDR to PHOTOPRISM_TRUSTED_PROXY. [1]
PHOTOPRISM_DISABLE_TLS="true" PHOTOPRISM_SITE_URL="https://gallery.example.org/" PHOTOPRISM_TRUSTED_PROXY="<proxy IP or CIDR>"
Syncthing app page
Upstream syncthing:8384 · WebSockets: not in the docs · Upload limit: not given · Timeouts: 600s [1] · docs checked 2026-09-26
# Syncthing: nginx for sync.example.org
# App-specific lines are checked against Syncthing's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name sync.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name sync.example.org;
ssl_certificate /etc/letsencrypt/live/sync.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sync.example.org/privkey.pem;
proxy_read_timeout 600s; # [1]
proxy_send_timeout 600s; # [1]
location / {
proxy_pass http://syncthing:8384;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Syncthing # Checked against Syncthing's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... sync.example.org Scheme ................ http Forward Hostname / IP . syncthing Forward Port .......... 8384 Cache Assets .......... off Block Common Exploits . on Websockets Support .... off (the docs ask for no WebSocket headers) SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
proxy_read_timeout 600s; # [1] proxy_send_timeout 600s; # [1]
# Syncthing: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Syncthing's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
sync.example.org {
reverse_proxy syncthing:8384 {
# [1]
header_up Host {upstream_hostport}
}
}
# Syncthing: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Syncthing's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
syncthing:
labels:
traefik.enable: "true"
traefik.http.routers.syncthing.rule: "Host(`sync.example.org`)"
traefik.http.routers.syncthing.entrypoints: "websecure"
traefik.http.routers.syncthing.tls: "true"
traefik.http.routers.syncthing.tls.certresolver: "letsencrypt"
traefik.http.routers.syncthing.service: "syncthing"
traefik.http.services.syncthing.loadbalancer.server.port: "8384"
traefik.http.services.syncthing.loadbalancer.passhostheader: "false"
# [1] passHostHeader=false is Traefik's form of the Caddy example's header_up Host
# {upstream_hostport}: Syncthing receives its own address as Host.
In Syncthing itself
No app-side setting for a reverse proxy. The project documents nothing more.
- Syncthing's examples serve the GUI under /syncthing/ on an existing site; here it gets its own subdomain, so the location is /. [1]
n8n app page
Upstream n8n:5678 · WebSockets: required [2] · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26
# n8n: nginx for n8n.example.org
# App-specific lines are checked against n8n's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name n8n.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name n8n.example.org;
ssl_certificate /etc/letsencrypt/live/n8n.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/n8n.example.org/privkey.pem;
location / {
proxy_pass http://n8n:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host; # [1]
proxy_http_version 1.1; # [2]
proxy_set_header Upgrade $http_upgrade; # [2]
proxy_set_header Connection "upgrade"; # [2]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for n8n # Checked against n8n's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... n8n.example.org Scheme ................ http Forward Hostname / IP . n8n Forward Port .......... 5678 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [2] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
# NPM puts this text inside the server block, where proxy_set_header does not reach its own
# location /. So this location / replaces NPM's (an Access List on this host no longer applies).
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
include conf.d/include/proxy.conf;
proxy_set_header X-Forwarded-Host $host; # [1]
}
# n8n: Caddyfile site block, the same one the dankhost builder writes.
# Checked against n8n's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
n8n.example.org {
reverse_proxy n8n:5678
}
# n8n: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against n8n's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
n8n:
labels:
traefik.enable: "true"
traefik.http.routers.n8n.rule: "Host(`n8n.example.org`)"
traefik.http.routers.n8n.entrypoints: "websecure"
traefik.http.routers.n8n.tls: "true"
traefik.http.routers.n8n.tls.certresolver: "letsencrypt"
traefik.http.routers.n8n.service: "n8n"
traefik.http.services.n8n.loadbalancer.server.port: "5678"
In n8n itself
Set the public webhook URL and tell n8n it sits behind one proxy. N8N_WEBHOOK_URL replaces WEBHOOK_URL, which is deprecated from n8n 2.35.0. [1]
N8N_WEBHOOK_URL=https://n8n.example.org/ N8N_PROXY_HOPS=1
- The editor gets live updates over WebSockets by default (N8N_PUSH_BACKEND=websocket); the alternative is sse. [2]
Open WebUI app page
Upstream open-webui:8080 · WebSockets: required [1] · Upload limit: 20M [1] · Timeouts: 10m [1] · docs checked 2026-09-26
# Open WebUI: nginx for chat.example.org
# App-specific lines are checked against Open WebUI's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name chat.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name chat.example.org;
ssl_certificate /etc/letsencrypt/live/chat.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.example.org/privkey.pem;
client_max_body_size 20M; # [1]
proxy_read_timeout 10m; # [1]
proxy_buffering off; # [1]
proxy_cache off; # [1]
location / {
proxy_pass http://open-webui:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection "upgrade"; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Open WebUI # Checked against Open WebUI's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... chat.example.org Scheme ................ http Forward Hostname / IP . open-webui Forward Port .......... 8080 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 20M; # [1] proxy_read_timeout 10m; # [1] proxy_buffering off; # [1] proxy_cache off; # [1]
- The docs give 30-minute timeouts for very long completions in NPM (proxy_read_timeout 1800; proxy_send_timeout 1800; proxy_connect_timeout 1800;). If you use them, replace the proxy_read_timeout line above rather than adding a second one: nginx refuses a duplicate. [1]
# Open WebUI: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Open WebUI's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
chat.example.org {
reverse_proxy open-webui:8080
}
# Open WebUI: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Open WebUI's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
open-webui:
labels:
traefik.enable: "true"
traefik.http.routers.open-webui.rule: "Host(`chat.example.org`)"
traefik.http.routers.open-webui.entrypoints: "websecure"
traefik.http.routers.open-webui.tls: "true"
traefik.http.routers.open-webui.tls.certresolver: "letsencrypt"
traefik.http.routers.open-webui.service: "open-webui"
traefik.http.services.open-webui.loadbalancer.server.port: "8080"
In Open WebUI itself
Set CORS_ALLOW_ORIGIN to the public address. The docs: without it WebSocket connections fail behind a proxy such as NPM, even with Websockets Support on. [1]
CORS_ALLOW_ORIGIN="https://chat.example.org"
- The docs call buffering 'the most common cause of garbled markdown': with proxy_buffering on, nginx re-chunks the streamed answer and tokens such as ** and ## show up raw. [1]
Grafana app page
Upstream grafana:3000 · WebSockets: required on /api/live/ [1] · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26
# Grafana: nginx for grafana.example.org
# App-specific lines are checked against Grafana's docs (2026-09-26); [n] = source n below.
# WebSocket map [1]. It belongs in the http {} context, which conf.d files are.
# Leave it out if your nginx already defines $connection_upgrade.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name grafana.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name grafana.example.org;
ssl_certificate /etc/letsencrypt/live/grafana.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/grafana.example.org/privkey.pem;
location / {
proxy_pass http://grafana:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket traffic [1]
location /api/live/ {
proxy_pass http://grafana:3000;
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection $connection_upgrade; # [1]
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Grafana # Checked against Grafana's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... grafana.example.org Scheme ................ http Forward Hostname / IP . grafana Forward Port .......... 3000 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
# Nothing to add: Grafana's docs give no setting beyond NPM's defaults. The project documents nothing more.
# Grafana: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Grafana's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
grafana.example.org {
reverse_proxy grafana:3000
}
# Grafana: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Grafana's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
grafana:
labels:
traefik.enable: "true"
traefik.http.routers.grafana.rule: "Host(`grafana.example.org`)"
traefik.http.routers.grafana.entrypoints: "websecure"
traefik.http.routers.grafana.tls: "true"
traefik.http.routers.grafana.tls.certresolver: "letsencrypt"
traefik.http.routers.grafana.service: "grafana"
traefik.http.services.grafana.loadbalancer.server.port: "3000"
In Grafana itself
Tell Grafana its public name so links and redirects are right; when the proxy terminates TLS, root_url must carry https. [1]
[server] domain = grafana.example.org root_url = https://grafana.example.org/
- Grafana Live holds one WebSocket per open dashboard; the tutorial notes you may have to raise nginx's worker_connections (512 by default). [1]
Forgejo app page
Upstream server:3000 · WebSockets: required [1] · Upload limit: 512M [1] · Timeouts: defaults · docs checked 2026-09-26
# Forgejo: nginx for git.example.org
# App-specific lines are checked against Forgejo's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name git.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name git.example.org;
ssl_certificate /etc/letsencrypt/live/git.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.example.org/privkey.pem;
client_max_body_size 512M; # [1]
merge_slashes off; # [1]
location / {
proxy_pass http://server:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection $http_connection; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Forgejo # Checked against Forgejo's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... git.example.org Scheme ................ http Forward Hostname / IP . server Forward Port .......... 3000 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 512M; # [1] merge_slashes off; # [1]
# Forgejo: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Forgejo's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
git.example.org {
reverse_proxy server:3000
}
# Forgejo: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Forgejo's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
server:
labels:
traefik.enable: "true"
traefik.http.routers.forgejo.rule: "Host(`git.example.org`)"
traefik.http.routers.forgejo.entrypoints: "websecure"
traefik.http.routers.forgejo.tls: "true"
traefik.http.routers.forgejo.tls.certresolver: "letsencrypt"
traefik.http.routers.forgejo.service: "forgejo"
traefik.http.services.forgejo.loadbalancer.server.port: "3000"
In Forgejo itself
Set ROOT_URL (app.ini [server], or FORGEJO__server__ROOT_URL in Docker) to the https address so the links Forgejo generates use HTTPS. [1]
FORGEJO__server__ROOT_URL=https://git.example.org/
- merge_slashes off is marked 'Required to handle URL-encoded slashes' in the docs. [1]
Frigate app page
Upstream frigate:8971 · WebSockets: required [1] · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26
# Frigate: nginx for nvr.example.org
# App-specific lines are checked against Frigate's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name nvr.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name nvr.example.org;
ssl_certificate /etc/letsencrypt/live/nvr.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/nvr.example.org/privkey.pem;
location / {
proxy_pass http://frigate:8971;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection $http_connection; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Frigate # Checked against Frigate's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... nvr.example.org Scheme ................ http Forward Hostname / IP . frigate Forward Port .......... 8971 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
# Nothing to add: Frigate's docs give no setting beyond NPM's defaults. The project documents nothing more.
# Frigate: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Frigate's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
nvr.example.org {
reverse_proxy frigate:8971
}
# Frigate: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Frigate's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
frigate:
labels:
traefik.enable: "true"
traefik.http.routers.frigate.rule: "Host(`nvr.example.org`)"
traefik.http.routers.frigate.entrypoints: "websecure"
traefik.http.routers.frigate.tls: "true"
traefik.http.routers.frigate.tls.certresolver: "letsencrypt"
traefik.http.routers.frigate.service: "frigate"
traefik.http.services.frigate.loadbalancer.server.port: "8971"
# No https server scheme label here: with Frigate's own TLS turned off (see "In Frigate
# itself") Traefik talks plain HTTP to port 8971, and nothing here skips certificate
# checks.
In Frigate itself
Turn Frigate's own TLS off first. Port 8971 serves HTTPS with a self-signed certificate by default, and a proxy that sends plain HTTP to it gets HTTP 400 'The plain HTTP request was sent to HTTPS port'. The docs' other route is to make the proxy accept the self-signed certificate; this page does not print that, because it switches off certificate checking. [2]
tls: enabled: False
- Camera controls, live streams and other live parts of the UI need WebSockets; the docs name NPM's Websockets Support toggle as one that must be switched on. [1]
- The dankhost builder takes the docs' other route for Frigate: it keeps Frigate's TLS on and has the proxy accept the self-signed certificate on the internal Docker network. This page does not print that, because it switches certificate checking off. [2]
Portainer CE app page
Upstream portainer:9000 · WebSockets: not in the docs · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26
# Portainer CE: nginx for portainer.example.org
# App-specific lines are checked against Portainer CE's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name portainer.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name portainer.example.org;
ssl_certificate /etc/letsencrypt/live/portainer.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/portainer.example.org/privkey.pem;
location / {
proxy_pass http://portainer:9000;
proxy_set_header Host $http_host; # [1]
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Portainer CE # Checked against Portainer CE's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... portainer.example.org Scheme ................ http Forward Hostname / IP . portainer Forward Port .......... 9000 Cache Assets .......... off Block Common Exploits . on Websockets Support .... off (the docs ask for no WebSocket headers) SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
# Host: NPM already sends Host $host, the same value as $http_host while NPM listens on 443.
- NPM sends Host $host, which equals $http_host while NPM listens on the standard port 443; on another port Portainer's CSRF check rejects requests with 403 unless you add the public origin with --trusted-origins. [1]
# Portainer CE: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Portainer CE's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
portainer.example.org {
reverse_proxy portainer:9000
}
# Portainer CE: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Portainer CE's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
portainer:
labels:
traefik.enable: "true"
traefik.http.routers.portainer.rule: "Host(`portainer.example.org`)"
traefik.http.routers.portainer.entrypoints: "websecure"
traefik.http.routers.portainer.tls: "true"
traefik.http.routers.portainer.tls.certresolver: "letsencrypt"
traefik.http.routers.portainer.service: "portainer"
traefik.http.services.portainer.loadbalancer.server.port: "9000"
In Portainer CE itself
Optional: pass the proxy's address with --trusted-proxies so the Authentication logs show the real client address. If the public origin cannot be inferred from the forwarded headers, list it with --trusted-origins. [1]
command: --trusted-proxies <proxy IP> --trusted-origins https://portainer.example.org
- Forward X-Forwarded-Proto https only when the browser really uses HTTPS: Portainer then sets Secure cookies, and a browser on plain HTTP drops them (401, instant logout). [1]
- Portainer's reverse-proxy page gives no WebSocket setting, so none is printed here. If the container console will not connect through nginx, that is the part to look at.
AdGuard Home app page
Upstream adguardhome:3000 · WebSockets: not in the docs · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26
# AdGuard Home: nginx for adguard.example.org
# App-specific lines are checked against AdGuard Home's docs (2026-09-26); [n] = source n below.
# AdGuard Home's docs give no nginx setting beyond the generic lines. The project documents nothing more.
server {
listen 80;
listen [::]:80;
server_name adguard.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name adguard.example.org;
ssl_certificate /etc/letsencrypt/live/adguard.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/adguard.example.org/privkey.pem;
location / {
proxy_pass http://adguardhome:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for AdGuard Home # Checked against AdGuard Home's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... adguard.example.org Scheme ................ http Forward Hostname / IP . adguardhome Forward Port .......... 3000 Cache Assets .......... off Block Common Exploits . on Websockets Support .... off (the docs ask for no WebSocket headers) SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
# Nothing to add: AdGuard Home's docs give no setting beyond NPM's defaults. The project documents nothing more.
# AdGuard Home: Caddyfile site block, the same one the dankhost builder writes.
# Checked against AdGuard Home's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
adguard.example.org {
reverse_proxy adguardhome:3000
}
# AdGuard Home: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against AdGuard Home's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
adguardhome:
labels:
traefik.enable: "true"
traefik.http.routers.adguard-home.rule: "Host(`adguard.example.org`)"
traefik.http.routers.adguard-home.entrypoints: "websecure"
traefik.http.routers.adguard-home.tls: "true"
traefik.http.routers.adguard-home.tls.certresolver: "letsencrypt"
traefik.http.routers.adguard-home.service: "adguard-home"
traefik.http.services.adguard-home.loadbalancer.server.port: "3000"
In AdGuard Home itself
In AdGuardHome.yaml: list the proxy under dns.trusted_proxies so AdGuard Home reads the real client address from its headers. To answer DNS-over-HTTPS through the proxy without AdGuard Home's own TLS, set http.doh.insecure_enabled (v0.107.74 and newer; older versions call it allow_unencrypted_doh under tls). [2]
http: doh: insecure_enabled: true dns: trusted_proxies: - <proxy IP>
ntfy app page
Upstream ntfy:80 · WebSockets: required [1] · Upload limit: 0 [1] · Timeouts: 3m [1] · docs checked 2026-09-26
# ntfy: nginx for ntfy.example.org
# App-specific lines are checked against ntfy's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name ntfy.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ntfy.example.org;
ssl_certificate /etc/letsencrypt/live/ntfy.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ntfy.example.org/privkey.pem;
client_max_body_size 0; # [1]
proxy_connect_timeout 3m; # [1]
proxy_read_timeout 3m; # [1]
proxy_send_timeout 3m; # [1]
proxy_buffering off; # [1]
proxy_request_buffering off; # [1]
proxy_redirect off; # [1]
location / {
proxy_pass http://ntfy:80;
proxy_set_header Host $http_host; # [1]
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection "upgrade"; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for ntfy # Checked against ntfy's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... ntfy.example.org Scheme ................ http Forward Hostname / IP . ntfy Forward Port .......... 80 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
client_max_body_size 0; # [1] proxy_connect_timeout 3m; # [1] proxy_read_timeout 3m; # [1] proxy_send_timeout 3m; # [1] proxy_buffering off; # [1] proxy_request_buffering off; # [1] proxy_redirect off; # [1] # Host: NPM already sends Host $host, the same value as $http_host while NPM listens on 443.
# ntfy: Caddyfile site block, the same one the dankhost builder writes.
# Checked against ntfy's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
ntfy.example.org {
reverse_proxy ntfy:80
}
# ntfy: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against ntfy's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
ntfy:
labels:
traefik.enable: "true"
traefik.http.routers.ntfy.rule: "Host(`ntfy.example.org`)"
traefik.http.routers.ntfy.entrypoints: "websecure"
traefik.http.routers.ntfy.tls: "true"
traefik.http.routers.ntfy.tls.certresolver: "letsencrypt"
traefik.http.routers.ntfy.service: "ntfy"
traefik.http.services.ntfy.loadbalancer.server.port: "80"
In ntfy itself
The docs' warning: behind a proxy you must set behind-proxy, otherwise all visitors are rate-limited as if they were one. Set base-url to the public address too. [1]
NTFY_BEHIND_PROXY=true NTFY_BASE_URL=https://ntfy.example.org
- client_max_body_size 0 streams request bodies (attachments) straight to ntfy; the nginx example marks it 'Stream request body to backend'. [1]
Gotify app page
Upstream gotify:80 · WebSockets: required [1] · Upload limit: not given · Timeouts: 1m [1] · docs checked 2026-09-26
# Gotify: nginx for gotify.example.org
# App-specific lines are checked against Gotify's docs (2026-09-26); [n] = source n below.
server {
listen 80;
listen [::]:80;
server_name gotify.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name gotify.example.org;
ssl_certificate /etc/letsencrypt/live/gotify.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gotify.example.org/privkey.pem;
proxy_connect_timeout 1m; # [1]
proxy_read_timeout 1m; # [1]
proxy_send_timeout 1m; # [1]
location / {
proxy_pass http://gotify:80;
proxy_set_header Host $http_host; # [1]
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect http:// $scheme://; # [1]
proxy_http_version 1.1; # [1]
proxy_set_header Upgrade $http_upgrade; # [1]
proxy_set_header Connection "upgrade"; # [1]
}
}
# Nginx Proxy Manager: Hosts > Proxy Hosts > Add Proxy Host, for Gotify # Checked against Gotify's docs (2026-09-26); [n] = source n below. Details tab Domain Names .......... gotify.example.org Scheme ................ http Forward Hostname / IP . gotify Forward Port .......... 80 Cache Assets .......... off Block Common Exploits . on Websockets Support .... on # [1] SSL tab SSL Certificate ....... Request a new SSL Certificate Force SSL ............. on HTTP/2 Support ........ on
proxy_connect_timeout 1m; # [1] proxy_read_timeout 1m; # [1] proxy_send_timeout 1m; # [1] proxy_redirect http:// $scheme://; # [1] # Host: NPM already sends Host $host, the same value as $http_host while NPM listens on 443.
# Gotify: Caddyfile site block, the same one the dankhost builder writes.
# Checked against Gotify's docs (2026-09-26); [n] = source n below.
# Caddy passes WebSockets and sets X-Forwarded-For/-Proto/-Host by itself.
gotify.example.org {
reverse_proxy gotify:80
}
# Gotify: Traefik v3 labels, the same ones the dankhost builder writes.
# Checked against Gotify's docs (2026-09-26); [n] = source n below.
# "websecure" and "letsencrypt" are the builder's entrypoint and resolver names: use yours.
# Traefik must share a Docker network with the container; it passes WebSockets by itself.
services:
gotify:
labels:
traefik.enable: "true"
traefik.http.routers.gotify.rule: "Host(`gotify.example.org`)"
traefik.http.routers.gotify.entrypoints: "websecure"
traefik.http.routers.gotify.tls: "true"
traefik.http.routers.gotify.tls.certresolver: "letsencrypt"
traefik.http.routers.gotify.service: "gotify"
traefik.http.services.gotify.loadbalancer.server.port: "80"
In Gotify itself
Trust the proxy for the client address, and mark session cookies Secure once Gotify is reached over HTTPS. [2]
GOTIFY_SERVER_TRUSTEDPROXIES=<proxy IP> GOTIFY_SERVER_SECURECOOKIE=true
- The docs: 'The proxy must preserve the host because Gotify verifies the host with the origin for WebSocket connections', hence Host $http_host. [1]
- Gotify docs: nginx (WebSocket headers, Host $http_host, 1m timeouts) checked 2026-09-26
- Gotify docs: Configuration (GOTIFY_SERVER_TRUSTEDPROXIES, GOTIFY_SERVER_SECURECOOKIE) checked 2026-09-26
The proxies' own documentation
The generic lines in every block (the listen, certificate and header lines of the nginx blocks, the Nginx Proxy Manager field names, Caddy's and Traefik's defaults) come from these.
nginx
- nginx docs: WebSocket proxying (Upgrade and Connection are hop-by-hop headers, so they have to be passed explicitly; HTTP/1.1 to the upstream) checked 2026-09-26
- nginx docs: ngx_http_core_module, client_max_body_size (default 1m; 0 disables the check) checked 2026-09-26
- nginx docs: ngx_http_proxy_module, proxy_read_timeout (default 60s) checked 2026-09-26
- nginx docs: ngx_http_proxy_module, proxy_http_version (1.1 by default since nginx 1.29.7, 1.0 before) checked 2026-09-26
Nginx Proxy Manager
- Nginx Proxy Manager source: proxy_host.conf template (Websockets Support adds Upgrade/Connection and HTTP/1.1; the Advanced text is placed inside the server block; a 'location /' in it replaces NPM's own) checked 2026-09-26
- Nginx Proxy Manager source: nginx.conf (client_max_body_size 2000m, proxy_read_timeout and proxy_send_timeout 90s for every host) checked 2026-09-26
- Nginx Proxy Manager source: include/proxy.conf (sends Host $host, X-Forwarded-Proto, X-Forwarded-For and X-Real-IP) checked 2026-09-26
- Nginx Proxy Manager source: en.json (the field and toggle names: Domain Names, Scheme, Forward Hostname / IP, Forward Port, Cache Assets, Block Common Exploits, Websockets Support, Force SSL, HTTP/2 Support, Custom Nginx Configuration) checked 2026-09-26
Caddy
- Caddy docs: reverse_proxy (supports WebSocket connections; sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host) checked 2026-09-26
- Caddy docs: request_body (max_size is an opt-in limit: bigger bodies get HTTP 413) checked 2026-09-26
Traefik
- Traefik docs: EntryPoints (http.redirections, http3, transport.respondingTimeouts.readTimeout default 60s) checked 2026-09-24
- Traefik docs: Docker routing labels (traefik.enable, routers, services, middlewares, traefik.docker.network) checked 2026-09-24
- Traefik docs: Exposing services, WebSocket supported out of the box checked 2026-09-24