Docker

Windshift provides official Docker images as minimal scratch containers. The multi-stage build includes only the compiled binary, CA certificates, and timezone data.

Quick Start

docker run -d \
  --name windshift \
  -p 8080:8080 \
  --tmpfs /tmp:exec,size=64M \
  -v windshift-data:/data \
  -e BASE_URL=http://localhost:8080 \
  -e SSO_SECRET=$(openssl rand -hex 32) \
  ghcr.io/windshiftapp/windshift:latest

Note: This command generates a random secret on each docker run. For production, generate a secret once and pass it explicitly. See the Docker Compose examples below.

Docker Compose

For production, use Docker Compose. Create docker-compose.yml:

services:
  windshift:
    image: ghcr.io/windshiftapp/windshift:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    tmpfs:
      - /tmp:exec,size=64M
    environment:
      - BASE_URL=https://windshift.example.com
      - SSO_SECRET=${SSO_SECRET}
      - DB_PATH=/data/windshift.db
      - ATTACHMENT_PATH=/data/attachments
    volumes:
      - windshift-data:/data

volumes:
  windshift-data:

Plain HTTP only works for localhost

The Compose example assumes HTTPS. Terminate TLS at Windshift or at a reverse proxy. For local testing, use BASE_URL=http://localhost:8080.

A plain-HTTP BASE_URL does not work with another hostname or IP, such as http://192.168.1.50:8080 or an internal DNS name. Windshift uses credentialed cross-origin requests. Its CORS layer rejects insecure HTTP origins except localhost. The server starts but logs:

Failed to create CORS middleware error="cors: for security reasons, insecure origin patterns like \"http://myhost.internal:8080\" cannot be allowed..."

Every browser request from that origin then fails with CORS_CONFIG_ERROR. Use one of these options:

  1. Recommended: Terminate TLS at a reverse proxy and set USE_PROXY=true.

  2. Let Windshift terminate TLS directly with --tls-cert and --tls-key.

  3. On a trusted LAN or test host where HTTPS is not available, opt in to plain HTTP:

    environment:
      - BASE_URL=http://192.168.1.50:8080
      - ALLOW_INSECURE_HTTP=true

    Sessions and data then travel unencrypted. Anyone on the network path can read or hijack them. CSRF protection and rate limiting still work. Do not use this for production.

Before First Startup

Before you run docker compose up, generate an SSO_SECRET and create a .env file. This secret secures SSO state and session cookies.

# Generate the secret
openssl rand -hex 32

Add it to a .env file alongside your other settings:

DOMAIN=windshift.example.com
BASE_URL=https://windshift.example.com
PORT=8080
SSO_SECRET=<your-generated-secret>
POSTGRES_PASSWORD=    # only needed for PostgreSQL
LETSENCRYPT_EMAIL=    # only needed for Traefik

With PostgreSQL

To use PostgreSQL instead of SQLite, add a postgres service:

services:
  windshift:
    image: ghcr.io/windshiftapp/windshift:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    tmpfs:
      - /tmp:exec,size=64M
    environment:
      - BASE_URL=https://windshift.example.com
      - SSO_SECRET=${SSO_SECRET}
      - POSTGRES_CONNECTION_STRING=postgres://windshift:${POSTGRES_PASSWORD}@postgres:5432/windshift?sslmode=disable
      - ATTACHMENT_PATH=/data/attachments
    volumes:
      - windshift-data:/data
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:18
    restart: unless-stopped
    environment:
      - POSTGRES_USER=windshift
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=windshift
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U windshift"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  windshift-data:
  postgres-data:

With Traefik (HTTPS)

Add Traefik for automatic HTTPS with Let's Encrypt:

services:
  windshift:
    image: ghcr.io/windshiftapp/windshift:latest
    restart: unless-stopped
    tmpfs:
      - /tmp:exec,size=64M
    environment:
      - BASE_URL=https://${DOMAIN}
      - SSO_SECRET=${SSO_SECRET}
      - USE_PROXY=true
      - ALLOWED_HOSTS=${DOMAIN}
      - DB_PATH=/data/windshift.db
      - ATTACHMENT_PATH=/data/attachments
    volumes:
      - windshift-data:/data
    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
    restart: unless-stopped
    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

volumes:
  windshift-data:
  letsencrypt-data:

When you run behind a reverse proxy, set:

  • USE_PROXY=true: Trusts X-Forwarded-Proto and X-Forwarded-For headers.
  • BASE_URL: Public URL for email links, SSO redirects, WebAuthn, and calendar feeds.
  • ALLOWED_HOSTS: Optional browser-origin allowlist for CORS, CSRF, WebAuthn, and SSO redirect validation. Windshift derives it from BASE_URL for a single domain. It does not filter Host headers.

Do not publish the Windshift backend port directly when USE_PROXY=true. Only the proxy should connect to it.

Docker Image Details

The official image uses a multi-stage build:

  1. Frontend build: Node.js 25-alpine runs npm ci and builds with Vite.
  2. Backend build: Go 1.26-alpine compiles a static binary (CGO_ENABLED=0).
  3. Runtime: Scratch image with CA certificates and timezone data.

The final image runs as an unprivileged user (UID 65534) and exposes port 8080.

The /tmp tmpfs mount

The scratch image has no /tmp directory. Windshift needs a temporary directory for two purposes:

  • Large multipart uploads can spill data to /tmp after they exceed the 32 MiB memory threshold.
  • The coding-agent runner uses /tmp for git operations. It creates a per-invocation GIT_ASKPASS helper and a sanitized staging repository for pushes. Repository tokens never appear in command lines or .git/config.

Mount a tmpfs at /tmp in every deployment:

windshift:
  image: ghcr.io/windshiftapp/windshift:latest
  tmpfs:
    - /tmp:exec,size=64M
  volumes:
    - windshift-data:/data

Two details matter:

  • exec is required. Docker mounts tmpfs with noexec by default. Git then cannot execute the askpass helper, even when /tmp exists. The exec option allows it.
  • Use the short syntax shown above. The long volumes: syntax (type: tmpfs with a tmpfs: sub-key) cannot disable noexec. Its mode: field also has a YAML pitfall. An unquoted mode: 1777 parses as decimal and sets incorrect permissions. The short syntax uses the correct sticky, world-writable mode (1777).

Without this mount, large attachment uploads can fail when they need temporary storage. Coding-agent runs can also fail with prepare checkout: ... setup askpass: stat /tmp: no such file or directory. Without exec, git cannot execute the credential helper.

Local AI models from Docker

By default, server-side HTTP clients block Local / Custom AI connections to loopback and private addresses. If Windshift must reach a model server on the Docker host or private network, enable the global private-egress switch. Then configure the AI connection in the admin UI:

services:
  windshift:
    image: ghcr.io/windshiftapp/windshift:latest
    environment:
      - ALLOW_LOCAL_CONNECTIONS=true

Then use a Local / Custom base URL such as:

http://172.17.0.1:11434/v1

On Docker Desktop, host.docker.internal usually works as the hostname instead of the bridge IP.

ALLOW_LOCAL_CONNECTIONS=true applies to all server-side outbound HTTP, not only AI connections. This includes LLM providers, SCM integrations, Jira import, OIDC, and webhooks. Enable it only when network policy keeps sensitive internal endpoints, such as cloud metadata services and admin panels, out of reach of the Windshift host. See Configuration Options for details.

Optional Services

Windshift supports companion services in separate containers:

  • Coding Agent Runner: Runs coding agents server-side in one ephemeral container per job and opens draft pull requests.