Set up a reverse proxy

Serve self-hosted Quackback over HTTPS with Caddy, Nginx, or Traefik, and set TRUSTED_PROXY_HOPS or TRUSTED_CLIENT_IP_HEADER so rate limits see each visitor's real IP.

JM
James Morton
Written By James MortonLast updated 1 day ago

Put a reverse proxy (Caddy, Nginx, or Traefik) in front of a self-hosted Quackback instance to serve it over HTTPS, and tell Quackback how to find each visitor's real IP address. Always use one in production.

Warning: 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 or 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 with TRUSTED_CLIENT_IP_HEADER. 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.

Warning: 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.

Warning: 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 provisions TLS certificates from Let's Encrypt automatically. 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

Caddy handles HTTPS, certificate renewal, and compression. Set TRUSTED_PROXY_HOPS=1 in Quackback's environment.

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";
    }
}

This config sets X-Real-IP to the address Nginx saw and overwrites any copy the client sent, so you can set TRUSTED_CLIENT_IP_HEADER=x-real-ip (or TRUSTED_PROXY_HOPS=1).

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 your Compose file:

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. Set TRUSTED_PROXY_HOPS=1 in Quackback's environment.

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/api/health/live
  • 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, or set TRUSTED_CLIENT_IP_HEADER.

Uploads fail with "request entity too large"

Raise the proxy's request body limit above 100 MB (client_max_body_size 110m; in Nginx).

Mixed content warnings

Make sure 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://.

Was this helpful?

Your feedback shapes what we write next.