Skip to content

Set up a reverse proxy

A reverse proxy sits between clients and Quackback, handling SSL/TLS termination, compression, and security headers. Always use one in production.

Never expose port 3000 directly to the internet. Use a reverse proxy for HTTPS.

Required Headers

Your reverse proxy must pass these headers to Quackback:

HeaderPurpose
HostOriginal hostname
X-Real-IPClient IP address
X-Forwarded-ForProxy chain
X-Forwarded-ProtoProtocol (http/https)
UpgradeWebSocket support
ConnectionKeep-alive handling

Set the trusted proxy hops

Quackback rate-limits requests per client IP. Behind a proxy, every request arrives from the proxy's address, so the app needs to know how many proxies to look past in X-Forwarded-For. Set TRUSTED_PROXY_HOPS to the number of proxies in front of it:

SetupTRUSTED_PROXY_HOPS
Clients connect to the app directly0 (default)
One reverse proxy (Nginx, Caddy, Traefik, Cloudflare Tunnel)1
A CDN in front of a reverse proxy2

If you leave it at 0 behind a proxy, every visitor shares the proxy's IP and one rate-limit bucket, and the app logs a warning.

Or name a client-IP header

If your proxy sets one authoritative client-IP header, you can point Quackback at that header instead of counting hops. This is the better choice when requests pass through several proxies whose number you don't control. TRUSTED_CLIENT_IP_HEADER takes precedence over TRUSTED_PROXY_HOPS.

ProxyProxy settingQuackback setting
Nginxproxy_set_header X-Real-IP $remote_addr; (in the config below)TRUSTED_CLIENT_IP_HEADER=x-real-ip
CloudflareSets CF-Connecting-IP on every requestTRUSTED_CLIENT_IP_HEADER=cf-connecting-ip

The header is used only when it holds exactly one valid IP address. Otherwise the app falls back to TRUSTED_PROXY_HOPS and logs a warning. X-Forwarded-For can't be used here; count hops with TRUSTED_PROXY_HOPS for it.

Only name a header your proxy always sets or overwrites. If the proxy passes a client's own copy of the header through, or clients can reach the app port directly, anyone can choose the IP the app sees.

Never set TRUSTED_PROXY_HOPS above 0 if clients can also reach the app port directly. Anyone could then send their own X-Forwarded-For header and pick the IP the app sees. Bind the app to 127.0.0.1 or a private network when a proxy sits in front of it.

Caddy

Caddy automatically provisions SSL certificates from Let's Encrypt. It is the simplest option for most deployments.

Create /etc/caddy/Caddyfile:

feedback.yourcompany.com {
    reverse_proxy localhost:3000
    encode gzip zstd
}

Start Caddy:

sudo systemctl enable caddy
sudo systemctl start caddy

That's it. Caddy handles HTTPS, certificate renewal, and compression automatically.

Nginx

Create /etc/nginx/sites-available/quackback:

server {
    listen 80;
    server_name feedback.yourcompany.com;
    return 301 https://$server_name$request_uri;
}
 
server {
    listen 443 ssl http2;
    server_name feedback.yourcompany.com;
 
    ssl_certificate /etc/letsencrypt/live/feedback.yourcompany.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/feedback.yourcompany.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
 
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
 
    gzip on;
    gzip_types text/plain text/css application/json application/javascript image/svg+xml;
 
    # Feedback videos can be up to 100 MB. Leave room for multipart overhead.
    client_max_body_size 110m;
 
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.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 Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Quackback accepts MP4 and WebM videos up to 100 MB and images up to 5 MB. If another proxy, load balancer, or platform sits in front of Quackback, raise its request body limit above 100 MB too.

Enable the site and obtain a certificate:

sudo ln -s /etc/nginx/sites-available/quackback /etc/nginx/sites-enabled/
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d feedback.yourcompany.com
sudo nginx -t && sudo systemctl reload nginx

Traefik

Add labels to your Quackback service in docker-compose.yml:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.quackback.rule=Host(`feedback.yourcompany.com`)"
  - "traefik.http.routers.quackback.tls.certresolver=letsencrypt"
  - "traefik.http.services.quackback.loadbalancer.server.port=3000"

See the Traefik documentation for full configuration including the certificate resolver setup.

Apache

See the Apache reverse proxy documentation for setup with mod_proxy. You need mod_proxy, mod_proxy_http, mod_proxy_wstunnel, mod_ssl, and mod_headers enabled.

Troubleshooting

502 Bad Gateway

  • Verify Quackback is running: curl http://localhost:3000
  • Check that the proxy_pass address and port match your Quackback instance

WebSocket Connection Failed

Make sure your proxy passes WebSocket upgrade headers:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Live chat updates arrive late or not at all

Live chat streams over a long-lived HTTP connection (server-sent events). If your proxy buffers responses, turn buffering off for Quackback (proxy_buffering off; in Nginx). If you can't, set CHAT_TRANSPORT_MODE=poll to switch the widget and portal to polling.

Every visitor hits the rate limit at once

TRUSTED_PROXY_HOPS is 0 behind a proxy, so all visitors share one IP. Set it as described in Set the trusted proxy hops.

Mixed Content Warnings

Ensure the X-Forwarded-Proto: https header is passed and BASE_URL in your .env uses https://.

Redirect Loops

Check that BASE_URL in .env uses https://.

Next Steps