Reverse Proxy

When running Windshift behind a reverse proxy, configure Windshift to trust forwarded headers and restrict allowed hostnames.

Required Settings

./windshift --use-proxy --allowed-hosts windshift.example.com --base-url https://windshift.example.com

Or via environment variables:

export USE_PROXY=true
export ALLOWED_HOSTS=windshift.example.com
export BASE_URL=https://windshift.example.com
./windshift
Flag Env Var Description
--use-proxy USE_PROXY Trust X-Forwarded-Proto and X-Forwarded-For headers
--allowed-hosts ALLOWED_HOSTS Comma-separated list of valid hostnames
--base-url BASE_URL Public URL used to generate links in emails, SSO redirects, and calendar feeds
--allowed-port - Port used for CORS and WebAuthn origin validation
--additional-proxies ADDITIONAL_PROXIES Additional trusted proxy IPs beyond the default private ranges

Note: BASE_URL should be set to the public URL users access Windshift from (e.g. https://windshift.example.com). Without it, email verification links, magic login links, SSO redirects, and calendar feed URLs will point at http://localhost:8080.

Important: When --use-proxy is enabled, Windshift trusts X-Forwarded-For and X-Forwarded-Proto headers to determine the client's real IP address and protocol. These headers can be spoofed by any client. You must ensure that Windshift is not directly reachable from the internet - only the reverse proxy should be able to connect to Windshift's port. Bind Windshift to 127.0.0.1 or use firewall rules / Docker networking to block external access. Otherwise, an attacker can forge these headers to bypass IP-based restrictions or trick Windshift into thinking a connection is over HTTPS when it isn't.

Caddy

Caddy provides automatic HTTPS with minimal configuration:

windshift.example.com {
    reverse_proxy localhost:8080
}

Caddy automatically handles TLS certificates via Let's Encrypt and sets the correct forwarded headers.

Nginx

server {
    listen 443 ssl http2;
    server_name windshift.example.com;

    ssl_certificate     /etc/ssl/certs/windshift.pem;
    ssl_certificate_key /etc/ssl/private/windshift.key;

    location / {
        proxy_pass http://localhost: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 (for live updates)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Traefik

Using Docker labels:

services:
  windshift:
    image: ghcr.io/windshiftapp/windshift:latest
    environment:
      - USE_PROXY=true
      - ALLOWED_HOSTS=${DOMAIN}
      - BASE_URL=https://${DOMAIN}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.windshift.rule=Host(`${DOMAIN}`)"
      - "traefik.http.routers.windshift.entrypoints=websecure"
      - "traefik.http.routers.windshift.tls.certresolver=letsencrypt"
      - "traefik.http.services.windshift.loadbalancer.server.port=8080"

  traefik:
    image: traefik:v3.4
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt-data:/letsencrypt

Mobile app (PWA) assets

The Windshift mobile app at /m is a Progressive Web App. Two assets at the site root must reach the browser unchanged for installation and push notifications to work:

  • GET /manifest.webmanifest, served as application/manifest+json
  • GET /service-worker.js, served as text/javascript with a Service-Worker-Allowed: / header

Most proxies pass these through correctly with the configs above. The pitfalls to avoid:

  • Do not rewrite or override their Content-Type. A service worker served with the wrong content type is rejected by the browser.
  • Do not strip the Service-Worker-Allowed header.
  • Do not add aggressive caching for these two paths. Windshift already sends Cache-Control: no-cache so clients pick up new versions; a proxy that caches them can pin users to a stale app.

If you proxy only specific paths rather than /, make sure /m, /manifest.webmanifest, and /service-worker.js are all included.

Subpath deployments

To serve Windshift under a path prefix (for example https://example.com/windshift/) instead of its own hostname, set the prefix in both BASE_URL and WINDSHIFT_CONTEXT_PATH:

export BASE_URL=https://example.com/windshift
export WINDSHIFT_CONTEXT_PATH=/windshift
export USE_PROXY=true

Forward the prefix to Windshift and do not strip it before proxying. With Nginx:

location /windshift/ {
    proxy_pass http://localhost:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Prefix /windshift;

    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Keep BASE_URL and WINDSHIFT_CONTEXT_PATH in agreement: generated links, SSO redirects, the mobile PWA, and asset URLs all derive from them.

TLS Termination

If you prefer to terminate TLS at Windshift itself (without a reverse proxy), use the built-in TLS support:

./windshift --tls-cert /path/to/cert.pem --tls-key /path/to/key.pem

This is useful for simple deployments where an external proxy isn't needed.