Why Windmill Instead of n8n or Make in 2026
n8n 3.0 marks a turning point: Docker is now mandatory, 30+ nodes have been removed, and Code nodes can no longer read environment variables. Make remains a closed SaaS with monthly operation limits. Windmill takes the opposite approach: your code is a first-class script, versioned, unit-testable, and runnable from a terminal just as easily as from the UI. The AGPLv3 license ensures the self-hosted version is functionally identical to the cloud offering — no enterprise-only features for everyday use.
Concrete Benefits for DevOps Teams and Agencies
- Native TypeScript and Python — scripts run in a Node.js or Python worker without transpilation or intermediate node layers; npm/pip dependencies are resolved automatically by Windmill on first execution.
- Web editor with direct execution — write, test, and re-run a script from the browser in seconds, without pushing a commit or restarting any service.
- Built-in multi-tenancy — a single deployment supports multiple isolated workspaces, ideal for an agency managing several clients or a team with dev/staging/prod environments.
- Cron jobs and webhooks out of the box — scheduling a job with
0 9 * * 1-5or exposing a webhook endpoint/api/w/<workspace>/jobs/run/p/<path>is a native operation, not an add-on. - Encrypted secret variables — secrets are AES-256 encrypted at rest and never exposed in logs; each workspace has its own secrets scope.
- No vendor lock-in — all scripts, flows, and resources are exportable as JSON/YAML and importable into any other Windmill deployment.
Minimum Requirements
Windmill relies on PostgreSQL for persistence and one or more workers for execution. The HTTP server itself is lightweight; worker load determines VPS sizing. Budget 1 worker per available vCPU and approximately 1 to 2 GB of RAM per active Python worker (pip dependencies are installed in a sandboxed cache on disk).
Minimum and Recommended Configurations
- Bare minimum — 2 vCPU, 4 GB RAM, 20 GB SSD; suitable for a solo workspace with 1 worker and occasional jobs.
- Small team (recommended) — 4 vCPU, 8 GB RAM, 40 GB SSD; supports 3 concurrent workers and PostgreSQL on the same host without memory pressure.
- Team or multi-client — 8 vCPU, 16 GB RAM, 80 GB SSD; supports 6 to 8 workers, multiple active workspaces, and Python jobs with heavy dependencies.
- OS — Ubuntu 22.04 LTS or Debian 12; Docker Engine 24+ and Docker Compose v2 required.
- Network — a dedicated IPv4 and a domain name for the TLS certificate; Windmill exposes port 8000 by default behind a reverse proxy.
Docker Compose Deployment in 7 Steps
Install Docker and Docker Compose v2
On Ubuntu 22.04 or Debian 12:
curl -fsSL https://get.docker.com | sh && docker compose version. Verify the command returnsDocker Compose version v2.x.x(no hyphen). If your system still hasdocker-compose(v1), uninstall it:apt remove docker-compose.Fetch the official docker-compose.yml
Create a dedicated directory and download the reference configuration:
mkdir -p /opt/windmill && cd /opt/windmill && curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml. This file declares four services:db(PostgreSQL 16),windmill_server,windmill_worker, andlsp(code completion server).Create the .env file
Windmill requires two mandatory variables. Generate a random key:
openssl rand -hex 32. Then create/opt/windmill/.envwith:WM_BASE_URL=https://windmill.your-domain.comandWINDMILL_SECRET_KEY=<generated-key>. Never commit this file — add.envto your.gitignoreif you version your configuration.Adjust the number of workers
By default
docker-compose.ymlstarts 1 replica ofwindmill_worker. To start 3:docker compose up -d --scale windmill_worker=3. On a 4 vCPU / 8 GB VPS, 2 or 3 workers is the right balance. You can adjust live without restarting the server:docker compose up -d --no-recreate --scale windmill_worker=2.Configure Nginx as a reverse proxy with TLS
Install Nginx and Certbot:
apt install -y nginx certbot python3-certbot-nginx. Create/etc/nginx/sites-available/windmillwith aserverblock listening on port 80, aproxy_pass http://127.0.0.1:8000;directive and theX-Real-IP,X-Forwarded-For,Upgrade, andConnectionheaders (for WebSocket support). Obtain the certificate:certbot --nginx -d windmill.your-domain.com. Certbot will automatically update the vhost to listen on HTTPS.Start the stack and create the initial workspace
Launch the stack:
cd /opt/windmill && docker compose up -d. Wait 30 seconds for PostgreSQL to be ready, then openhttps://windmill.your-domain.comin a browser. The interface prompts you to create a workspace — choose a short identifier (prod,acme-client) — and set the admin account. This account is local to Windmill; you can connect an SSO provider (GitHub, OIDC) fromSettings → Authentication.Enable automatic restarts and verify logs
Add the restart policy to the stack: in
docker-compose.yml, ensure each service hasrestart: unless-stopped. Reload if needed:docker compose up -d. Check the server logs:docker compose logs -f windmill_server. The lineWindmill server started on port 8000confirms the instance is running. For workers:docker compose logs -f windmill_worker.
Configuration: Workspace, Secret Variables, and Crons
Once the instance is running, three configuration areas should be mastered before creating your first workflows.
Essential Post-Installation Settings
- Workspace and permissions — in
Settings → Workspace, set the timezone (used for crons) and enableAuto-approve executionsonly if all members are trusted. By default, scripts created by non-admin users enter a review queue before production execution. - Variables and resources —
Variablesstores AES-256 encrypted scalar keys (tokens, URLs).Resourcesstores typed structured objects (database connections, OAuth credentials) with a JSON schema. Prefer resources for anything with multiple fields: fewer copy-paste errors. - Schedules (crons) — in the script editor, click
Add schedule. The accepted syntax is standard 5-field cron format. The per-workspace timezone applies automatically:0 8 * * 1-5triggers at 8:00 AM workspace time, not UTC. - Webhooks — each script exposes an endpoint of the form
/api/w/<workspace>/jobs/run/p/<script-path>. Authenticate incoming calls with a token generated inSettings → Tokens. Pass script parameters as a JSON POST body. - Git Sync — in
Settings → Git Sync, connect a Git repository to automatically push all scripts, flows, and resources on every change. Ideal for traceability and cross-environment deployment.
Complete Python Workflow Example
Here is a concrete Python script: it queries an external API, filters the results, and inserts rows into a PostgreSQL database via a Windmill resource. Create a new Python 3.11 script, paste the code below, and run it directly from the editor to validate.
Python Script: API Import → Filter → PostgreSQL
- Dependency declaration —
# requirements:lines at the top of the script tell Windmill which pip packages to install in the isolated sandbox:# requirements:\n# httpx\n# psycopg2-binary. Windmill caches these dependencies between executions. - Main function signature — Windmill always calls the
def main(...)function. Parameters are injected from the UI, the cron, or the webhook body:def main(api_url: str, db: dict) -> dict:. Thedicttype fordbcorresponds to a PostgreSQL resource. - HTTP call with httpx —
import httpx; resp = httpx.get(api_url, timeout=10); resp.raise_for_status(); data = resp.json(). Windmill captures unhandled exceptions and displays them in the execution log with a full stack trace. - Filtering and insertion —
rows = [r for r in data if r.get('active')]; conn = psycopg2.connect(**db); cur = conn.cursor(); cur.executemany('INSERT INTO items(id, name) VALUES (%(id)s, %(name)s) ON CONFLICT DO NOTHING', rows); conn.commit(). Thedbresource is passed as a Python dict — keys matchpsycopg2.connectparameters. - Return value —
return {'inserted': len(rows), 'source': api_url}. The returned value is displayed in the UI and accessible to subsequent steps in a Flow.
Hardening and Security
Restrict network access to the VPS: block port 8000 in UFW (ufw deny 8000/tcp) — only Nginx should reach it via localhost. Enable security headers in Nginx: add_header X-Frame-Options SAMEORIGIN; add_header X-Content-Type-Options nosniff; add_header Referrer-Policy strict-origin-when-cross-origin. Configure PostgreSQL backups: docker exec windmill-db-1 pg_dump -U windmill windmill | gzip > /backups/windmill-$(date +%Y%m%d).sql.gz. Schedule this dump with a systemd cron — or a Windmill job itself. Enable password policy in Settings → Security and enforce 2FA for admin accounts. Restrict worker permissions: Windmill supports Worker Groups with restricted Docker images for untrusted scripts.
Troubleshooting — Common Errors
Here are the five most common problems encountered during a first Windmill deployment, with their real causes and fixes.
5 Real Errors and Their Fixes
connection refusedat server startup — thewindmill_servercontainer starts before PostgreSQL is ready. Add ahealthcheckto thedbservice and adepends_on: db: condition: service_healthyclause onwindmill_server. Without this, the server attempts to connect to PostgreSQL during initialization and gives up. Check:docker compose logs db | grep 'ready to accept'.- Workers that stay
IDLEindefinitely — classic cause: theDATABASE_URLvariable in worker environments points tolocalhostinstead of the Docker service name (db). Indocker-compose.yml, the correct value ispostgresql://windmill:windmill@db/windmill.localhostfrom inside a container does not resolve the stack's PostgreSQL service. - Python scripts failing with
ModuleNotFoundError— Windmill resolves pip dependencies on the first script run via a local cache. If the worker has no internet access (restrictive firewall), the silent installation fails. Check:docker exec <worker-id> pip install httpx. Open outbound port 443 forpypi.organdfiles.pythonhosted.org, or mount an internal pip mirror via thePIP_INDEX_URLvariable. - Webhook returns 403 Forbidden — the token passed in the
Authorization: Bearer <token>header is correct, but the script is inRequire approvalmode. Disable this in the script editor (Settings → Execution policy → Direct execution) or use a service token with theexecutorrole. - Cron that does not trigger at the expected time — the workspace timezone defaults to UTC. If your workspace is in
Africa/CasablancaorEurope/Paris, a0 9 * * *expression triggers at 9:00 AM workspace time. Verify the timezone inSettings → Workspace → Timezoneand restart the scheduler:docker compose restart windmill_server.
Toward Production: Final Checklist
Before exposing Windmill to real traffic or critical jobs, go through this list. It covers the points most commonly overlooked during a first deployment.
Validation Points Before Going Live
- Valid TLS and HSTS — verify that
https://windmill.your-domain.comresponds without certificate warnings and addStrict-Transport-Security: max-age=31536000to the Nginx vhost. - Tested backups — make a PostgreSQL dump, restore it in a test container, and verify that the Windmill interface connects. An untested backup does not exist.
- Worker monitoring — the
Admin → Workerspage shows real-time status. Set up an alert (Grafana, UptimeRobot, or a Windmill job itself) if the number of active workers drops below 1. - Secret rotation — Windmill variables are encrypted with
WINDMILL_SECRET_KEY. If you lose this key, secrets encrypted in the database are unrecoverable. Back up the key in a separate secrets manager (Vaultwarden, Bitwarden) off the VPS. - Pinned version in docker-compose.yml — replace
image: ghcr.io/windmill-labs/windmill:latestwithimage: ghcr.io/windmill-labs/windmill:1.830.0to avoid uncontrolled automatic updates. Update deliberately by consulting the CHANGELOG. - Git Sync enabled — connect a private repository to version all your scripts and flows. If you lose the VPS, restore the database and reimport from Git.
Windmill vs n8n vs Temporal
Scroll the table
| Criterion | Windmill 1.830 | n8n 3.x | Temporal Cloud/Self-hosted |
|---|---|---|---|
| Workflow language | TypeScript, Python, Go, Bash | JavaScript (nodes) + limited Code nodes | Go, Java, Python, TypeScript via SDK |
| Deployment model | Docker Compose, single image | Docker required since v3.0 | Docker or Temporal Cloud SaaS |
| License | AGPLv3 (full self-host) | Sustainable Use License (agency restrictions) | MIT (core) + paid cloud offering |
| Secret variables (self-hosted) | Included, AES-256 encrypted | Included | Included (Temporal Namespace) |
| Built-in web editor | Yes — full IDE with autocompletion | Yes — visual canvas | No — local development only |
| Multi-tenancy | Yes — native isolated workspaces | Partial — separate instances recommended | Yes — Temporal Namespaces |
| Minimum RAM | 4 GB (1 worker + PostgreSQL) | 2 GB (SQLite), 4 GB (PostgreSQL) | 4 GB (self-hosted) / N/A (cloud) |
| Max workflow duration | Unrestricted (long-running jobs) | Limited by node timeout | Unrestricted (activity durability) |
| Learning curve | Low — standard Python/TS code | Low — visual canvas | High — activities/signals model |