Skip to content
BurnerByte

self-hosting

Reverse Proxy

Nginx and Caddy configuration for BurnerByte.

Nginx Proxy Manager

The fields, for both hosts:

FieldAPI hostFrontend host
Domain Namesapi.example.commail.example.com
Schemehttphttp
Forward Hostname / IPthe container or host addresssame
Forward Port80803000
Websockets SupportOnOn
Block Common ExploitsOnOn
Cache AssetsOffOff — Next.js sets its own cache headers
Careful

Then SSLRequest a new SSL Certificate, with Force SSL and HTTP/2 Support enabled. Use a DNS challenge if port 80 is not reachable.

Finally, paste the directives from Header Hardening into Advanced → Custom Nginx Configuration on the API host, along with:

nginx
proxy_read_timeout 86400;proxy_send_timeout 86400;

NPM's generated config appends to whatever the client sent and never clears True-Client-IP, so the hardening is not optional here.

Nginx

nginx
server {    listen 443 ssl;    server_name api.example.com;     ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;     location / {        proxy_pass http://127.0.0.1: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;    }     # WebSocket support    location /api/v1/ws/ {        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-Real-IP $remote_addr;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto $scheme;        proxy_read_timeout 86400;        proxy_send_timeout 86400;    }} server {    listen 443 ssl;    server_name app.example.com;     ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;     location / {        proxy_pass http://127.0.0.1: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;    }}

Caddy

Caddyfile
api.example.com {    reverse_proxy localhost:8080} app.example.com {    reverse_proxy localhost:3000}

Caddy automatically handles TLS certificates and WebSocket upgrades.

Traefik

As labels on the services in your Compose file:

docker-compose.override.yml
services:  api:    labels:      - "traefik.enable=true"      - "traefik.http.routers.bb-api.rule=Host(`api.example.com`)"      - "traefik.http.routers.bb-api.entrypoints=websecure"      - "traefik.http.routers.bb-api.tls.certresolver=le"      - "traefik.http.services.bb-api.loadbalancer.server.port=8080"      - "traefik.http.routers.bb-api.middlewares=bb-realip@docker"      - "traefik.http.middlewares.bb-realip.headers.customrequestheaders.True-Client-IP="   frontend:    labels:      - "traefik.enable=true"      - "traefik.http.routers.bb-web.rule=Host(`mail.example.com`)"      - "traefik.http.routers.bb-web.entrypoints=websecure"      - "traefik.http.routers.bb-web.tls.certresolver=le"      - "traefik.http.services.bb-web.loadbalancer.server.port=3000"

Traefik forwards WebSocket upgrades without configuration, and sets X-Real-IP and X-Forwarded-For from the connection itself — so the only header needing explicit treatment is True-Client-IP, cleared above. Set --entrypoints.websecure.forwardedHeaders.trustedIPs if Traefik itself sits behind another proxy.

SMTP

The SMTP server (port 2525) should not be behind a reverse proxy. Use iptables to forward port 25:

bash
sudo iptables -t nat -A PREROUTING -p tcp --dport 25 -j REDIRECT --to-port 2525

Header Hardening

Careful

Add to every nginx location block:

nginx
    proxy_set_header True-Client-IP "";    proxy_set_header X-Real-IP $remote_addr;    proxy_set_header X-Forwarded-For $remote_addr;

Caddy's bare reverse_proxy sets none of these, so it needs the equivalent:

text
api.example.com {    reverse_proxy localhost:8080 {        header_up True-Client-IP ""        header_up X-Real-IP {remote_host}        header_up X-Forwarded-For {remote_host}    }}

Rebuild the Frontend for TLS

NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WS_URL and NEXT_PUBLIC_SITE_URL are inlined into the client bundle at build time — they are Docker build args, not runtime environment. Terminating TLS at the proxy is therefore not enough: an image built with ws:// keeps dialling ws:// from an https:// page, and the browser blocks it as mixed content. Set the public URLs and rebuild:

bash
API_BASE_URL=https://api.example.comWS_BASE_URL=wss://api.example.comFRONTEND_URL=https://app.example.com docker compose build frontend && docker compose up -d frontend