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: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 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: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 above assumes you serve Windshift over HTTPS, with TLS terminated either by Windshift itself or by a reverse proxy in front of it (see the Traefik example below). BASE_URL=http://localhost:8080 is also fine for trying things out on the machine itself.
What does not work is a plain-http BASE_URL with any other hostname or IP, such as http://192.168.1.50:8080 or an internal DNS name. Windshift uses credentialed cross-origin requests, and the CORS layer refuses insecure (http) origins other than 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..."and every browser request from that origin fails with CORS_CONFIG_ERROR. Pick one of these instead:
Recommended: terminate TLS at a reverse proxy and set
USE_PROXY=true(Traefik example below, or your existing proxy).Let Windshift terminate TLS directly with
--tls-certand--tls-key.For a homelab or test box on a trusted LAN where HTTPS is not an option, opt in to plain http explicitly:
environment: - BASE_URL=http://192.168.1.50:8080 - ALLOW_INSECURE_HTTP=trueSessions and data then travel unencrypted, so anyone on the network path can read or hijack them. Everything else (CSRF protection, rate limiting) keeps working normally. Do not use this for production deployments.
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 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: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: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.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- TrustsX-Forwarded-ProtoandX-Forwarded-ForheadersBASE_URL- Public URL for generating links in emails, SSO redirects, WebAuthn, and calendar feedsALLOWED_HOSTS- Restricts which hostnames Windshift will accept requests for
Do not publish the Windshift backend port directly when USE_PROXY=true; only the proxy should be able to 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 certs and timezone data
The final image runs as an unprivileged user (UID 65534) and exposes port 8080.
The /tmp tmpfs mount
The scratch image contains no /tmp directory at all — only what the build copies in. Windshift needs a temp directory for two things:
- SQLite writes WAL and other temporary files to
/tmp, and a tmpfs avoids filesystem compatibility issues there. - The coding-agent runner uses it as scratch space for git operations: a per-invocation
GIT_ASKPASScredential helper (so repository tokens never appear in command lines or.git/config) and a sanitized staging repository for pushes.
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 in that one line matter:
execis required. Docker mounts tmpfs withnoexecby default, which prevents git from executing the askpass helper script — coding-agent runs would fail with a git credential error even though/tmpexists. Theexecoption lifts that.- Use the short syntax shown above. The long
volumes:syntax (type: tmpfswith atmpfs:sub-key) offers no way to disablenoexec, and itsmode:field is a YAML footgun — an unquotedmode: 1777is parsed as decimal and mounts with the wrong permissions. The short syntax defaults to the correct sticky, world-writable mode (1777).
Without the mount, the symptom depends on the feature: SQLite fails on WAL operations, and coding-agent runs fail with errors like prepare checkout: ... setup askpass: stat /tmp: no such file or directory. With the mount but without exec, coding-agent runs fail with a permission error executing the credential helper instead.
Local AI models from Docker
Local / Custom AI connections are SSRF-protected by default: server-side HTTP clients refuse to dial loopback and private addresses. If Windshift needs to reach a model server on the Docker host or a private network, enable the global private-egress switch and 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 (LLM providers, SCM integrations, Jira import, OIDC, webhooks), not just AI connections. Only enable it when your network policy keeps sensitive internal endpoints (cloud metadata services, admin panels) out of reach of the Windshift host. See Configuration Options for details.
Optional Services
Windshift supports additional companion services that run as separate containers:
- Coding Agent Runner - Runs coding agents server-side in ephemeral per-run containers and opens draft pull requests