Docker

Windshift provides official Docker images built as minimal scratch containers. The multi-stage build produces a small image containing only the compiled binary, CA certificates, and timezone data.

Quick Start

docker run -d \
  --name windshift \
  -p 8080:8080 \
  --tmpfs /tmp: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 generates a random secret on each docker run. For production, generate a secret once and pass it explicitly so it persists across container restarts - see the Docker Compose examples below.

Docker Compose

The recommended way to run Windshift in production. Create a docker-compose.yml:

services:
  windshift:
    image: ghcr.io/windshiftapp/windshift:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    tmpfs:
      - /tmp: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:

Before First Startup

Generate an SSO_SECRET and create a .env file before running docker compose up. This secret secures both 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
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: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:17-alpine
    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: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.4
    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 running behind a reverse proxy, always set:

  • USE_PROXY=true - Trusts X-Forwarded-Proto and X-Forwarded-For headers
  • BASE_URL - Public URL for generating links in emails, SSO redirects, and calendar feeds
  • ALLOWED_HOSTS - Restricts which hostnames Windshift will accept requests for

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 certs and timezone data

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

SQLite with tmpfs

When using SQLite in a scratch container, mount a tmpfs volume for the WAL files to avoid filesystem compatibility issues:

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

Optional Services

Windshift supports additional companion services that run as separate containers:

  • LLM Inference - Local AI model powering features like Plan My Day, Catch Me Up, and task decomposition (~2 GB RAM)
  • Logbook - Knowledge management and document ingestion service for team knowledge bases