Deployment guide

Windmill on VPS: TypeScript/Python Workflows Without Lock-in

Deploy on a VPS Cloud →

Tutorial

Windmill on VPS: TypeScript/Python Workflows Without Lock-in

Automation11 min read7 steps

n8n 3.0, released in October 2026, makes Docker mandatory and removes over 30 legacy nodes. For teams evaluating alternatives, Windmill stands out: write native TypeScript or Python, run scripts directly from the web editor, and orchestrate dozens of workers on a single VPS. No paid plan required for secret variables, no proprietary nodes — just code versioned in a Git-backed workspace. This guide gets you to a production-ready Windmill v1.830 instance in under an hour.

Contents· Why Windmill Instead of n8n or Make in 20261/15
  1. 01Why Windmill Instead of n8n or Make in 2026
  2. 02Concrete Benefits for DevOps Teams and Agencies
  3. 03Minimum Requirements
  4. 04Minimum and Recommended Configurations
  5. 05Docker Compose Deployment in 7 Steps
  6. 06Configuration: Workspace, Secret Variables, and Crons
  7. 07Essential Post-Installation Settings
  8. 08Complete Python Workflow Example
  9. 09Python Script: API Import → Filter → PostgreSQL
  10. 10Hardening and Security
  11. 11Troubleshooting — Common Errors
  12. 125 Real Errors and Their Fixes
  13. 13Toward Production: Final Checklist
  14. 14Validation Points Before Going Live
  15. 15Windmill vs n8n vs Temporal

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-5 or 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

  1. 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 returns Docker Compose version v2.x.x (no hyphen). If your system still has docker-compose (v1), uninstall it: apt remove docker-compose.

  2. 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, and lsp (code completion server).

  3. Create the .env file

    Windmill requires two mandatory variables. Generate a random key: openssl rand -hex 32. Then create /opt/windmill/.env with: WM_BASE_URL=https://windmill.your-domain.com and WINDMILL_SECRET_KEY=<generated-key>. Never commit this file — add .env to your .gitignore if you version your configuration.

  4. Adjust the number of workers

    By default docker-compose.yml starts 1 replica of windmill_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.

  5. 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/windmill with a server block listening on port 80, a proxy_pass http://127.0.0.1:8000; directive and the X-Real-IP, X-Forwarded-For, Upgrade, and Connection headers (for WebSocket support). Obtain the certificate: certbot --nginx -d windmill.your-domain.com. Certbot will automatically update the vhost to listen on HTTPS.

  6. 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 open https://windmill.your-domain.com in 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) from Settings → Authentication.

  7. Enable automatic restarts and verify logs

    Add the restart policy to the stack: in docker-compose.yml, ensure each service has restart: unless-stopped. Reload if needed: docker compose up -d. Check the server logs: docker compose logs -f windmill_server. The line Windmill server started on port 8000 confirms 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 enable Auto-approve executions only if all members are trusted. By default, scripts created by non-admin users enter a review queue before production execution.
  • Variables and resources — Variables stores AES-256 encrypted scalar keys (tokens, URLs). Resources stores 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-5 triggers 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 in Settings → 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:. The dict type for db corresponds 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(). The db resource is passed as a Python dict — keys match psycopg2.connect parameters.
  • 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 refused at server startup — the windmill_server container starts before PostgreSQL is ready. Add a healthcheck to the db service and a depends_on: db: condition: service_healthy clause on windmill_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 IDLE indefinitely — classic cause: the DATABASE_URL variable in worker environments points to localhost instead of the Docker service name (db). In docker-compose.yml, the correct value is postgresql://windmill:windmill@db/windmill. localhost from 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 for pypi.org and files.pythonhosted.org, or mount an internal pip mirror via the PIP_INDEX_URL variable.
  • Webhook returns 403 Forbidden — the token passed in the Authorization: Bearer <token> header is correct, but the script is in Require approval mode. Disable this in the script editor (Settings → Execution policy → Direct execution) or use a service token with the executor role.
  • Cron that does not trigger at the expected time — the workspace timezone defaults to UTC. If your workspace is in Africa/Casablanca or Europe/Paris, a 0 9 * * * expression triggers at 9:00 AM workspace time. Verify the timezone in Settings → Workspace → Timezone and 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.com responds without certificate warnings and add Strict-Transport-Security: max-age=31536000 to 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 → Workers page 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:latest with image: ghcr.io/windmill-labs/windmill:1.830.0 to 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

CriterionWindmill 1.830n8n 3.xTemporal Cloud/Self-hosted
Workflow languageTypeScript, Python, Go, BashJavaScript (nodes) + limited Code nodesGo, Java, Python, TypeScript via SDK
Deployment modelDocker Compose, single imageDocker required since v3.0Docker or Temporal Cloud SaaS
LicenseAGPLv3 (full self-host)Sustainable Use License (agency restrictions)MIT (core) + paid cloud offering
Secret variables (self-hosted)Included, AES-256 encryptedIncludedIncluded (Temporal Namespace)
Built-in web editorYes — full IDE with autocompletionYes — visual canvasNo — local development only
Multi-tenancyYes — native isolated workspacesPartial — separate instances recommendedYes — Temporal Namespaces
Minimum RAM4 GB (1 worker + PostgreSQL)2 GB (SQLite), 4 GB (PostgreSQL)4 GB (self-hosted) / N/A (cloud)
Max workflow durationUnrestricted (long-running jobs)Limited by node timeoutUnrestricted (activity durability)
Learning curveLow — standard Python/TS codeLow — visual canvasHigh — activities/signals model

A VPS Ready for Windmill in Minutes

ServOrbit offers VPS with root access, dedicated IPv4, and a choice of Ubuntu/Debian OS. Get your Windmill Docker Compose stack running from the first SSH login.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab