Skip to content

Reverse proxy setup

You can run TravStats over plain HTTP on your LAN forever. But if you want HTTPS or remote access, you’ll need a reverse proxy in front. This page has working configs for the five most common choices.

Two things have to land at the backend:

  1. Host header — what hostname the user typed. Used in emails (the “click here” links).
  2. X-Forwarded-Protohttp or https. TravStats reads this to decide whether to set the Secure flag on the JWT cookie. If the cookie is set Secure and the browser arrives over HTTP, the cookie is dropped → instant logout.

All five proxies below forward both correctly with the configs shown. If you write your own from scratch, those two headers are the things to verify.

Section titled “Cloudflare Tunnel (recommended for public domains)”

The smoothest option for “I have a domain, I want HTTPS, and I don’t want any ports exposed”.

You install cloudflared somewhere on your network. It opens an outbound connection to Cloudflare and tunnels traffic back to your TravStats container. Cloudflare handles TLS termination, DDoS protection, and a free public hostname.

No ports forwarded. No router config. Works from anywhere on the internet — without exposing your home IP.

  1. Sign in at dash.cloudflare.com, add a domain you control, set up the DNS.

  2. Zero Trust → Networks → Tunnels → Create a tunnel. Name it travstats. Cloudflare gives you a cloudflared install command and a token.

  3. Run cloudflared somewhere it can reach the TravStats container. Easiest: as a sidecar service in your docker-compose:

    # docker-compose.prod.yml — add to existing services:
    cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: travstats-cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
    TUNNEL_TOKEN: ${CLOUDFLARED_TUNNEL_TOKEN}
    networks:
    - travstats-network
    depends_on:
    - app

    And CLOUDFLARED_TUNNEL_TOKEN=… in .env next to DB_PASSWORD.

  4. Back in the Cloudflare dashboard, set up a Public Hostname:

    • Subdomain: travstats
    • Domain: your domain
    • Service: http://travstats-app:80 (the container hostname inside the docker network)
  5. Done. https://travstats.example.com works from anywhere, with valid TLS.

Cloudflare Tunnel forwards X-Forwarded-Proto: https automatically. TravStats sets Secure cookies; nothing extra to configure.

The simplest non-cloud option for an open homelab.

If you don’t already run Caddy, the caddyserver/caddy image works as a sidecar:

docker-compose.prod.yml
caddy:
image: caddy:latest
container_name: travstats-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- travstats-caddy-data:/data
- travstats-caddy-config:/config
networks:
- travstats-network
volumes:
travstats-caddy-data:
travstats-caddy-config:

A two-line site block does all the work:

travstats.example.com {
reverse_proxy travstats-app:80
}

Caddy fetches a Let’s Encrypt cert automatically, sets up HTTPS, forwards everything correctly. X-Forwarded-Proto is set per default — TravStats picks up the Secure cookie behaviour.

For internal-only use without a public domain, use a *.local hostname and Caddy’s tls internal directive:

travstats.local {
tls internal
reverse_proxy travstats-app:80
}

The browser will warn about the self-signed cert; accept once and move on.

The classic.

In your sites-enabled (or the equivalent for your distro):

server {
listen 443 ssl http2;
server_name travstats.example.com;
ssl_certificate /etc/letsencrypt/live/travstats.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/travstats.example.com/privkey.pem;
# Pass headers TravStats relies on
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;
location / {
proxy_pass http://127.0.0.1:3000; # APP_PORT from your .env
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
}
}
server {
listen 80;
server_name travstats.example.com;
return 301 https://$host$request_uri;
}

The four proxy_set_header lines are the important bit. Especially X-Forwarded-Proto — without it the cookie is set without Secure and the browser drops it on HTTPS, causing a login loop.

Terminal window
sudo certbot --nginx -d travstats.example.com

Auto-renewal lands in the system cron. Done.

Common in container-heavy homelabs because it auto-discovers services from labels.

Add to the existing app service:

services:
app:
image: ghcr.io/abrechen2/travstats:${VERSION:-latest}
# ... existing config
labels:
- "traefik.enable=true"
- "traefik.http.routers.travstats.rule=Host(`travstats.example.com`)"
- "traefik.http.routers.travstats.entrypoints=websecure"
- "traefik.http.routers.travstats.tls.certresolver=letsencrypt"
- "traefik.http.services.travstats.loadbalancer.server.port=80"
networks:
- traefik-public # Traefik's own network
- travstats-network

Assumes Traefik is already up with an letsencrypt certresolver configured. The passHostHeader=true and X-Forwarded-Proto are set by default — TravStats works without further customisation.

For “I want it on my private network without exposing anything to the public internet”.

On the host running TravStats:

Terminal window
sudo tailscale serve --bg --https=443 http://localhost:3000

Now https://travstats.<your-tailnet>.ts.net works from any device on your tailnet, with a valid Let’s Encrypt cert provisioned by Tailscale. No port-forwards, no public DNS records.

If you do want public access:

Terminal window
sudo tailscale funnel --bg --https=443 http://localhost:3000

Same as Serve but the URL is reachable from the public internet. Free tier supports it for a single domain.

Tailscale’s reverse proxy forwards X-Forwarded-Proto: https — TravStats sets Secure cookies, browsers honour them.

Mixing: Cloudflare Tunnel + Tailscale on different paths

Section titled “Mixing: Cloudflare Tunnel + Tailscale on different paths”

Some homelabs run both — public domain via Cloudflare Tunnel for “share with friends” + Tailscale Serve for “fast LAN access without the Cloudflare hop”. TravStats handles both fine; just make sure each proxy forwards the right Host and X-Forwarded-Proto and both tunnels point at travstats-app:80 (or localhost:3000 from the host).

SymptomCauseFix
Login loop, immediately back to login screenX-Forwarded-Proto not forwardedAdd proxy_set_header X-Forwarded-Proto $scheme; (nginx) or equivalent
Login works on localhost:3000 but not on the public domainSame as above
”Site can’t be reached” with cert warningCert not provisioned (Let’s Encrypt rate-limit, DNS not propagated)Wait, retry, check caddy --version / certbot certificates
Site loads but invitation emails point at localhost:3000Admin → Settings → Public URL still on the old valueUpdate it to your real URL
Slow first load, then fastCold-start of the bundled Ollama containerNormal, Ollama warms up after first request

The Troubleshooting page has the full proxy-related symptom catalog.

The bundled db and ollama services are on the internal travstats-network for a reason. Do not add port mappings to expose them to the host or the public internet — there’s no reason to, and Postgres exposed on the public internet is a recipe for problems.

The only service that needs an exposed port is app:80, and even that is usually fronted by a reverse proxy rather than directly exposed.