Windmill은 Python, TypeScript, Go, Bash 등으로 작성한 스크립트를 자동으로 웹 UI, API 엔드포인트, 워크플로우로 변환하는 오픈소스 개발자 플랫폼입니다. Retool과 Airflow의 장점을 결합한 도구로, Y Combinator 출신 스타트업이 개발했습니다. Airflow 대비 13배 빠른 워크플로우 엔진을 제공하며, 내부 도구 구축부터 데이터 파이프라인까지 다양한 자동화 작업에 활용됩니다.
Windmill이란?
Windmill은 “스크립트를 프로덕션 등급 내부 앱으로 전환”하는 것을 목표로 하는 오픈소스 플랫폼입니다. 개발자가 간단한 스크립트를 작성하면, Windmill이 자동으로 입력 폼 UI를 생성하고, API 엔드포인트로 노출하며, 스케줄링과 워크플로우 연결을 지원합니다.
핵심 컨셉
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 스크립트 작성 │ ──▶ │ 자동 UI 생성 │ ──▶ │ 워크플로우 연결 │
│ Python/TS/Go... │ │ 입력 폼 생성 │ │ Flow 빌더 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 앱 빌더 (선택) │
│ 대시보드 구축 │
└─────────────────┘
왜 Windmill인가?
| 특징 | Windmill | Retool | Airflow |
|---|---|---|---|
| 오픈소스 | ✅ 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_URL | PostgreSQL 연결 URL | 필수 |
MODE | 실행 모드 (server/worker/standalone) | standalone |
BASE_URL | 외부 접속 URL | http://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 구축
도구 비교
| 기능 | Windmill | n8n | Retool | Airflow |
|---|---|---|---|---|
| 코드 기반 | ✅ | ⚠️ | ⚠️ | ✅ |
| UI 자동생성 | ✅ | ❌ | ❌ | ❌ |
| 앱 빌더 | ✅ | ❌ | ✅ | ❌ |
| 워크플로우 | ✅ | ✅ | ⚠️ | ✅ |
| 오픈소스 | ✅ | ✅ | ❌ | ✅ |
| 성능 | 매우 빠름 | 보통 | – | 느림 |
| 학습 곡선 | 중간 | 낮음 | 낮음 | 높음 |
결론
Windmill은 다음과 같은 경우에 적합합니다:
- 개발자 중심 팀: 코드로 자동화를 구축하고 싶은 경우
- 내부 도구 필요: Retool 대신 오픈소스로 대시보드/도구 구축
- 워크플로우 + UI: 단순 자동화를 넘어 사용자 인터페이스까지 필요한 경우
- 고성능 요구: Airflow보다 빠른 실행이 필요한 경우
- 셀프호스팅 선호: 데이터 주권과 비용 절감
“스크립트 하나 작성하면 즉시 앱이 된다”는 Windmill의 철학은 개발자 생산성을 극대화합니다. 복잡한 인프라 없이 3개 파일로 시작할 수 있으니, 직접 체험해보시기 바랍니다!