dankhostself-host anything

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.

The defaults that cause most of it

Names used in every block: <app>.example.org is your hostname, the upstream is the service name and port from the project's own compose file (replace it with the container name or LAN address your proxy can reach), and <proxy IP> is the address the app sees the proxy connect from.

Immich app page

Upstream immich-server:2283 · WebSockets: required [1] · Upload limit: 50000M [1] · Timeouts: 600s [1] · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

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]

Caddyfile

# 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
}

Traefik labels

# 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

  1. Immich docs: Reverse Proxy (nginx, Caddy and Traefik examples) checked 2026-09-26
  2. 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

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

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]

Caddyfile

# 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
}

Traefik labels

# 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

  1. Nextcloud admin manual: Reverse proxy (trusted_proxies, overwrite parameters, CalDAV/CardDAV redirects, the NPM proxy_hide_header note) checked 2026-09-26
  2. 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
  3. Nextcloud admin manual: NGINX configuration (client_max_body_size 512M) checked 2026-09-26
  4. 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

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

proxy_buffering off; # [2]

Caddyfile

# 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
}

Traefik labels

# 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

  1. Home Assistant: HTTP integration (Reverse proxies; settings moved to Settings > System > Network in 2026.8; migrating from YAML) checked 2026-09-26
  2. Home Assistant's official NGINX add-on: nginx.conf.gtpl (Upgrade/Connection via map, proxy_buffering off) checked 2026-09-26

Vaultwarden app page

Upstream vaultwarden:80 · WebSockets: required [1] · Upload limit: 525M [1] · Timeouts: defaults · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

client_max_body_size 525M; # [1]

Caddyfile

# 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}
	}
}

Traefik labels

# 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.

  1. Vaultwarden wiki: Proxy examples (nginx with websocket map and client_max_body_size 525M; Caddy 2.x header_up X-Real-IP) checked 2026-09-26

Jellyfin app page

Upstream jellyfin:8096 · WebSockets: required on /socket [1] · Upload limit: 20M [1] · Timeouts: defaults · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

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]
}

Caddyfile

# 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
}

Traefik labels

# 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

  1. Jellyfin docs: Reverse Proxy > Nginx (client_max_body_size 20M, proxy_buffering off, the /socket location) checked 2026-09-26
  2. Jellyfin docs: Reverse Proxy (Known Proxies, Websockets) checked 2026-09-26

Paperless-ngx app page

Upstream webserver:8000 · WebSockets: required [1] · Upload limit: 10M [1] · Timeouts: defaults · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

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]

Caddyfile

# 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
}

Traefik labels

# 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

  1. Paperless-ngx wiki: Using a Reverse Proxy with Paperless-ngx (nginx, Nginx Proxy Manager) checked 2026-09-26
  2. 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

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

# Nothing to add: Uptime Kuma's docs give no setting beyond NPM's defaults. The project documents nothing more.

Caddyfile

# 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
}

Traefik labels

# 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

  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

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

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]

Caddyfile

# 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
}

Traefik labels

# 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.

  1. Audiobookshelf README: Reverse Proxy Set Up ('Audiobookshelf requires a websocket connection') checked 2026-09-26
  2. Audiobookshelf docs: Reverse proxy > NGINX (client_max_body_size 10240M, Host $http_host, proxy_redirect) checked 2026-09-26
  3. 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

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

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]
}

Caddyfile

# 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
	}
}

Traefik labels

# 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

  1. PhotoPrism docs: Using NGINX as Reverse Proxy (client_max_body_size 512M, proxy_buffering off, 600s timeouts, websocket map, PHOTOPRISM_TRUSTED_PROXY) checked 2026-09-26
  2. PhotoPrism docs: Caddy 2 (encode zstd gzip, flush_interval -1) checked 2026-09-26

Syncthing app page

Upstream syncthing:8384 · WebSockets: not in the docs · Upload limit: not given · Timeouts: 600s [1] · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

proxy_read_timeout 600s; # [1]
proxy_send_timeout 600s; # [1]

Caddyfile

# 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}
	}
}

Traefik labels

# 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.

  1. Syncthing docs: Reverse Proxy Setup (nginx with 600s timeouts; Caddy v2 header_up Host {upstream_hostport}) checked 2026-09-26

n8n app page

Upstream n8n:5678 · WebSockets: required [2] · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

# 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]
}

Caddyfile

# 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
}

Traefik labels

# 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

  1. n8n docs: Configure webhook URLs with reverse proxy (N8N_WEBHOOK_URL, N8N_PROXY_HOPS=1, X-Forwarded-For/-Host/-Proto) checked 2026-09-26
  2. n8n docs: Deployment environment variables (N8N_PUSH_BACKEND defaults to websocket) checked 2026-09-26

Open WebUI app page

Upstream open-webui:8080 · WebSockets: required [1] · Upload limit: 20M [1] · Timeouts: 10m [1] · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

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]

Caddyfile

# 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
}

Traefik labels

# 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

  1. Open WebUI docs: HTTPS with Nginx (WebSocket headers, proxy_buffering off for streaming, timeouts, client_max_body_size 20M, CORS_ALLOW_ORIGIN, Nginx Proxy Manager) checked 2026-09-26

Grafana app page

Upstream grafana:3000 · WebSockets: required on /api/live/ [1] · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

# Nothing to add: Grafana's docs give no setting beyond NPM's defaults. The project documents nothing more.

Caddyfile

# 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
}

Traefik labels

# 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

  1. Grafana tutorial: Run Grafana behind a reverse proxy (domain, root_url, the /api/live/ WebSocket location) checked 2026-09-26

Forgejo app page

Upstream server:3000 · WebSockets: required [1] · Upload limit: 512M [1] · Timeouts: defaults · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

client_max_body_size 512M; # [1]
merge_slashes off; # [1]

Caddyfile

# 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
}

Traefik labels

# 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

  1. Forgejo docs: Reverse proxy (nginx: merge_slashes off, client_max_body_size 512M, Upgrade/Connection; ROOT_URL) checked 2026-09-26

Frigate app page

Upstream frigate:8971 · WebSockets: required [1] · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

# Nothing to add: Frigate's docs give no setting beyond NPM's defaults. The project documents nothing more.

Caddyfile

# 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
}

Traefik labels

# 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

  1. Frigate docs: Setting up a reverse proxy (disable Frigate's TLS first; WebSocket support; NPM's Websockets Support toggle) checked 2026-09-26
  2. Frigate docs: TLS (self-signed certificate on 8971 by default; tls: enabled: False, or Settings > System > TLS) checked 2026-09-26

Portainer CE app page

Upstream portainer:9000 · WebSockets: not in the docs · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

# 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]

Caddyfile

# 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
}

Traefik labels

# 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

  1. Portainer docs: Using Portainer with reverse proxies (X-Forwarded-Proto and Secure cookies, Host $http_host for the CSRF check, --trusted-origins, --trusted-proxies) checked 2026-09-26

AdGuard Home app page

Upstream adguardhome:3000 · WebSockets: not in the docs · Upload limit: not given · Timeouts: defaults · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

# Nothing to add: AdGuard Home's docs give no setting beyond NPM's defaults. The project documents nothing more.

Caddyfile

# 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
}

Traefik labels

# 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

  1. AdGuard Home FAQ: How do I configure a reverse proxy server for AdGuard Home? (nginx, Caddy, DoH without TLS, trusted_proxies) checked 2026-09-26
  2. AdGuard Home wiki: Configuration (dns.trusted_proxies; http.doh.insecure_enabled since v0.107.74) checked 2026-09-26

ntfy app page

Upstream ntfy:80 · WebSockets: required [1] · Upload limit: 0 [1] · Timeouts: 3m [1] · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

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.

Caddyfile

# 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
}

Traefik labels

# 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

  1. ntfy docs: Configuring the ntfy server, 'Behind a proxy (TLS, etc.)' and the nginx example checked 2026-09-26

Gotify app page

Upstream gotify:80 · WebSockets: required [1] · Upload limit: not given · Timeouts: 1m [1] · docs checked 2026-09-26

nginx

# 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

# 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

Advanced tab: Custom Nginx Configuration

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.

Caddyfile

# 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
}

Traefik labels

# 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

  1. Gotify docs: nginx (WebSocket headers, Host $http_host, 1m timeouts) checked 2026-09-26
  2. 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 Proxy Manager

Caddy

Traefik

The Caddy and Traefik blocks are generated from the same data as the builder, so a file built there and a block copied here say the same thing. The one exception is marked in its section: nothing on this page turns off certificate checking toward the app.