Production-ready self-hosting

Use this guide before you expose Windshift to users. It gives you an ordered setup and security checklist.

The examples assume a public HTTPS deployment behind a reverse proxy. For a private organization network, adapt the outbound network policy to your integrations.

Before you start

Prepare these resources:

  • A DNS name and an HTTPS certificate
  • A host or container platform with persistent storage
  • A backup destination outside the Windshift deployment
  • A secret manager or another protected location for SSO_SECRET
  • A PostgreSQL database, or a plan to use SQLite for a small single-node installation

Windshift allows local and private outbound connections by default. This supports internal SCM, Jira, identity, mail, webhook, model, plugin, and runner services. A public or multi-user installation should usually block these connections.

1. Set the required configuration

Generate SSO_SECRET once. Keep the same value across restarts and upgrades:

openssl rand -hex 32

Set these values before the first startup:

SSO_SECRET=<generated-secret>
BASE_URL=https://windshift.example.com

SSO_SECRET signs session cookies and SSO state. Windshift also uses it to encrypt stored SSO, SCM, and LLM credentials. If you change it, existing sessions and stored encrypted credentials stop working.

Set BASE_URL to the URL that users enter in their browsers. Windshift uses it for email links, SSO redirects, WebAuthn, calendar feeds, and runner callbacks.

2. Serve Windshift through HTTPS

Use a reverse proxy such as Caddy, Nginx, or Traefik. Set these Windshift values when the proxy terminates TLS:

USE_PROXY=true
BASE_URL=https://windshift.example.com

Set ALLOWED_HOSTS only when you need additional browser origins. For one public origin, Windshift derives it from BASE_URL.

When USE_PROXY=true, Windshift trusts X-Forwarded-Proto and X-Forwarded-For. Do not expose the Windshift port directly. Bind it to a private interface, restrict it with a firewall, or keep it on an internal Docker network.

ALLOWED_HOSTS controls CORS, CSRF, WebAuthn, and SSO redirect validation. It does not reject requests based on the Host header. Your proxy must still route only the intended public hostname.

Set WEBAUTHN_RP_ID explicitly in containers:

WEBAUTHN_RP_ID=windshift.example.com

The value must be a hostname without a scheme, port, or path. See Reverse Proxy for proxy configurations.

3. Activate SSRF protection

Why this matters

A user, imported value, webhook, or automation can influence an outbound destination. Without a network boundary, that destination could expose services on the Windshift host or its private network.

Enable the protection

Set ALLOW_LOCAL_CONNECTIONS=false. This is the explicit opt-in for SSRF address blocking:

ALLOW_LOCAL_CONNECTIONS=false

For a standalone binary, export the value before you start Windshift:

export ALLOW_LOCAL_CONNECTIONS=false
./windshift

For Docker Compose, add the value to .env:

ALLOW_LOCAL_CONNECTIONS=false

Then pass it to the Windshift service:

services:
  windshift:
    environment:
      - ALLOW_LOCAL_CONNECTIONS=${ALLOW_LOCAL_CONNECTIONS:-false}

Restart the service after you change the setting. Confirm the startup log contains:

local connections disabled: server-side HTTP clients will block loopback/private addresses

When this setting is false, Windshift blocks server-side connections to loopback, unspecified, link-local, multicast, private, and CGNAT address ranges. The check runs after DNS resolution and before the connection, which also protects against DNS rebinding. Redirects are not followed by the SSRF-safe HTTP client.

This setting is global. It affects configured server-side clients for SCM, Jira, LLM, OIDC, webhooks, SMTP, IMAP, plugins, and runner-related requests. It is not a per-integration allowlist.

Check the trade-off

Private integrations fail while the setting is false. Test every configured integration after you enable it. This includes:

  • A self-hosted SCM or Jira Data Center instance
  • A private OIDC provider
  • An internal SMTP or IMAP server
  • A local or private LLM endpoint
  • Internal webhook destinations

If Windshift must reach private services, keep ALLOW_LOCAL_CONNECTIONS=true and enforce an outbound firewall policy outside the application. Block cloud metadata services, container or virtual-machine control sockets, host management ports, Kubernetes control planes, and unrelated private subnets. The application setting does not replace network segmentation.

4. Configure authentication

Configure OIDC in Admin > Single Sign-On (SSO) when your identity provider is ready. Test a new login before you disable password login.

Register passkeys after BASE_URL and WEBAUTHN_RP_ID are correct. Use SESSION_IP_BINDING=log first. Review the mismatch logs before you use strict, because mobile clients, load balancers, and changing networks can change a client IP.

Keep admin fallback disabled unless your recovery plan needs it. If you enable it, protect the recovery credentials and document how you will disable the fallback after recovery.

5. Persist and protect application data

Persist these locations:

  • The selected SQLite database or PostgreSQL data
  • The directory in ATTACHMENT_PATH
  • The plugin directories when plugins are enabled
  • Any other application data under /data

The Docker image runs as UID 65534. Make sure this user can write to the mounted data directory. Mount a writable tmpfs at /tmp with exec in every Docker deployment:

tmpfs:
  - /tmp:exec,size=64M

For SQLite, stop writes before a file-copy backup, or use the SQLite .backup command. For PostgreSQL, use pg_dump or your managed database backup service. Include attachments and plugins in the backup. Test a restore before you rely on the backup.

6. Keep outbound TLS and deployment limits safe

Keep certificate verification enabled:

TLS_SKIP_VERIFY=false

This setting controls outbound HTTPS, SMTP, IMAP, and LDAP connections for the whole process. Install a trusted internal certificate authority instead of disabling verification.

Pin a published Docker tag or an immutable image digest. Set LOG_FORMAT=json in container deployments. Set a process memory budget and a container limit that is at least as large. Keep PostgreSQL connection budgets below the database max_connections value.

Disable plugins when you do not use them:

DISABLE_PLUGINS=true

Run coding-agent runners on dedicated hosts when possible. Do not mount the Docker socket into the Windshift server.

7. Verify the installation

Run these checks after startup:

curl https://windshift.example.com/healthz
curl https://windshift.example.com/readyz
curl https://windshift.example.com/api/version
curl https://windshift.example.com/api/setup/status

Check that:

  • /healthz returns {"status":"ok"}.
  • /readyz returns {"status":"ready","database":"ok"}.
  • /api/version reports the pinned release.
  • The setup status shows that the intended admin account exists.
  • The browser uses HTTPS without certificate warnings.
  • Login, logout, passkeys or SSO, attachments, and one configured integration work.
  • Direct requests to the Windshift backend port fail from outside the private network.
  • The logs show the expected database, proxy, and SSRF settings.

Ongoing operations

  • Back up the database and persistent files before every upgrade.
  • Test database and attachment restores on a schedule.
  • Review administrator and workspace-administrator membership.
  • Monitor health, readiness, database capacity, memory, cache evictions, authentication failures, and failed background work.
  • Read the release notes before each upgrade.
  • Keep Windshift, PostgreSQL, the reverse proxy, and runner hosts current.