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:
| Header | Purpose |
|---|---|
Host | Original hostname |
X-Real-IP | Client IP address |
X-Forwarded-For | Proxy chain |
X-Forwarded-Proto | Protocol (http/https) |
Upgrade | WebSocket support |
Connection | Keep-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:
| Setup | TRUSTED_PROXY_HOPS |
|---|---|
| Clients connect to the app directly | 0 (default) |
| One reverse proxy (Nginx, Caddy, Traefik, Cloudflare Tunnel) | 1 |
| A CDN in front of a reverse proxy | 2 |
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.
| Proxy | Proxy setting | Quackback setting |
|---|---|---|
| Nginx | proxy_set_header X-Real-IP $remote_addr; (in the config below) | TRUSTED_CLIENT_IP_HEADER=x-real-ip |
| Cloudflare | Sets CF-Connecting-IP on every request | TRUSTED_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 caddyThat'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 nginxTraefik
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_passaddress 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
- Environment Variables - Configure email, OAuth, and integrations
- Docker Deployment - Container-based deployment
- Troubleshooting - Common issues and solutions