Windmill Docker 설치 가이드: 스크립트를 내부 앱과 워크플로우로 변환하는 개발자 플랫폼




Windmill은 Python, TypeScript, Go, Bash 등으로 작성한 스크립트를 자동으로 웹 UI, API 엔드포인트, 워크플로우로 변환하는 오픈소스 개발자 플랫폼입니다. Retool과 Airflow의 장점을 결합한 도구로, Y Combinator 출신 스타트업이 개발했습니다. Airflow 대비 13배 빠른 워크플로우 엔진을 제공하며, 내부 도구 구축부터 데이터 파이프라인까지 다양한 자동화 작업에 활용됩니다.


Windmill이란?

Windmill은 “스크립트를 프로덕션 등급 내부 앱으로 전환”하는 것을 목표로 하는 오픈소스 플랫폼입니다. 개발자가 간단한 스크립트를 작성하면, Windmill이 자동으로 입력 폼 UI를 생성하고, API 엔드포인트로 노출하며, 스케줄링과 워크플로우 연결을 지원합니다.

핵심 컨셉

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  스크립트 작성   │ ──▶ │   자동 UI 생성   │ ──▶ │  워크플로우 연결  │
│ Python/TS/Go... │     │   입력 폼 생성   │     │   Flow 빌더     │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                                │
                                ▼
                        ┌─────────────────┐
                        │  앱 빌더 (선택)  │
                        │  대시보드 구축   │
                        └─────────────────┘

왜 Windmill인가?

특징WindmillRetoolAirflow
오픈소스✅ AGPLv3❌ 상용✅ Apache
워크플로우⚠️ 제한적
UI 빌더✅ 강력함
스크립트→UI✅ 자동수동
성능13x 빠름기준
셀프호스팅✅ 쉬움⚠️ Enterprise✅ 복잡

주요 기능

📝 스크립트 (Scripts)

  • Python, TypeScript, Go, Bash, SQL, GraphQL, PowerShell, Rust 지원
  • 스크립트 매개변수가 자동으로 웹 UI 폼으로 변환
  • 내장 Web IDE + VS Code 확장 + GitHub 동기화
  • 종속성 자동 관리 (pip, npm 등)

🔄 플로우 (Flows)

  • 스크립트들을 연결하여 워크플로우 구성
  • 분기, 반복, 에러 핸들링 지원
  • Sub-20ms 오버헤드의 고성능 실행
  • AI 어시스턴스로 플로우 생성

🖥️ 앱 (Apps)

  • 로우코드 앱 빌더로 대시보드 구축
  • 드래그 앤 드롭 UI 컴포넌트
  • React/Svelte 프론트엔드 연결 가능
  • 스크립트와 플로우를 백엔드로 사용

⏰ 스케줄링 & 트리거

  • Cron 스케줄링
  • Webhook 트리거
  • CLI 실행
  • 이벤트 기반 실행

🔐 엔터프라이즈 기능

  • RBAC (역할 기반 접근 제어)
  • 감사 로그
  • SAML/SSO 지원 (EE)
  • Git 동기화 및 CI/CD
  • S3 기반 분산 캐시 (EE)

사전 요구사항

  • Docker 및 Docker Compose
  • 최소 2GB RAM (권장 4GB+)
  • PostgreSQL (내장 또는 외부)
  • 프로덕션: 도메인 및 HTTPS

Docker Compose 설치 방법

방법 1: 공식 빠른 시작 (3파일)

가장 간단한 공식 설치 방법입니다:

mkdir windmill && cd windmill

# 3개 파일 다운로드
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile -o Caddyfile
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/.env -o .env

# 실행
docker compose up -d

http://localhost로 접속합니다. 기본 계정: admin@windmill.dev / changeme

방법 2: 기본 설치 (PostgreSQL 포함)

version: '3.8'

services:
  db:
    image: postgres:16-alpine
    container_name: windmill-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: windmill
      POSTGRES_PASSWORD: windmill
      POSTGRES_DB: windmill
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U windmill"]
      interval: 10s
      timeout: 5s
      retries: 5

  windmill_server:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-server
    restart: unless-stopped
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://windmill:windmill@db/windmill?sslmode=disable
      - MODE=server
      - BASE_URL=http://localhost:8000
    depends_on:
      db:
        condition: service_healthy

  windmill_worker:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-worker
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://windmill:windmill@db/windmill?sslmode=disable
      - MODE=worker
      - WORKER_GROUP=default
    depends_on:
      - windmill_server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

volumes:
  postgres_data:

방법 3: Caddy 리버스 프록시 포함 (프로덕션)

version: '3.8'

services:
  db:
    image: postgres:16-alpine
    container_name: windmill-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: windmill
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-windmill_secure_password}
      POSTGRES_DB: windmill
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U windmill"]
      interval: 10s
      timeout: 5s
      retries: 5
    shm_size: 1g

  windmill_server:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-server
    restart: unless-stopped
    expose:
      - 8000
    environment:
      - DATABASE_URL=postgres://windmill:${POSTGRES_PASSWORD:-windmill_secure_password}@db/windmill?sslmode=disable
      - MODE=server
      - BASE_URL=${BASE_URL:-http://localhost}
      - RUST_LOG=info
      - NUM_WORKERS=0
    depends_on:
      db:
        condition: service_healthy

  windmill_worker:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-worker
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://windmill:${POSTGRES_PASSWORD:-windmill_secure_password}@db/windmill?sslmode=disable
      - MODE=worker
      - WORKER_GROUP=default
      - KEEP_JOB_DIR=false
    depends_on:
      - windmill_server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - worker_cache:/tmp/windmill/cache

  windmill_worker_native:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-worker-native
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://windmill:${POSTGRES_PASSWORD:-windmill_secure_password}@db/windmill?sslmode=disable
      - MODE=worker
      - WORKER_GROUP=native
    depends_on:
      - windmill_server

  caddy:
    image: caddy:2-alpine
    container_name: windmill-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - windmill_server

volumes:
  postgres_data:
  worker_cache:
  caddy_data:
  caddy_config:

Caddyfile:

{$BASE_URL:localhost} {
    reverse_proxy windmill_server:8000
}

.env:

BASE_URL=https://windmill.yourdomain.com
POSTGRES_PASSWORD=your_secure_password

방법 4: 다중 워커 (고성능)

version: '3.8'

services:
  db:
    image: postgres:16-alpine
    container_name: windmill-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: windmill
      POSTGRES_PASSWORD: windmill
      POSTGRES_DB: windmill
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U windmill"]
      interval: 10s
      timeout: 5s
      retries: 5
    command: >
      postgres
      -c max_connections=200
      -c shared_buffers=256MB

  windmill_server:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-server
    restart: unless-stopped
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://windmill:windmill@db/windmill?sslmode=disable
      - MODE=server
      - BASE_URL=http://localhost:8000

  # 기본 워커
  windmill_worker_1:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-worker-1
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://windmill:windmill@db/windmill?sslmode=disable
      - MODE=worker
      - WORKER_GROUP=default
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - worker_cache_1:/tmp/windmill/cache

  windmill_worker_2:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-worker-2
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://windmill:windmill@db/windmill?sslmode=disable
      - MODE=worker
      - WORKER_GROUP=default
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - worker_cache_2:/tmp/windmill/cache

  # 네이티브 워커 (빠른 실행)
  windmill_worker_native:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-worker-native
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://windmill:windmill@db/windmill?sslmode=disable
      - MODE=worker
      - WORKER_GROUP=native

volumes:
  postgres_data:
  worker_cache_1:
  worker_cache_2:

방법 5: Traefik 리버스 프록시 연동

version: '3.8'

services:
  db:
    image: postgres:16-alpine
    container_name: windmill-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: windmill
      POSTGRES_PASSWORD: windmill
      POSTGRES_DB: windmill
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U windmill"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - windmill

  windmill_server:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-server
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://windmill:windmill@db/windmill?sslmode=disable
      - MODE=server
      - BASE_URL=https://windmill.yourdomain.com
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.windmill.rule=Host(`windmill.yourdomain.com`)"
      - "traefik.http.routers.windmill.entrypoints=websecure"
      - "traefik.http.routers.windmill.tls=true"
      - "traefik.http.routers.windmill.tls.certresolver=letsencrypt"
      - "traefik.http.services.windmill.loadbalancer.server.port=8000"
    depends_on:
      db:
        condition: service_healthy
    networks:
      - windmill
      - traefik

  windmill_worker:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-worker
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://windmill:windmill@db/windmill?sslmode=disable
      - MODE=worker
      - WORKER_GROUP=default
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - windmill

volumes:
  postgres_data:

networks:
  windmill:
    driver: bridge
  traefik:
    external: true

샘플 스크립트 작성

Python 스크립트

# main.py
# 매개변수가 자동으로 UI 폼으로 변환됩니다

def main(
    name: str,
    count: int = 5,
    greeting: str = "Hello"
):
    """
    간단한 인사 스크립트
    
    Args:
        name: 인사할 대상 이름
        count: 인사 횟수
        greeting: 인사말
    """
    results = []
    for i in range(count):
        results.append(f"{greeting}, {name}! ({i+1}/{count})")
    
    return {"messages": results, "total": count}

TypeScript 스크립트

// main.ts
export async function main(
  url: string,
  method: "GET" | "POST" = "GET",
  body?: object
) {
  const response = await fetch(url, {
    method,
    headers: { "Content-Type": "application/json" },
    body: body ? JSON.stringify(body) : undefined,
  });
  
  return {
    status: response.status,
    data: await response.json(),
  };
}

SQL 스크립트

-- main.sql
-- $1: user_id (integer)
-- $2: status (string)

SELECT 
    id,
    name,
    email,
    created_at
FROM users
WHERE 
    ($1::integer IS NULL OR id = $1)
    AND ($2::text IS NULL OR status = $2)
ORDER BY created_at DESC
LIMIT 100;

플로우 예제 (YAML)

# ETL 파이프라인 플로우
summary: 데이터 추출-변환-로드 파이프라인

value:
  modules:
    - id: extract
      value:
        type: script
        path: f/data/extract_from_api
        input_transforms:
          api_url:
            type: static
            value: "https://api.example.com/data"
    
    - id: transform
      value:
        type: script
        path: f/data/transform_data
        input_transforms:
          raw_data:
            type: javascript
            expr: results.extract
    
    - id: load
      value:
        type: script
        path: f/data/load_to_db
        input_transforms:
          transformed_data:
            type: javascript
            expr: results.transform

환경 변수 설정

주요 환경 변수

변수명설명기본값
DATABASE_URLPostgreSQL 연결 URL필수
MODE실행 모드 (server/worker/standalone)standalone
BASE_URL외부 접속 URLhttp://localhost
WORKER_GROUP워커 그룹 이름default
NUM_WORKERS서버 내장 워커 수3
RUST_LOG로그 레벨info

워커 설정

# 워커 동시 실행 수
WORKER_CONCURRENCY=8

# 작업 타임아웃 (초)
JOB_DEFAULT_TIMEOUT=900

# 캐시 디렉토리
CACHE_DIR=/tmp/windmill/cache

# Docker 소켓 (컨테이너 실행용)
DOCKER_SOCKET=/var/run/docker.sock

보안 설정

# 비밀 암호화 키
SECRET_SALT=your_random_secret_salt

# 쿠키 도메인
COOKIE_DOMAIN=.yourdomain.com

# CORS 허용 출처
ALLOWED_ORIGIN=https://yourdomain.com

외부 PostgreSQL 사용

AWS RDS, GCP Cloud SQL 등 관리형 DB 사용 시:

version: '3.8'

services:
  windmill_server:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-server
    restart: unless-stopped
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://user:password@your-rds-endpoint.amazonaws.com:5432/windmill?sslmode=require
      - MODE=server
      - BASE_URL=https://windmill.yourdomain.com

  windmill_worker:
    image: ghcr.io/windmill-labs/windmill:latest
    container_name: windmill-worker
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://user:password@your-rds-endpoint.amazonaws.com:5432/windmill?sslmode=require
      - MODE=worker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

업그레이드 및 백업

버전 업그레이드

# 최신 이미지 풀
docker compose pull

# 재시작
docker compose down
docker compose up -d

데이터베이스 백업

# PostgreSQL 백업
docker exec windmill-db pg_dump -U windmill windmill > windmill_backup_$(date +%Y%m%d).sql

# 복원
docker exec -i windmill-db psql -U windmill windmill < windmill_backup_20250305.sql

문제 해결

서버 시작 실패

# 로그 확인
docker logs windmill-server

# DB 연결 확인
docker exec windmill-server curl -s http://localhost:8000/api/version

워커 연결 안 됨

# 워커 로그 확인
docker logs windmill-worker

# DB URL 확인
docker exec windmill-worker env | grep DATABASE_URL

스크립트 실행 실패

  • Docker 소켓 마운트 확인 (/var/run/docker.sock)
  • 워커 그룹 설정 확인
  • 종속성 설치 오류 시 캐시 삭제

사용 사례

1. 내부 도구 구축

  • 관리자 대시보드
  • 데이터 조회/수정 도구
  • 고객 지원 도구

2. 데이터 파이프라인

  • ETL 워크플로우
  • API 데이터 수집
  • 정기 리포트 생성

3. DevOps 자동화

  • 배포 파이프라인
  • 인프라 프로비저닝
  • 모니터링 알림

4. AI/ML 워크플로우

  • 모델 학습 파이프라인
  • 데이터 전처리
  • 추론 API 구축

도구 비교

기능Windmilln8nRetoolAirflow
코드 기반⚠️⚠️
UI 자동생성
앱 빌더
워크플로우⚠️
오픈소스
성능매우 빠름보통느림
학습 곡선중간낮음낮음높음

결론

Windmill은 다음과 같은 경우에 적합합니다:

  • 개발자 중심 팀: 코드로 자동화를 구축하고 싶은 경우
  • 내부 도구 필요: Retool 대신 오픈소스로 대시보드/도구 구축
  • 워크플로우 + UI: 단순 자동화를 넘어 사용자 인터페이스까지 필요한 경우
  • 고성능 요구: Airflow보다 빠른 실행이 필요한 경우
  • 셀프호스팅 선호: 데이터 주권과 비용 절감

“스크립트 하나 작성하면 즉시 앱이 된다”는 Windmill의 철학은 개발자 생산성을 극대화합니다. 복잡한 인프라 없이 3개 파일로 시작할 수 있으니, 직접 체험해보시기 바랍니다!


참고 링크




댓글 남기기