The best ways to host Windmill in 2026
Windmill is an open-source workflow engine and internal-tooling platform — you write scripts in Python, TypeScript, Go, Bash, or SQL, and it turns them into UIs, APIs, and scheduled jobs. Self-hosting is a first-class path, and the deployment is genuinely simple: a server, a Postgres database, and one or more workers.
The simplicity hides one decision worth thinking about carefully. The workers execute arbitrary code that anyone with access to your Windmill instance wrote. Where and how you run those workers is a security and capacity question, not a deployment detail — and it's the part most self-hosting guides skip.
What you're deploying
- Server — the API, web UI, and job scheduler. Stateless, so scaling it is easy.
- Postgres — the whole state of the system: scripts, flows, job queue, results, permissions. Windmill uses Postgres as its queue too, so the database is both your durability story and a performance-sensitive component.
- Workers — pull jobs from the queue and execute them. This is where your scripts actually run, and where the interesting decisions live.
- Optionally a dedicated native/report worker pool, and object storage for larger artifacts.
The worker question
Windmill runs your team's scripts. Those scripts have credentials, reach your internal network, and are written by whoever has access to your workspace. That's a very different threat model from a web app, and there are two failure modes worth separating.
The first is accidental. Someone writes a script with an infinite loop, or one that allocates until the machine dies, or one that leaves a process running. On a shared worker, that takes out every job on the same worker, including unrelated ones. This happens routinely and it's the common case.
The second is deliberate, and only applies if your Windmill instance is reachable by people you don't fully trust — contractors, a wide internal user base, or anything customer-facing. Then a worker becomes a place where someone can run arbitrary code inside your network. Windmill has its own sandboxing options for scripts, and they're worth enabling, but the strength of the boundary underneath still matters.
So the practical question when choosing a host: how isolated is a worker, and how much does it cost you when one goes bad?
The options
1. Docker Compose on one VM
The documented starting point and completely fine for a small team. Server, Postgres, and a couple of workers on one machine, up in minutes.
What you're accepting is that a runaway script competes with your database for the same CPU and memory. That's the specific way this setup fails: someone runs a heavy job, Postgres gets starved, and the whole instance becomes unresponsive rather than just that one job being slow. Setting worker resource limits in compose is worth doing on day one, not after the first incident.
2. Split: server and database managed, workers separate
The first real improvement, and it fixes the failure above. Put Postgres on a managed instance and run workers on their own machines. Now a runaway script degrades a worker rather than the platform, and your database gets backups and failover from someone whose job that is.
It also lets you scale the two independently, which matters because they scale on completely different signals — workers on job volume, database on query load.
3. Kubernetes
Windmill publishes a Helm chart and this is a good fit if you already run Kubernetes. Workers become a deployment you scale, resource limits are native, and you can run separate worker pools with different tags for different job types — a heavy pool for reports, a lighter one for quick scripts.
The usual caveat applies: worth it if the cluster exists. Pod-level isolation is still a shared kernel, so it fixes the noisy-neighbour problem well and the untrusted-code problem only partially.
4. MicroVM-isolated workers
The approach I'd argue for if your scripts are genuinely untrusted, and where our platform fits. Instead of workers as long-lived containers, each job — or each worker — runs in a hardware-isolated microVM with its own kernel.
What that changes: a script that exhausts memory, forks endlessly, or tries to escape affects one VM that gets destroyed. There's no shared kernel between jobs, so the isolation boundary is the hypervisor rather than container namespaces. And because a snapshot restore takes roughly 180ms, a fresh environment per job is affordable rather than something you avoid for performance reasons.
The honest trade is complexity. This is more moving parts than running workers as containers, and for a trusted internal team it's more isolation than the situation calls for. If five colleagues write all your scripts, container workers with resource limits are the proportionate answer and you should do that instead.
Sizing and operational notes
- Worker count is about concurrency, not throughput. Each worker runs a bounded number of jobs at once; if your queue backs up during peaks, add workers rather than bigger ones.
- Use worker tags to separate job classes. A pool for long-running heavy jobs and another for quick interactive ones stops a report generation from delaying a button click.
- Watch job result retention. Results live in Postgres and accumulate faster than people expect, particularly with frequent scheduled flows. Set a retention policy early.
- Dependency installation is a real cost. Scripts that install packages at runtime pay that on every cold worker; pre-baking common dependencies into your worker image is usually the single biggest latency improvement available.
- Back up Postgres and test the restore. Every script, flow, schedule, and permission lives there — it isn't just a queue.
# Before sizing workers, look at where queue time actually goes.
# Long waiting times mean too few workers; long running times mean
# the scripts themselves (often dependency installs) are the problem.
psql "$WINDMILL_DATABASE_URL" -c "
SELECT
date_trunc('hour', created_at) AS hour,
count(*) AS jobs,
round(avg(extract(epoch FROM (started_at - created_at)))::numeric, 2) AS avg_wait_s,
round(avg(extract(epoch FROM (duration_ms/1000.0)))::numeric, 2) AS avg_run_s
FROM completed_job
WHERE created_at > now() - interval '7 days'
GROUP BY 1 ORDER BY 1 DESC LIMIT 24;"Check the column names against your Windmill version before running that — the schema does change between releases.
Choosing
- Small trusted team, internal tooling: Docker Compose on one VM, with worker resource limits set.
- Production internal platform: managed Postgres, workers on separate machines, scaled independently.
- Already on Kubernetes: the Helm chart, with tagged worker pools by job class.
- Scripts written by people you don't fully trust, or a customer-facing surface: microVM-isolated workers, where a bad job destroys only its own environment.
The short version
Windmill's server and database will run happily almost anywhere — that part is a solved deployment. The decision that matters is where the workers run, because they execute arbitrary code and their blast radius is whatever you put next to them.
Start by getting Postgres off the worker machine. That single change fixes the most common way self-hosted Windmill falls over. Escalate to hardware isolation when the people writing scripts are people you'd rather not implicitly trust with your network.
Frequently asked questions
What do you need to self-host Windmill?
A stateless server (API, UI, scheduler), a Postgres database, and one or more workers that execute jobs. Optionally dedicated worker pools and object storage for artifacts. The key architectural detail is that Postgres is also the job queue, so it's performance-sensitive under high job throughput — size it for your queue volume, not just your data size.
How should Windmill workers be isolated?
It depends who writes the scripts. For a small trusted team, container workers with resource limits are proportionate — the main risk is accidental, like a runaway loop starving neighbours. If scripts come from people you don't fully trust or from a customer-facing surface, workers become a place to run arbitrary code inside your network, and a hardware boundary like a microVM is worth the extra complexity.
Can I run Windmill on a single VM?
Yes, and the documented Docker Compose setup makes it quick. The characteristic failure is that a heavy script competes with Postgres for the same CPU and memory, so one bad job makes the whole instance unresponsive rather than just being slow itself. Set worker resource limits from the start, and move Postgres off that machine as soon as the instance matters.
How many Windmill workers do I need?
Workers govern concurrency rather than raw throughput — each runs a bounded number of jobs simultaneously. If your queue backs up at peaks, add more workers rather than larger ones. Use worker tags to separate job classes so long-running reports don't delay quick interactive scripts.
Why are my Windmill jobs slow to start?
Most often runtime dependency installation. Scripts that install packages on each execution pay that cost on every cold worker. Pre-baking common dependencies into the worker image is usually the largest single improvement. Distinguish queue wait time from execution time first — long waits mean too few workers, long runs point at the scripts themselves.
Keep reading
- Sandboxes — Hardware-isolated environments for running arbitrary scripts.
- Managed Postgres — Getting the database off your worker machine.
- How to run a job queue without a worker fleet — The same pattern, built directly on Postgres.
- Running user-defined functions safely — When the code comes from someone else.
49ms p50 cold start. Fork, snapshot, and scale to zero.