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:latestNote: 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:
Recommended: Terminate TLS at a reverse proxy and set
USE_PROXY=true.Let Windshift terminate TLS directly with
--tls-certand--tls-key.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=trueSessions 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 32Add 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 TraefikWith 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: TrustsX-Forwarded-ProtoandX-Forwarded-Forheaders.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 fromBASE_URLfor a single domain. It does not filterHostheaders.
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:
- Frontend build: Node.js 25-alpine runs
npm ciand builds with Vite. - Backend build: Go 1.26-alpine compiles a static binary (
CGO_ENABLED=0). - 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
/tmpafter they exceed the 32 MiB memory threshold. - The coding-agent runner uses
/tmpfor git operations. It creates a per-invocationGIT_ASKPASShelper 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:/dataTwo details matter:
execis required. Docker mounts tmpfs withnoexecby default. Git then cannot execute the askpass helper, even when/tmpexists. Theexecoption allows it.- Use the short syntax shown above. The long
volumes:syntax (type: tmpfswith atmpfs:sub-key) cannot disablenoexec. Itsmode:field also has a YAML pitfall. An unquotedmode: 1777parses 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=trueThen use a Local / Custom base URL such as:
http://172.17.0.1:11434/v1On 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.