Reverse Proxy

When you run Windshift behind a reverse proxy, configure it to trust forwarded headers and set its browser-security origins.

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 browser origins for CORS, CSRF, WebAuthn, and SSO redirect validation. It does not filter Host headers. Leave it unset for one public origin to derive it from BASE_URL.
--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: Set BASE_URL to the public URL users use to access Windshift, such as https://windshift.example.com. Without it, Windshift points email links, SSO redirects, and calendar feeds to http://localhost:8080.

Important: When you enable --use-proxy, Windshift trusts the X-Forwarded-For and X-Forwarded-Proto headers. Any client can spoof these headers. Make sure only the reverse proxy can connect to the Windshift port. Bind Windshift to 127.0.0.1, or use firewall rules or Docker networking to block external access. Otherwise, an attacker can forge headers to bypass IP restrictions or make Windshift treat an HTTP connection as HTTPS.

Caddy

Use this Caddy configuration for automatic HTTPS:

windshift.example.com {
    reverse_proxy localhost:8080
}

Caddy gets TLS certificates from Let's Encrypt and sets the required 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;

        # Server-Sent Events (live updates)
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_read_timeout 3600s;

        # Match Windshift's 32 MiB multipart upload limit.
        client_max_body_size 32m;
    }
}

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.0
    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. The browser must receive two root assets unchanged. They enable installation and push notifications:

  • 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 assets through with the configurations above. Avoid these common mistakes:

  • Do not rewrite or override their Content-Type. The browser rejects a service worker with the wrong content type.
  • Do not strip the Service-Worker-Allowed header.
  • Do not add aggressive caching for these paths. Windshift sends Cache-Control: no-cache so clients get new versions. A caching proxy can keep users on a stale app.

If you proxy specific paths instead of /, include /m, /manifest.webmanifest, and /service-worker.js.

Subpath deployments

To serve Windshift under a path prefix, set the prefix in BASE_URL and WINDSHIFT_CONTEXT_PATH. For example, use https://example.com/windshift/:

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;

    # Server-Sent Events (live updates)
    proxy_http_version 1.1;
    proxy_buffering off;
    proxy_read_timeout 3600s;
    client_max_body_size 32m;
}

Keep BASE_URL and WINDSHIFT_CONTEXT_PATH in agreement. Windshift derives generated links, SSO redirects, mobile PWA paths, and asset URLs from them.

TLS Termination

If Windshift terminates TLS without a reverse proxy, use its built-in TLS support:

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

Use this option for a simple deployment that does not need an external proxy.