Self-Hosting Windmill on Ubuntu: Complete Docker Setup Tutorial with PostgreSQL Troubleshooting

Workflow automation platforms are essential for modern development teams, but cloud solutions like Windmill Cloud can become expensive as usage grows. We’ll show you how to set up your own Windmill instance on Ubuntu with Docker Compose and Traefik integration, overcoming critical PostgreSQL authentication issues that can derail your installation.

What You’ll Build

By the end of this tutorial, you’ll have:

  • ✅ Fully functional Windmill installation with HTTPS
  • ✅ Automatic SSL certificates via Let’s Encrypt through Traefik
  • ✅ Production-ready PostgreSQL database with proper authentication
  • ✅ Resource-optimized worker configuration
  • ✅ Integrated with existing Docker infrastructure
  • ✅ Production-ready setup for professional workflow automation

Monthly cost: €4.51 (CX11 server) + domain costs – same infrastructure that can handle multiple automation tools

Prerequisites

  • Ubuntu 24.04 LTS server with Docker and Docker Compose installed
  • Existing Traefik reverse proxy setup (see our n8n setup guide for Traefik configuration)
  • Domain name pointing to your server IP
  • At least 4GB RAM and 2 vCPUs recommended
  • SSH access and basic command line knowledge

Understanding Windmill

Windmill is an open-source workflow engine that provides:

  • Visual workflow editor with TypeScript/Python/Go support
  • Job scheduling and execution management
  • API integration capabilities
  • Team collaboration features
  • Self-hostable with no usage limits

Unlike n8n’s node-based approach, Windmill focuses on code-first workflows with a powerful development environment.

Step 1: Server Preparation and Directory Structure

First, let’s prepare our server environment. We’ll use a food-based naming convention for Windmill instances to avoid conflicts:

# Create Windmill directory (first instance: "pizza")
mkdir -p /opt/windmill-pizza
cd /opt/windmill-pizza

# Create required subdirectories
mkdir -p postgres-data windmill-data lsp-cache

# Verify directory structure
ls -la

Naming Convention: Use simple food names for multiple Windmill instances:

  • First instance: pizza
  • Additional instances: pastasaladsoupburger, etc.
  • This avoids conflicts and makes management easier

Step 2: Environment Configuration

Create a secure environment file with proper credentials:

# Generate a secure hex password (no special characters!)
SECURE_PASSWORD=$(openssl rand -hex 16)
echo "Generated password: $SECURE_PASSWORD"

# Create environment file
cat > /opt/windmill-pizza/.env << EOF
# Windmill Image Version
WM_IMAGE=ghcr.io/windmill-labs/windmill:main

# Database Configuration - SECURE PASSWORDS
DATABASE_URL=postgresql://postgres:${SECURE_PASSWORD}@windmill-db:5432/windmill_pizza?sslmode=disable
POSTGRES_PASSWORD=${SECURE_PASSWORD}
POSTGRES_DB=windmill_pizza
POSTGRES_USER=postgres

# Windmill Configuration
BASE_URL=https://windmill.yourdomain.com
RUST_LOG=info

# Worker Configuration
WORKER_GROUP=default
KEEP_JOB_DIR=false

# Instance Identifier
INSTANCE_NAME=pizza
EOF

Critical: Replace windmill.yourdomain.com with your actual domain!

Step 3: Docker Compose Configuration

Create the main Docker Compose configuration:

cat > /opt/windmill-pizza/docker-compose.yml << 'EOF'
version: "3.8"

services:
  # PostgreSQL Database for Windmill
  windmill-db:
    image: postgres:16
    container_name: windmill-pizza-db
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    networks:
      - proxy
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Windmill Server
  windmill-server:
    image: ${WM_IMAGE}
    container_name: windmill-pizza-server
    restart: unless-stopped
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - BASE_URL=${BASE_URL}
      - RUST_LOG=${RUST_LOG}
      - MODE=server
    networks:
      - proxy
    depends_on:
      windmill-db:
        condition: service_healthy
    volumes:
      - ./windmill-data:/tmp/windmill
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.windmill-pizza.rule=Host(`windmill.yourdomain.com`)"
      - "traefik.http.routers.windmill-pizza.entrypoints=https"
      - "traefik.http.routers.windmill-pizza.tls.certresolver=letsencrypt"
      - "traefik.http.services.windmill-pizza.loadbalancer.server.port=8000"

  # Windmill Worker
  windmill-worker:
    image: ${WM_IMAGE}
    container_name: windmill-pizza-worker
    restart: unless-stopped
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - BASE_URL=${BASE_URL}
      - RUST_LOG=${RUST_LOG}
      - MODE=worker
      - WORKER_GROUP=${WORKER_GROUP}
      - KEEP_JOB_DIR=${KEEP_JOB_DIR}
    networks:
      - proxy
    depends_on:
      windmill-db:
        condition: service_healthy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./windmill-data:/tmp/windmill
      - worker_dependency_cache:/tmp/windmill/cache
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 2G

networks:
  proxy:
    external: true

volumes:
  worker_dependency_cache:
    driver: local
EOF

Important: Update the domain in the Traefik labels to match your setup!

Step 4: The PostgreSQL Password Problem (Critical Issue)

Here’s where most Windmill installations fail, and it took considerable troubleshooting to identify the root cause:

The Problem: Special Characters in Passwords

When using openssl rand -base64 32 to generate passwords, you often get special characters like =@#%, etc. These characters cause PostgreSQL authentication failures in Docker environments, even when properly escaped.

Example of a problematic password:

# This WILL cause authentication failures:
PASSWORD="305t6m9KrChkvbyNEFLEYQ6pqAGlApn9rbJPH3D5y9g="

The Solution: Hex-Only Passwords

Use hex-only passwords that contain no special characters:

# This WORKS reliably:
PASSWORD=$(openssl rand -hex 16)
# Example result: 5781b14ec0ec1bc184653ffa5e379411

Additional PostgreSQL Configuration Issues

  1. User Configuration: Use postgres as the default user, not custom users like windmill_user
  2. Volume Persistence: PostgreSQL ignores POSTGRES_PASSWORD environment variables when existing data volumes contain different credentials
  3. URL Format: Include ?sslmode=disable in the database URL for Docker environments

Step 5: Installation and Startup

Now let’s install Windmill with our corrected configuration:

cd /opt/windmill-pizza

# Verify configuration syntax
docker compose config --quiet

# Pull images
docker compose pull

# Start services
docker compose up -d

# Check status
docker compose ps

You should see output like:

NAME                    STATUS                    PORTS
windmill-pizza-db       Up (healthy)              5432/tcp
windmill-pizza-server   Up                        8000/tcp
windmill-pizza-worker   Up                        8000/tcp

Step 6: Troubleshooting Common Issues

Issue 1: PostgreSQL Authentication Failures

Symptoms:

windmill-pizza-server | Error: password authentication failed for user "postgres"

Solution:

# Stop containers
docker compose down --volumes

# Remove old data
rm -rf postgres-data/*

# Regenerate hex password
NEW_PASSWORD=$(openssl rand -hex 16)
sed -i "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$NEW_PASSWORD/" .env
sed -i "s/:.*@/:$NEW_PASSWORD@/" .env

# Restart
docker compose up -d

Issue 2: Container Won’t Start

Symptoms:

  • Container exits immediately
  • Resource allocation errors

Solution:

# Check container logs
docker logs windmill-pizza-server
docker logs windmill-pizza-db

# Check system resources
docker stats
free -h

Issue 3: SSL Certificate Issues

Symptoms:

  • HTTPS not working
  • Certificate errors

Solution:

# Check Traefik logs
docker logs traefik

# Verify DNS resolution
nslookup windmill.yourdomain.com

# Restart Traefik if needed
docker restart traefik

Step 7: Access and Initial Setup

Once installation is complete:

  1. Access Windmill: https://windmill.yourdomain.com
  2. Default credentials:
    • Email: admin@windmill.dev
    • Password: changeme
  3. Complete setup:
    • Change admin password
    • Configure base URL
    • Set up user accounts

Step 8: Resource Optimization

Memory Allocation (for 8GB server)

Our configuration allocates resources efficiently:

  • Windmill Server: ~800MB
  • Windmill Worker: 2GB (limited)
  • PostgreSQL: ~500MB
  • System Reserve: ~4.7GB

CPU Allocation (for 4 vCPU server)

  • Worker: 1 vCPU (limited)
  • Other services: 3 vCPUs (shared)

Scaling rule: 1 worker per vCPU with 1-2GB RAM each

Step 9: Production Hardening

Create Backup Script

cat > /opt/windmill-pizza/backup.sh << 'EOF'
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p /opt/backups
docker compose exec windmill-db pg_dump -U postgres windmill_pizza > /opt/backups/windmill_pizza_${DATE}.sql
echo "Backup created: windmill_pizza_${DATE}.sql"
EOF

chmod +x /opt/windmill-pizza/backup.sh

Set Up Monitoring

# Monitor container health
docker compose ps

# Check resource usage
docker stats

# Monitor logs
docker compose logs -f windmill-server

Configure Automated Updates

cat > /opt/windmill-pizza/update.sh << 'EOF'
#!/bin/bash
cd /opt/windmill-pizza
docker compose pull
docker compose up -d --force-recreate
EOF

chmod +x /opt/windmill-pizza/update.sh

Step 10: Advanced Configuration

Integration with Existing SMTP

If you have a mailserver (like from our n8n setup), integrate it:

# Add to docker-compose.yml environment for windmill-server:
- SMTP_HOST=mailserver
- SMTP_PORT=25
- SMTP_USERNAME=
- SMTP_PASSWORD=
- SMTP_FROM=noreply@yourdomain.com

Multiple Windmill Instances

For teams requiring isolated environments:

# Create second instance
cp -r /opt/windmill-pizza /opt/windmill-pasta

# Update configuration
sed -i 's/pizza/pasta/g' /opt/windmill-pasta/.env
sed -i 's/windmill.yourdomain.com/pasta.yourdomain.com/g' /opt/windmill-pasta/.env

# Generate new password
NEW_PASSWORD=$(openssl rand -hex 16)
sed -i "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$NEW_PASSWORD/" /opt/windmill-pasta/.env

# Update docker-compose.yml
sed -i 's/pizza/pasta/g' /opt/windmill-pasta/docker-compose.yml
sed -i 's/windmill.yourdomain.com/pasta.yourdomain.com/g' /opt/windmill-pasta/docker-compose.yml

Security Considerations

Network Isolation

  • PostgreSQL only accessible within Docker network
  • No external database ports exposed
  • HTTPS termination at Traefik level

Resource Limits

  • Worker containers have CPU and memory limits
  • Prevents resource exhaustion attacks
  • Configurable based on server capacity

SSL Security

  • Automatic Let’s Encrypt certificates
  • HTTP to HTTPS redirects
  • Modern TLS configuration

Monitoring and Maintenance

Weekly Health Checks

# Container status
docker compose ps

# Resource usage
docker stats --no-stream

# Log analysis
docker compose logs | grep -i error

Monthly Maintenance

# Update containers
cd /opt/windmill-pizza
./update.sh

# Clean old data
docker system prune -f

# Backup database
./backup.sh

Cost Breakdown and Comparison

Monthly Costs

Self-hosted setup:

  • Hetzner CX21 (4GB RAM): €8.46/month
  • Domain costs: ~€1/month
  • Total: ~€9.50/month

Windmill Cloud comparison:

  • Team plan: $30/month per user
  • Savings: $250+ annually for small teams

Performance Benefits

Self-hosted advantages:

  • Unlimited workflow executions
  • No external rate limits
  • Full data control
  • Custom integrations
  • Resource scaling flexibility

Troubleshooting Reference

Quick Diagnostics

# Container health
docker compose ps | grep -E "(healthy|Up)"

# Network connectivity
docker network inspect proxy

# Database connection
docker compose exec windmill-db psql -U postgres -d windmill_pizza -c "SELECT version();"

# Log analysis
docker compose logs --tail 50 windmill-server | grep -E "(ERROR|WARN)"

Common Error Patterns

  1. “password authentication failed” → Use hex passwords, clear volumes
  2. “connection refused” → Check network configuration
  3. “certificate errors” → Verify DNS and Traefik setup
  4. “out of memory” → Adjust worker resource limits

Scaling Your Windmill Infrastructure

Horizontal Scaling

For high-volume environments:

# Add additional workers
windmill-worker-2:
  image: ${WM_IMAGE}
  container_name: windmill-pizza-worker-2
  environment:
    - DATABASE_URL=${DATABASE_URL}
    - MODE=worker
    - WORKER_GROUP=heavy
  deploy:
    resources:
      limits:
        cpus: '2'
        memory: 4G

Vertical Scaling

Upgrade server resources:

  • CX31 (8GB RAM): €16.07/month for heavy workloads
  • CX41 (16GB RAM): €29.75/month for enterprise usage

Integration with Existing Infrastructure

Working with n8n

If you’re already running n8n (from our previous tutorials):

  • Windmill handles code-first workflows
  • n8n handles visual, simple automations
  • Both share the same Traefik proxy
  • Separate databases prevent conflicts

Shared Services

Leverage existing infrastructure:

  • Traefik: Handles SSL for all services
  • Mailserver: Shared SMTP for notifications
  • Monitoring: Unified logging and metrics
  • Backups: Centralized backup strategy

Conclusion

Self-hosting Windmill provides enterprise-grade workflow automation at a fraction of cloud hosting costs. The key to success is understanding the PostgreSQL authentication requirements and using hex-only passwords to avoid special character issues that can derail installations.

Key Benefits of This Setup

  • Cost-effective: Save hundreds annually compared to cloud solutions
  • Production-ready: Handles enterprise workloads reliably
  • Secure: HTTPS, isolated networks, and resource limits
  • Scalable: Easy to add workers and resources as needed
  • Private: Your code and data never leave your infrastructure

This configuration has been tested in production environments and provides the reliability needed for business-critical workflow automation. The troubleshooting steps address real-world issues encountered during deployment, particularly the PostgreSQL authentication problems that affect many self-hosted installations.

For complex workflow requirements or enterprise deployments, consider professional consultation to optimize your specific use case and ensure optimal resource allocation.

Next Steps


About tva

tva ensures comprehensive infrastructure management of database systems, cloud environments, and global supply chains. Our methodical approach combines rigorous security protocols with performance optimization, while strategic advisory services enable precise coordination of both digital capabilities and physical assets – maintaining the highest standards of operational excellence and compliance throughout all engagements.

Visit tva.sg for more information about our services and additional automation tutorials.

Scroll to Top