Reverse proxy
Configure an HTTPS reverse proxy (Caddy, Nginx, Traefik) in front of Postdot with Let's Encrypt, SSE streaming, and media upload support.
Postdot runs its API, Studio dashboard, scheduler, and background worker in a single
container or process listening on loopback (127.0.0.1:3000 by default). In production,
place an HTTPS reverse proxy on the host or ingress network in front of Postdot to
terminate TLS, provide automatic certificates, and forward client requests.
For initial single-node container deployment, follow the Docker Compose installation guide. For OAuth callback domains and external reachability constraints, see provider configuration.
Proxy requirements
A reverse proxy in front of Postdot must satisfy four operational requirements:
- Origin alignment: Configure both
BETTER_AUTH_URLandPOSTDOT_WEB_ORIGINin your.envfile to match the public canonical HTTPS origin (for example,https://postdot.example.com). Better Auth and session cookie validation rely on exact origin matching. - Forwarded headers: Forward
Host,X-Forwarded-Proto,X-Forwarded-For, andX-Forwarded-Host. These ensure cookies receive theSecureflag and OAuth redirects target the correct public domain. - Response streaming (Server-Sent Events): Postdot uses SSE streams for real-time
project events and progress notifications (
GET /api/projects/:projectId/events). Proxy response buffering must be disabled so events reach the browser immediately. - Media upload request size: Postdot accepts direct media uploads (images, videos,
and avatars). Default proxy request body limits (such as Nginx's 1 MB limit) will
fail uploads with
413 Request Entity Too Large. Set the body limit to at least 100 MB.
Caddy
Caddy handles automatic TLS certificate issuance and renewal via Let's Encrypt or ZeroSSL with zero manual certificate renewal cron jobs. Caddy also streams SSE responses without buffering by default.
Caddyfile
Add this block to your /etc/caddy/Caddyfile:
postdot.example.com {
encode zstd gzip
# Set max upload size to match application needs (e.g. 100 MB)
request_body {
max_size 100MB
}
reverse_proxy 127.0.0.1:3000 {
# Forward public host and proxy headers
header_up Host {host}
header_up X-Forwarded-Host {host}
header_up X-Forwarded-Proto https
# Buffering is off by default in Caddy, preserving SSE real-time events
flush_interval -1
}
}Apply the configuration:
sudo caddy reload --config /etc/caddy/CaddyfileNginx
Nginx requires explicit configuration for forwarded headers, request body size, and unbuffered streaming for Server-Sent Events.
Virtual host configuration
Save the following server configuration to /etc/nginx/sites-available/postdot
(and symlink it to /etc/nginx/sites-enabled/):
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name postdot.example.com;
# Redirect all HTTP traffic to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name postdot.example.com;
ssl_certificate /etc/letsencrypt/live/postdot.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/postdot.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Allow large media file uploads (images, video)
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# Forward client identity and scheme
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;
# WebSocket / Connection upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Disable buffering for Server-Sent Events (SSE)
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h;
proxy_send_timeout 24h;
}
}Test and reload Nginx:
sudo nginx -t
sudo systemctl reload nginxTraefik
When running Traefik as an edge router in Docker, configure Postdot
using Docker labels in your compose.yaml file and connect Postdot to Traefik's
external network.
Docker Compose labels
services:
postdot:
image: ghcr.io/postdot/postdot:0.1.0-alpha.6
restart: unless-stopped
environment:
- BETTER_AUTH_URL=https://postdot.example.com
- POSTDOT_WEB_ORIGIN=https://postdot.example.com
networks:
- traefik-public
- internal
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.postdot.rule=Host(`postdot.example.com`)'
- 'traefik.http.routers.postdot.entrypoints=websecure'
- 'traefik.http.routers.postdot.tls.certresolver=letsencrypt'
- 'traefik.http.services.postdot.loadbalancer.server.port=3000'
- 'traefik.http.services.postdot.loadbalancer.passHostHeader=true'
networks:
traefik-public:
external: true
internal:
internal: trueTraefik passes the Host and X-Forwarded-* headers automatically and does
not buffer HTTP chunked or SSE streams unless explicitly configured with a
buffering middleware.
Environment configuration
Update your .env file to instruct Postdot to bind to loopback and advertise its public
canonical URL:
# Bind to loopback interface on host
POSTDOT_HTTP_PORT=127.0.0.1:3000
# Canonical public HTTPS origin (no trailing slash)
BETTER_AUTH_URL=https://postdot.example.com
POSTDOT_WEB_ORIGIN=https://postdot.example.comRestart the Postdot service after editing .env:
docker compose up -d --waitNever leave BETTER_AUTH_URL or POSTDOT_WEB_ORIGIN set to http://127.0.0.1:3000 in
production. If the configured origin does not match the browser's URL, authentication cookies
will fail to set or validate.
Verification checklist
After starting your reverse proxy and Postdot, run these checks to qualify the deployment:
Check HTTP to HTTPS redirection
Verify that unencrypted HTTP requests redirect to HTTPS:
curl -I http://postdot.example.comThe response should return HTTP 301 or 308 with a Location: https://postdot.example.com/ header.
Verify health and readiness endpoints
Query Postdot's health probes through your public domain:
curl --fail https://postdot.example.com/health
curl --fail https://postdot.example.com/readyBoth should return HTTP 200 OK with JSON bodies ({"status":"ok",...}).
Verify Server-Sent Events (SSE) streaming
Test that proxy buffering does not block event stream flushing:
curl -N -H "Accept: text/event-stream" https://postdot.example.com/readyThe connection should establish immediately without hanging on proxy buffers.
Test media upload capacity
Upload an image or sample file larger than 1 MB through Postdot Studio. If your reverse
proxy limits body sizes, you will observe HTTP 413 Request Entity Too Large in the
browser network console. Adjust client_max_body_size (Nginx) or max_size (Caddy)
if needed.