Reverse proxy & TLS
The Desk’s containers speak plain HTTP — TLS/HTTPS is your responsibility. Put a reverse proxy in front of the stack to terminate TLS and route traffic to the two published ports:
- The web app on
APP_PORT(default8000). - Reverb (WebSockets) on
REVERB_PORT(default8080).
Both publish to loopback by default (APP_BIND=127.0.0.1) since they speak
plain HTTP — a host-based proxy connects to 127.0.0.1:8000 / 127.0.0.1:8080.
If your proxy runs inside the compose network instead (e.g. a Caddy container
on the same network), target the service names app:8080 / reverb:8080 and you
don’t need any host publishing at all.
HTTPS URLs: the app trusts your proxy’s
X-Forwarded-*headers out of the box, so it generateshttps://links from aX-Forwarded-Proto: httpsrequest. You only need your proxy to forward those headers — Caddy and Traefik do automatically; the nginx example below setsX-Forwarded-Proto. Without them the app would emithttp://links on anhttps://page and the browser blocks them as mixed content (login/registration break).
The single hard requirement beyond normal HTTPS termination is that your proxy forwards WebSocket upgrade requests to Reverb. Without it, the app loads but real-time updates (new messages, typing indicators, presence) never arrive.
Voice messages need HTTPS. Browsers only grant microphone access in a secure context, so the composer’s mic button is hidden entirely on a plain-HTTP origin (
localhostexcepted). There is no setting to turn it on — terminate TLS and it appears. Playing and uploading audio files works either way.
Make sure the browser-facing Reverb settings match your proxy — see Configuration.
Caddy terminates TLS automatically and proxies WebSockets with no extra config:
chat.example.com { reverse_proxy localhost:8000}
# Reverb on its own subdomain (matches REVERB_HOST_PUBLIC=ws.example.com):ws.example.com { reverse_proxy localhost:8080}If you instead route Reverb under a path on the same host, proxy that path to
port 8080; Caddy handles the upgrade headers for you.
Caddy inside the compose network (single domain)
Section titled “Caddy inside the compose network (single domain)”If you’d rather run Caddy as a container on the app’s Docker network, reference the
services by name and split the Reverb WebSocket path (/app/*) off to reverb.
The app then needs no host-published ports:
chat.example.com { # Reverb WebSocket (browser connects to wss://…/app/{key}). @reverb path /app/* /apps/* reverse_proxy @reverb reverb:8080
reverse_proxy app:8080}nginx needs the Upgrade/Connection headers set explicitly for the WebSocket
location:
# Appserver { listen 443 ssl; server_name chat.example.com; # ssl_certificate ... ; ssl_certificate_key ... ;
# Set to your ATTACHMENT_MAX_SIZE_MB (default 25M) plus a little headroom for # multipart overhead, or large uploads are rejected by nginx before the app # sees them. client_max_body_size 30m;
location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}
# Reverb (WebSockets)server { listen 443 ssl; server_name ws.example.com; # ssl_certificate ... ; ssl_certificate_key ... ;
location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}Traefik
Section titled “Traefik”If you front the stack with Traefik, add router/service labels (or a dynamic
config) that route chat.example.com to the app service and your WebSocket
host to the reverb service. Traefik forwards WebSocket upgrades automatically
once the service is reachable.
Upload body size
Section titled “Upload body size”Message attachments are capped by ATTACHMENT_MAX_SIZE_MB
(default 25 MB), but that limit only applies once the request reaches the app. Your proxy — and
PHP itself — reject an oversized body first, so raise their limits to your configured cap plus a
little headroom for multipart encoding overhead:
- nginx:
client_max_body_size(shown above) — set it to your cap plus headroom. - Caddy / Traefik: no request-body cap by default, so nothing to change unless you added one.
- PHP:
upload_max_filesizeandpost_max_sizemust both comfortably exceed the cap.
Once TLS is terminated, the app sends
Strict-Transport-Security: max-age=31536000; includeSubDomains on every
response that arrived over HTTPS, so a browser that has seen your instance once
refuses to speak plain HTTP to it again. Nothing to configure — it rides on the
same X-Forwarded-Proto: https your proxy already sets, and is withheld
entirely on plain-HTTP requests so a http:// deployment cannot lock itself out
of its own hostname.
If your proxy already sends the header, set HSTS_ENABLED=false so the two do
not disagree about the max-age. The knobs (HSTS_MAX_AGE,
HSTS_INCLUDE_SUBDOMAINS, and the deliberately opt-in HSTS_PRELOAD) are in
Feature toggles → HTTPS enforcement (HSTS).
Session cookies pick up the matching Secure flag automatically when APP_URL
is an https:// URL — see
SESSION_SECURE_COOKIE.
Verifying
Section titled “Verifying”After wiring up the proxy:
- Load
APP_URLover HTTPS — the app should render. - Open the browser dev tools Network → WS tab; you should see an open
WebSocket connection to your Reverb host. If it fails to connect or falls
back repeatedly, re-check the upgrade headers and the
REVERB_*_PUBLICvalues. A persistent “Reconnecting…” banner has its own walkthrough in Troubleshooting.