Open-source alternatives guide
How to Self-Host Windmill 2026
Deploy Windmill on your own server with Docker Compose. The open source workflow automation platform designed for developers — scripts, flows, and apps.
What Is Windmill?
Windmill is an open source developer platform for building internal tools, workflows, and scripts. Unlike n8n (visual flowchart-first) or Zapier (no-code), Windmill is built for teams that want to write real code while getting the scheduling, monitoring, and UI generation for free.
The pitch: Write a Python or TypeScript function → Windmill gives you a REST API endpoint, a scheduled job, a shareable form UI, and a production monitoring dashboard. All in one platform.
n8n Cloud costs $20-50/month. Windmill is free when self-hosted.
Key features:
- Scripts in Python, TypeScript/Deno, Go, Bash, SQL
- Flows — visual orchestration of scripts (like n8n but code-first)
- Apps — low-code UI builder for internal tools (like Retool)
- Job scheduler (cron and webhook triggers)
- Resource management (secrets, API credentials)
- Audit logs and job history
- Worker groups for scaling
- GitHub sync for version control
Windmill vs n8n: Which Should You Use?
| Scenario | Choose |
|---|---|
| Non-engineers building automations | n8n |
| Developers building internal tooling | Windmill |
| Replacing Zapier/Make | n8n |
| Replacing Retool/Airplane | Windmill |
| Data pipelines with Python | Windmill |
| Visual workflow designer | n8n |
| API endpoints from scripts | Windmill |
Windmill isn't trying to replace n8n — it's a different tool for a different audience.
Prerequisites
- VPS with 2 vCPU, 2GB RAM (Hetzner CX32 ~€5.49/month)
- Docker + Docker Compose v2
- Domain name with DNS pointed to your server
- SMTP for email (optional)
Docker Compose Deployment
1. Clone Official Docker Setup
git clone https://github.com/windmill-labs/windmill
cd windmill
2. Configure Environment
cp .env.example .env
Edit .env:
# .env
# Domain
WM_BASE_URL=https://wm.yourdomain.com
# Database
DATABASE_URL=postgres://windmill:your-password@db/windmill
# Secrets
PASSWORD=your-windmill-admin-password
# Email (optional)
SMTP_FROM=noreply@yourdomain.com
SMTP_HOST=smtp.yourdomain.com
SMTP_PORT=587
SMTP_USERNAME=your-smtp-user
SMTP_PASSWORD=your-smtp-password
SMTP_TLS_IMPLICIT=true
# Enterprise features (leave blank for Community Edition)
# LICENSE_KEY=
3. The docker-compose.yaml
version: "3.7"
services:
db:
image: postgres:16
shm_size: 1g
restart: unless-stopped
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: windmill
POSTGRES_USER: windmill
POSTGRES_PASSWORD: your-password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U windmill"]
interval: 10s
timeout: 5s
retries: 5
windmill_server:
image: ghcr.io/windmill-labs/windmill:main
pull_policy: always
restart: unless-stopped
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=${DATABASE_URL}
- WM_BASE_URL=${WM_BASE_URL}
- RUST_LOG=info
- MODE=server
volumes:
- worker_logs:/tmp/windmill/logs
windmill_worker:
image: ghcr.io/windmill-labs/windmill:main
pull_policy: always
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=${DATABASE_URL}
- WM_BASE_URL=${WM_BASE_URL}
- RUST_LOG=info
- MODE=worker
- WORKER_GROUP=default
volumes:
- worker_logs:/tmp/windmill/logs
- /var/run/docker.sock:/var/run/docker.sock # for Docker-in-Docker scripts
- worker_dependency_cache:/tmp/windmill/cache
windmill_worker_native:
image: ghcr.io/windmill-labs/windmill:main
pull_policy: always
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=${DATABASE_URL}
- WM_BASE_URL=${WM_BASE_URL}
- RUST_LOG=info
- MODE=worker
- WORKER_GROUP=native
- NUM_WORKERS=8
lsp:
image: ghcr.io/windmill-labs/windmill-lsp:latest
pull_policy: always
restart: unless-stopped
ports:
- "3001:3001"
caddy:
image: caddy:2.7-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- windmill_server
volumes:
db_data:
worker_logs:
worker_dependency_cache:
caddy_data:
caddy_config:
4. Configure Built-In Caddy
Windmill ships a Caddyfile for automatic HTTPS:
# Caddyfile
{$WM_BASE_URL} {
reverse_proxy /ws/* windmill_server:8000
reverse_proxy /* windmill_server:8000
}
Update .env and run:
docker compose up -d
# Check all services
docker compose ps
docker compose logs -f windmill_server
First Login
Visit https://wm.yourdomain.com:
Default credentials:
Email: admin@windmill.dev
Password: changeme (or what you set in PASSWORD env)
Change the password immediately:
Top-right → Account → Change Password
Windmill Architecture: Scripts, Flows, Apps
Scripts — The Foundation
A script is a function with typed inputs that Windmill runs on demand or on a schedule:
# Python script: fetch_github_stats.py
import requests # auto-installed by Windmill
def main(owner: str, repo: str, github_token: str) -> dict:
"""
Fetch GitHub repository statistics.
owner: GitHub org or username
repo: Repository name
github_token: Personal access token (use Resource for secrets)
"""
headers = {"Authorization": f"token {github_token}"}
r = requests.get(
f"https://api.github.com/repos/{owner}/{repo}",
headers=headers
)
r.raise_for_status()
data = r.json()
return {
"stars": data["stargazers_count"],
"forks": data["forks_count"],
"open_issues": data["open_issues_count"],
"last_push": data["pushed_at"],
}
Save this script in Windmill → it immediately gets:
- A REST API endpoint (
POST /api/w/workspace/jobs/run/p/fetch_github_stats) - A form UI (auto-generated from type annotations)
- A scheduler (add cron expression)
- Job history and logs
TypeScript Scripts
// TypeScript/Deno script: send_slack_notification.ts
import * as wmill from "npm:windmill-client@1";
// Resources are stored in Windmill's credential manager
type SlackResource = {
token: string;
};
export async function main(
message: string,
channel: string,
slack: SlackResource, // auto-populated from stored resource
): Promise<{ ok: boolean; ts: string }> {
const response = await fetch("https://slack.com/api/chat.postMessage", {
method: "POST",
headers: {
"Authorization": `Bearer ${slack.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ channel, text: message }),
});
const data = await response.json();
return { ok: data.ok, ts: data.ts };
}
SQL Scripts
-- SQL script: monthly_revenue_report.sql
-- Database resource: production_postgres
SELECT
date_trunc('month', created_at) AS month,
COUNT(*) AS orders,
SUM(amount) AS revenue,
AVG(amount) AS avg_order_value
FROM orders
WHERE
created_at >= NOW() - INTERVAL '12 months'
AND status = 'completed'
GROUP BY 1
ORDER BY 1 DESC;
Windmill connects to your PostgreSQL, MySQL, BigQuery, or Snowflake via Resources.
Flows — Orchestrate Scripts
A flow is a visual DAG of steps, each step being a script:
Flow: New User Onboarding
Step 1: get_user_details (Python)
↓
Step 2: create_stripe_customer (TypeScript)
↓
Step 3: add_to_crm (Python)
↓ (parallel)
Step 4a: send_welcome_email (Python)
Step 4b: notify_slack (TypeScript)
↓
Step 5: log_to_database (SQL)
Create a Flow
Flows → New Flow
1. Add step → Choose script from library or write inline
2. Connect inputs: previous step outputs → next step inputs
3. Add conditions: "If step 2 fails → go to error handler"
4. Add loops: "For each user in list → run step"
5. Save and test with sample inputs
Branching Logic
# In flow editor (visual), this represents:
# "If Stripe customer creation fails, send alert and stop"
Step 2: create_stripe_customer
onError:
- notify_slack: { message: "Stripe failure for {user_email}" }
- stop_flow
onSuccess:
- next_step
Apps — Internal Tool Builder
Apps let non-engineers interact with scripts via a UI you build in Windmill:
Apps → New App
Components available:
- Text, Number, Date inputs
- Dropdowns (static or from script)
- Tables (with inline editing)
- Charts (from script output)
- Buttons (run a script on click)
Example: Customer Support Lookup App
App: Customer Lookup
Input: customer_email (text field)
Button: "Lookup" → runs get_customer_details script
Table: displays orders, subscription status, recent activity
Button: "Issue Refund" → runs issue_refund script
(shows confirmation dialog first)
Button: "Reset Password" → runs trigger_password_reset
This replaces a Retool/Airplane internal tool — built in Windmill.
Resources — Secrets and Credentials
Resources are named credentials stored in Windmill:
Resources → New Resource
Type: PostgreSQL
Name: production_db
Fields:
host: db.yourdomain.com
port: 5432
dbname: app_production
user: readonly_user
password: ***
sslmode: require
Scripts receive resources as typed arguments — no hardcoded secrets in code:
import psycopg2
def main(db: dict): # db is the PostgreSQL resource
conn = psycopg2.connect(**db)
# ...
Schedule Scripts and Flows
Scripts → My Script → Schedule
→ Cron expression: "0 9 * * 1" (every Monday at 9am)
→ Or: "*/15 * * * *" (every 15 minutes)
→ Input defaults (filled in for scheduled runs)
→ Timezone: America/New_York
All scheduled job runs appear in the job queue with status, logs, and output.
GitHub Sync (GitOps)
Windmill can sync scripts and flows to/from a GitHub repository:
Settings → Git Sync
→ Repository: github.com/your-org/windmill-scripts
→ Branch: main
→ Token: GitHub PAT with repo access
Then:
# Your scripts in GitHub
scripts/
python/
fetch_github_stats.py
send_slack_notification.py
sql/
monthly_revenue_report.sql
flows/
new_user_onboarding.json
Changes pushed to GitHub automatically sync to Windmill — infrastructure-as-code for your automation.
Backup
#!/bin/bash
# backup-windmill.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/windmill"
mkdir -p $BACKUP_DIR
docker compose exec -T db pg_dump \
-U windmill windmill | gzip > $BACKUP_DIR/db_$DATE.sql.gz
find $BACKUP_DIR -mtime +14 -delete
Windmill vs n8n vs Zapier
| Feature | Windmill (self-hosted) | n8n (self-hosted) | Zapier |
|---|---|---|---|
| Target audience | Developers | Mixed | Non-engineers |
| Scripting | ✅ Python/TS/Go/SQL | ⚠️ JS only | ❌ |
| Visual flow builder | ✅ | ✅ | ✅ |
| Internal app builder | ✅ | ❌ | ❌ |
| Scheduler | ✅ | ✅ | ✅ |
| REST API for scripts | ✅ Auto-generated | ❌ | ❌ |
| Secret management | ✅ Resources system | ✅ Credentials | ✅ |
| Git sync | ✅ | ⚠️ | ❌ |
| Self-hosted | ✅ | ✅ | ❌ |
| Price | ~$10/mo server | ~$10/mo server | $20-49/mo |
| Non-engineer friendly | ❌ | ✅ | ✅ |
Troubleshooting
Workers not executing jobs:
docker compose logs windmill_worker | tail -30
# Check DATABASE_URL — workers connect directly to PostgreSQL
Python packages not installing:
# Windmill installs packages via pip in isolated environments
# Check worker logs for pip install failures
# Usually network issues in air-gapped environments
Script times out:
# Default timeout is 180s
# Increase per-script: Script settings → Timeout
# Or set WM_JOB_DEFAULT_TIMEOUT_SECS env on worker
Why Self-Host Windmill
Retool charges $10/user/month for their Starter plan and $50/user/month for Business. A small engineering team of 8 using Retool for internal tooling pays $4,800/year at the Starter tier — before any usage limits kick in. Windmill replaces Retool's internal app builder, plus adds a script runner and visual flow orchestrator that Retool doesn't have. Self-hosted Windmill runs on a $10–20/month VPS and has no per-user fees. n8n Cloud starts at $20/month for their Starter plan and scales to $50/month for teams — again, self-hosted Windmill covers this use case for server cost only.
The developer experience is the real differentiator. Retool and Airplane abstract over your code in ways that make simple things easy but complex things impossible. Windmill takes the opposite approach: write a real Python, TypeScript, or Go function, and Windmill handles the infrastructure around it — REST endpoint generation, form UI, scheduling, secrets, monitoring, and audit logs. The code is yours, version-controlled via the GitHub sync feature, and portable. You can copy a script out of Windmill and run it anywhere; you can't do that with a Retool "query."
Temporal is another comparison point for teams running long-running workflows. Temporal is excellent for durable execution but requires significant operational overhead — the server, database, and worker infrastructure are non-trivial to run. Windmill flows are simpler and cover most use cases without Temporal's complexity. For teams that need scheduled jobs, webhook-triggered pipelines, and dependency chaining between scripts, Windmill hits the right level of abstraction.
The GitHub sync feature is undervalued. All your scripts and flows live in a GitHub repository alongside your application code, in regular Python/TypeScript/SQL files. Code review, pull requests, and CI/CD apply to your automation code the same way they apply to your application code. This is infrastructure-as-code for internal tooling.
When NOT to self-host Windmill. Windmill is developer-facing by design. Non-engineers can use the Apps builder (the Retool-like UI builder), but writing scripts and flows requires code. If your team's automations are primarily built by non-technical users, n8n's visual node-based editor is a better starting point. Also, Windmill's Community Edition has some limitations versus Enterprise: no SSO, no SAML, no audit log streaming, and no worker group isolation. For regulated industries requiring detailed audit trails, evaluate the Enterprise tier.
Prerequisites
Windmill runs four services: a server, one or more workers, a native worker, and a language server (LSP) for in-browser code completion. The minimum recommended setup is 2 vCPU and 4GB RAM. A Hetzner CX32 (4 vCPU, 8GB RAM) at €9.90/month is the practical target for a development team — the extra CPU headroom matters when workers are actively executing Python or TypeScript scripts. For a solo developer or CI/CD use case, a CX22 (2 vCPU, 4GB RAM) at €4.50/month works. See the VPS comparison guide for full provider comparisons including dedicated CPU options.
Docker Engine 24+ and Docker Compose v2 are required. The official Windmill compose file is well-maintained and includes Caddy for automatic HTTPS — you don't need to configure a separate reverse proxy unless you want to integrate with an existing one. The compose setup includes separate containers for the server, workers, native workers, and the language server, so plan for a minimum of five containers running on the host.
Workers mount /var/run/docker.sock for Docker-in-Docker script execution. If you run scripts that spawn Docker containers, ensure the Docker socket is accessible. For production deployments where script isolation matters, consider using gVisor or containerd runtime alternatives. The worker_dependency_cache volume caches installed Python packages and npm modules between runs — this significantly speeds up repeated script executions after the first cold install.
Python dependency management is automatic but requires internet access on the workers. Windmill installs required packages into isolated virtual environments per script. If your VPS is in an air-gapped environment or behind a corporate proxy, configure the appropriate proxy environment variables on the worker containers.
DNS: create an A record for wm.yourdomain.com pointing to your server. Set WM_BASE_URL=https://wm.yourdomain.com in your .env before first launch — this value is embedded in generated script URLs and webhook endpoints. Getting this wrong means all generated webhook URLs will point to the wrong domain and will need to be regenerated after fixing the config.
Production Security Hardening
Windmill executes arbitrary code in workers — Python, TypeScript, Go, SQL scripts triggered by schedules or webhooks. Hardening the host is critical.
UFW firewall. Allow only SSH and HTTPS (Caddy handles port 80 for ACME redirects):
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Don't expose port 8000 (Windmill server) or 3001 (LSP) directly — Caddy proxies everything.
Change the default admin password immediately. Windmill ships with admin@windmill.dev / changeme. This is publicly documented. Change it before the instance is network-accessible.
Fail2ban. Protect SSH from brute force:
apt install fail2ban -y
Create /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 2h
findtime = 15m
maxretry = 5
[sshd]
enabled = true
Secrets in Resources, not scripts. Never put API keys or passwords directly in script code. Use Windmill's Resources system (Settings → Resources) to store credentials and pass them as typed function arguments. This keeps secrets out of version-controlled script files.
Environment variable security. Store DATABASE_URL and other secrets in .env with restricted permissions:
chmod 600 .env
SSH hardening:
# /etc/ssh/sshd_config
PasswordAuthentication no
PermitRootLogin no
Database backups. All scripts, flows, resources, and job history live in PostgreSQL. Schedule automated backups — the automated backup guide covers Restic-based encrypted backup workflows. Back up daily and test restores quarterly. The GitHub sync feature (if configured) also provides a secondary backup of scripts and flows in your version control system, but Resources (which contain API credentials and database passwords) are stored only in the database and require database-level backups.
Worker scaling. The default compose file includes one windmill_worker and one windmill_worker_native. If you're running many concurrent jobs, you can scale workers horizontally: docker compose up -d --scale windmill_worker=4. Each worker connects to PostgreSQL independently and picks up jobs from the queue. For CPU-intensive Python or TypeScript workloads, more workers on a larger VPS is often more cost-effective than a single beefy worker.
For a full hardening reference, see the self-hosting security checklist.
Troubleshooting Common Issues
Windmill UI loads but scripts won't execute. The most common cause is workers not connecting to PostgreSQL. Check worker logs with docker compose logs windmill_worker. Workers connect directly to PostgreSQL using the DATABASE_URL variable — if the URL is wrong or the database isn't accepting connections, workers silently wait for jobs that never execute. Run docker compose exec db pg_isready -U windmill to verify the database is accepting connections.
TypeScript script fails with import errors. Windmill uses Deno for TypeScript execution, not Node.js. Deno imports use URLs rather than npm package names. Use import something from "npm:package-name@version" syntax rather than Node.js-style imports. If you're migrating scripts from Node.js, this is the first thing to fix. Check the Windmill documentation for the correct import syntax for common libraries.
Flow step showing "queued" but never running. This indicates the worker handling that job type isn't running. Windmill has two worker types: default (for most scripts) and native (for lightweight SQL and requests). Verify both windmill_worker and windmill_worker_native containers are running with docker compose ps. Also check that the job's worker group assignment matches an active worker group.
"Permission denied" when script tries to access a Resource. Resources in Windmill have their own permission model. A script can only use a Resource if the resource is shared with the script's workspace or explicitly granted. Go to Resources → the specific resource → Permissions and verify the script's user or workspace has access. This is different from the script's own execution permissions.
LSP (code completion) not working in the editor. The language server (LSP) container provides in-browser IntelliSense for Python, TypeScript, and other languages. If code completion isn't working, check whether the LSP container is running: docker compose ps lsp. The LSP service runs on port 3001 and Windmill's Caddy config proxies it. Verify the Caddyfile includes a route for the LSP websocket endpoint.
Old job logs consuming excessive disk space. Windmill stores all job logs in the worker_logs volume and in PostgreSQL. For long-running production instances, the logs table can grow large. Configure log retention in Admin Settings → Instances → Log Retention Days to automatically purge old job logs. A 30-day retention is reasonable for most teams.
Windmill is a top n8n alternative on OSSAlt — explore all open source automation platforms.
The SaaS-to-Self-Hosted Migration Guide (Free PDF)
Step-by-step: infrastructure setup, data migration, backups, and security for 15+ common SaaS replacements. Used by 300+ developers.
Join 300+ self-hosters. Unsubscribe in one click.