Self-Hosting Windmill: A Complete Guide to Running an Open-Source Workflow Automation Platform

Windmill is an open-source developer platform for workflows, scheduled jobs, scripts, internal tools, and automation APIs. This guide walks through a practical Docker Compose deployment on Ubuntu 24.04 LTS with Caddy, Postgres, worker sizing, security settings, backups, and operational checks.

 Windmill self-hosted workflow automation platform

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 A or AAAA record 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.

 Windmill Docker Compose stack

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 25 to 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.

 Windmill worker and security flow

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:

  1. Create a new workspace for experiments.
  2. Add a script that calls a harmless public endpoint or an internal health URL.
  3. Run it manually and inspect the logs.
  4. Add a schedule and confirm the next run appears in history.
  5. Create a resource with test credentials and confirm access controls work.
  6. 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.

 Windmill backup and restore plan

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, ufw allows ports 80 and 443, the caddy container is running, and docker compose logs caddy does not show ACME failures.
  • docker compose config fails: 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.

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.