Caption: Windmill combines a web UI, API server, workers, Postgres-backed queues, language services, and a reverse proxy into a self-hosted automation platform.
Introduction
Windmill is an open-source platform for turning scripts, APIs, forms, scheduled jobs, background tasks, and workflow graphs into reusable internal automation. It sits in the same broad category as workflow tools, cron runners, low-code back-office apps, and server-side script platforms, but its center of gravity is developer productivity. A team can write Python, TypeScript, Bash, SQL, or other supported scripts, expose them as flows or apps, schedule them, pass typed inputs, store secrets as resources, and see runs, logs, approvals, and failures in one browser-based interface.
Self-hosting Windmill is useful when automation touches private systems. The jobs you run might rotate cloud credentials, sync CRM records, provision accounts, send webhook notifications, move data between databases, or generate internal reports. Hosting the platform yourself lets you control network placement, database storage, worker permissions, audit retention, secret handling, and upgrade timing. It also gives homelab administrators and small teams a practical way to replace scattered cron scripts with a central queue and UI.
This guide installs Windmill Community Edition on Ubuntu 24.04 LTS with Docker Compose using the official Windmill Compose stack. The example uses windmill.example.com, stores the project under /opt/windmill, uses the bundled Postgres service for a single-server deployment, and exposes the application through the bundled Caddy reverse proxy. Replace example domains, email addresses, resource sizes, and passwords before using this in production.
Why Choose Windmill?
- Script-first automation: Developers can keep using normal languages while getting schedules, forms, logs, retries, flows, and permissions around them.
- Open-source control: The Community Edition can run on infrastructure you control, with the application source and deployment files available for review.
- Postgres-backed queue: Windmill stores state and job queues in Postgres, making backup planning and data ownership straightforward.
- Worker-based execution: API servers and workers are separate services, so job throughput can grow by adding or resizing workers.
- Internal tool building: Flows and apps can become lightweight admin panels, approval tools, or operational runbooks.
- Useful integrations: Windmill can call HTTP APIs, databases, cloud services, and internal systems from scripts and flows.
- Docker Compose friendly: The official single-server stack includes Postgres, server, workers, extra services, and Caddy.
Windmill is still an automation system that can execute code. Treat it with the same care you would give a CI runner, job scheduler, or internal admin panel. Worker privileges, network access, secrets, user permissions, and backups matter more than the first successful login screen.
Prerequisites
Hardware Recommendations:
- 2 vCPU and 4 GB RAM for a small personal instance or evaluation environment
- 4 vCPU and 8 GB RAM for a small team with several workers and scheduled jobs
- SSD storage for Postgres data, worker logs, dependency caches, and backups
- More CPU and memory if scripts perform scraping, report generation, data processing, or long-running API calls
- Off-server backup storage such as another VPS, NAS, S3-compatible bucket, or encrypted backup repository
Software and Accounts:
- Ubuntu 24.04 LTS server with sudo access
- A domain such as
windmill.example.com - DNS
AorAAAArecord pointing the hostname to the server - Docker Engine with the Docker Compose plugin
- curl, OpenSSL, jq, ufw, and the PostgreSQL client tools
- A password manager for generated database credentials, superadmin credentials, and recovery notes
Security Notes:
- Put Windmill behind HTTPS before using it with real credentials or internal systems.
- Change the default first-login credentials immediately and create a named administrator account.
- Do not mount the host Docker socket into workers unless every Windmill user is trusted to administer the host.
- Keep
.env, generated passwords, and backup archives private. - Limit outbound network access for the server if jobs should only reach approved internal services.
- Back up Postgres before upgrades, worker changes, or major flow migrations.
Start from a patched host with a tight firewall:
sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl ufw jq postgresql-client
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Installation Guide
This deployment follows the official Docker Compose path: download docker-compose.yml, Caddyfile, and .env, then run the stack with Docker Compose. The bundled stack includes Postgres 16, the Windmill server, default workers, a native worker, extra services for editor intelligence and debugging, and Caddy as the public reverse proxy.
1. Install Docker Engine
Install Docker and verify that the Compose v2 plugin is available:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker
docker --version
docker compose version
If docker compose version fails, fix Docker before continuing. The examples below assume the v2 plugin syntax, not the legacy docker-compose Python command.
2. Download the Official Compose Files
Create a private project directory and download the three official files from the Windmill repository:
sudo mkdir -p /opt/windmill
sudo chown "$USER":"$USER" /opt/windmill
chmod 700 /opt/windmill
cd /opt/windmill
curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile -o Caddyfile
curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/.env -o .env
chmod 600 .env
docker compose config --services
The service list should include db, windmill_server, windmill_worker, windmill_worker_native, windmill_extra, and caddy. Review the files before starting them. They are short enough to understand and should be treated as part of your operations runbook.
Caption: The official Compose stack separates Postgres state, the Windmill server, job workers, extra editor services, Caddy, volumes, and backups.
3. Set a Database Password and Public URL
The downloaded examples are intentionally easy to start, but you should replace the default database password before a real deployment. Generate a password, update both .env and the Compose file, then set Caddy to serve your hostname:
cd /opt/windmill
umask 077
WINDMILL_DB_PASSWORD="$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32)"
python3 - <<'PY'
from pathlib import Path
import os
password = os.environ["WINDMILL_DB_PASSWORD"]
env_path = Path(".env")
compose_path = Path("docker-compose.yml")
env_text = env_path.read_text()
env_text = env_text.replace("postgres:changeme@db", f"postgres:{password}@db")
env_path.write_text(env_text)
compose_text = compose_path.read_text()
compose_text = compose_text.replace("POSTGRES_PASSWORD: changeme", f"POSTGRES_PASSWORD: {password}")
compose_text = compose_text.replace('- BASE_URL=":80"', "- BASE_URL=windmill.example.com")
compose_text = compose_text.replace("# - 443:443 # Uncomment to enable HTTPS handling by Caddy", "- 443:443 # Enable HTTPS handling by Caddy")
compose_path.write_text(compose_text)
PY
docker compose config > /tmp/windmill-rendered.yml
Store the generated password in your password manager. If you prefer to terminate TLS at Cloudflare, a load balancer, Nginx, or Traefik, keep Caddy on an internal HTTP port and set the Windmill base URL in the instance settings after login. The important point is that browser traffic should use HTTPS before real secrets enter the system.
4. Start Windmill
Pull images and start the services in detached mode:
cd /opt/windmill
docker compose pull
docker compose up -d
docker compose ps
Watch startup logs until Postgres is healthy and the server has finished migrations:
docker compose logs --since=10m db windmill_server
docker compose logs --since=10m windmill_worker windmill_worker_native windmill_extra caddy
Check the HTTP health endpoint from the host:
curl -fsS http://127.0.0.1/api/health/status | jq
If you configured Caddy with a public domain, also check the HTTPS endpoint from your workstation:
curl -I https://windmill.example.com
Certificate issuance requires DNS to point at the server and ports 80 and 443 to be reachable. If the domain check fails, inspect the caddy logs before changing Windmill itself.
5. Complete First Login
Open https://windmill.example.com in a browser. Windmill's first login uses the default superadmin credentials documented by the project:
admin@windmill.dev
changeme
Use those only for bootstrap. After login, follow the instance settings flow, create a named administrator account, set the base URL to your final HTTPS URL, and store recovery details in your password manager. Then sign out and confirm the named account can log in before sharing the instance with other users.
Good first settings to review:
- Instance URL and email settings
- Workspace membership and role assignments
- Resource and secret policies
- Schedules and concurrency limits
- Worker groups for jobs that need special packages or network access
- Retention settings for logs and completed runs
6. Size Workers for the Host
The official documentation suggests one worker per vCPU with roughly 1-2 GB RAM per worker as a practical rule of thumb. The default Compose file starts multiple default workers plus a native worker. On a small VPS, reduce worker replicas or memory limits before users schedule heavy jobs:
services:
windmill_worker:
deploy:
replicas: 2
resources:
limits:
memory: 1024M
windmill_worker_native:
deploy:
replicas: 1
Apply changes with:
cd /opt/windmill
docker compose up -d
docker compose ps
Workers pull jobs from Postgres. They do not need inbound traffic from the server, which makes scaling simpler: give workers enough CPU, memory, disk cache, and network access for the scripts they run. If a script should have broader access than the default pool, create a separate worker group and assign those jobs deliberately.
Configuration
Windmill configuration has three layers: the Compose files, environment variables, and instance settings inside the application. Keep a copy of your edited Compose files under private version control or encrypted backup storage so you can rebuild the host after a failure.
Important settings to decide early:
- Public URL: Set the instance base URL to the final HTTPS hostname.
- Database: The bundled Postgres service is fine for one server; use managed Postgres for larger or high-availability deployments.
- Email triggers: The bundled Caddy configuration can forward TCP port
25to Windmill for email triggers. Open that port only if you intend to receive mail and understand your provider's mail policy. - Worker isolation: Keep the default sandboxed path for untrusted scripts. Avoid the host Docker socket for shared environments.
- Secrets: Store API keys as Windmill resources or secrets, not hard-coded script values.
- Backups: Back up Postgres data, Compose files,
.env, Caddy data if it holds certificates, and any external files your jobs depend on.
Caption: A safer Windmill deployment limits public access to HTTPS, keeps secrets in resources, runs jobs through workers, and avoids unnecessary host-level privileges.
For managed Postgres, set DATABASE_URL in .env to your provider's connection string and set the db service replicas to 0. Some managed providers do not grant PostgreSQL superuser privileges. In those environments, run Windmill's official role initialization script with a privileged database user before first startup, then grant the windmill_admin and windmill_user roles to the application user.
Usage
After the first login, create one non-destructive test workflow before connecting production systems. A simple HTTP check is enough to prove that schedules, workers, logs, and results work:
- Create a new workspace for experiments.
- Add a script that calls a harmless public endpoint or an internal health URL.
- Run it manually and inspect the logs.
- Add a schedule and confirm the next run appears in history.
- Create a resource with test credentials and confirm access controls work.
- Invite a second user with limited permissions and verify they cannot edit administrator settings.
Useful operational commands:
cd /opt/windmill
docker compose ps
docker compose logs --since=30m windmill_server
docker compose logs --since=30m windmill_worker
curl -fsS http://127.0.0.1/api/health/status | jq
When building real workflows, keep scripts small and observable. Prefer named resources over copied secrets, add explicit error messages for expected failure modes, and use worker groups when a job needs special dependencies. For example, database maintenance jobs, browser automation jobs, and cloud administration jobs should not necessarily run in the same pool.
Screenshots and Visuals
The diagrams in this guide are original architecture illustrations, not screenshots from the Windmill product UI. For production documentation, add your own internal screenshots after deployment: the instance settings page, workspace list, a sample flow, run history, worker health, and backup dashboard. Redact URLs, tokens, resource names, user emails, and customer data before sharing those screenshots outside your team.
Caption: Backups should capture Postgres, Compose files, environment settings, Caddy state, and restore verification notes.
Troubleshooting
- Browser cannot reach the site: Confirm DNS points to the server,
ufwallows ports80and443, thecaddycontainer is running, anddocker compose logs caddydoes not show ACME failures. docker compose configfails: Re-check indentation after editing YAML. Run the command before every restart so Compose catches syntax errors early.- Database migration fails on managed Postgres: Initialize Windmill's required roles with a privileged database user, grant the roles to the application user, and confirm schema permissions.
- Workers show high memory usage: Reduce replicas, lower memory limits, split heavy jobs into a separate worker group, and inspect run logs for scripts that fetch too much data at once.
- Jobs cannot reach internal systems: Check Docker networking, host firewalls, VPN routes, DNS resolution from inside the worker container, and any outbound egress rules.
- Default login still works after setup: Create and verify a named administrator account, then follow Windmill's instance setup flow so the bootstrap account is no longer the operational login.
- HTTPS works but web sockets fail: Make sure the reverse proxy forwards Windmill's extra service routes such as
/ws/*to the extra service as shown in the official Caddyfile. - Backups restore but jobs fail: Confirm
.env, resource settings, worker group names, and external credentials were restored alongside the database.
Scaling, Securing, and Next Steps
Once the single-server deployment is stable, focus on repeatability. Keep /opt/windmill/docker-compose.yml, /opt/windmill/Caddyfile, and /opt/windmill/.env in a secure backup process. Schedule database dumps, test restores on a separate host, and document the exact steps needed to rebuild the instance. A backup that has never been restored is only a guess.
A simple Postgres backup routine can run from the Docker host:
sudo mkdir -p /var/backups/windmill
sudo chmod 700 /var/backups/windmill
cd /opt/windmill
docker compose exec -T db pg_dump -U postgres windmill \
| gzip > "/var/backups/windmill/windmill-$(date +%F).sql.gz"
find /var/backups/windmill -type f -name 'windmill-*.sql.gz' -mtime +14 -delete
Copy those archives off the server with rsync, restic, Borg, rclone, or your provider's encrypted backup service. Also back up the edited Compose files and .env separately, because the database dump alone does not explain how the services were configured.
For updates, read Windmill release notes, take a backup, then pull and restart:
cd /opt/windmill
docker compose pull
docker compose up -d
docker compose ps
curl -fsS http://127.0.0.1/api/health/status | jq
By following this guide, you end with a working Windmill deployment that can run internal scripts, scheduled jobs, flows, and lightweight tools from a central self-hosted service. The next useful improvements are SSO, managed Postgres, separated worker groups for sensitive jobs, monitoring on the health endpoint, and a restore rehearsal that proves you can recover the automation platform when the host fails.