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 32Set these values before the first startup:
SSO_SECRET=<generated-secret>
BASE_URL=https://windshift.example.comSSO_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.comSet 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.comThe 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=falseFor a standalone binary, export the value before you start Windshift:
export ALLOW_LOCAL_CONNECTIONS=false
./windshiftFor Docker Compose, add the value to .env:
ALLOW_LOCAL_CONNECTIONS=falseThen 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 addressesWhen 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=64MFor 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=falseThis 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=trueRun 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/statusCheck that:
/healthzreturns{"status":"ok"}./readyzreturns{"status":"ready","database":"ok"}./api/versionreports 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.