Sau ba tháng chạy n8n trong HomeLab, tôi đụng phải một bức tường. Không phải loại nhỏ — mà là loại khiến bạn ngồi đến 2 giờ sáng nhìn chằm chằm vào một Function node, cố hiểu tại sao đoạn JavaScript 120 dòng của mình lại không lưu dữ liệu giữa các lần thực thi như kỳ vọng.
n8n hoạt động rất tốt cho các kết nối API đơn giản. Nhưng khi workflow trở nên phức tạp — nhiều dependency Python, các hàm tiện ích dùng chung, xử lý lỗi thực sự — thì GUI lại trở thành gánh nặng. Bạn cuối cùng phải viết code trong một textarea không có linting, không có git history, và không có cách nào test từng bước riêng lẻ mà không phải chạy toàn bộ flow.
Đó là lúc tôi tìm thấy Windmill.
Windmill Thực Sự Là Gì
Windmill là một nền tảng tự động hóa workflow có thể tự host, nhưng nó tiếp cận vấn đề theo hướng ngược lại so với n8n. Thay vì “xây dựng workflow trực quan, tùy chọn thêm code,” Windmill coi script của bạn là công dân hạng nhất.
Mỗi script trong Windmill là một file thực sự — Python, TypeScript, Go, hoặc Bash — được lưu trong workspace có git backup. Bạn viết code thực sự trong một web IDE hỗ trợ LSP (Language Server Protocol). Autocomplete hoạt động. Type checking hoạt động. Bạn có thể test script độc lập mà không cần đụng đến phần còn lại của flow.
Các Khái Niệm Cơ Bản Trước Khi Deploy
- Scripts: Các hàm có kiểu dữ liệu riêng lẻ (Python
def, TypeScriptexport default function) nhận đầu vào và trả về đầu ra có kiểu - Flows: Chuỗi script trực quan — mỗi bước là một file script thực sự, không phải đoạn code nhúng inline
- Schedules: Trigger kiểu cron gắn với bất kỳ script hoặc flow nào
- Webhooks: HTTP endpoint được tự động tạo cho mỗi script, sẵn sàng ngay lập tức
- Variables & Secrets: Quản lý secret tập trung với tham chiếu an toàn kiểu dữ liệu trong code
- Workers: Các container riêng biệt thực thi script — scale độc lập khi tải tăng
Sự thay đổi tư duy: trong n8n, bạn xây workflow và nhúng code vào trong đó. Trong Windmill, bạn viết script và kết nối chúng thành flow. Nếu bạn đã quen tư duy theo hàm và giá trị trả về có kiểu, bạn sẽ cảm thấy thoải mái ngay trong hai mươi phút đầu.
Deploy Windmill với Docker Compose
Bạn cần cài Docker và Docker Compose. Nếu bạn đang chạy các dịch vụ HomeLab khác, hẳn bạn đã có sẵn rồi. Hãy dự trù ít nhất 2 GB RAM — mỗi container server, worker và LSP đều có overhead đáng kể, và database cần thêm không gian để hoạt động.
Tạo thư mục làm việc:
mkdir -p ~/homelab/windmill && cd ~/homelab/windmill
Tạo docker-compose.yml:
version: "3.7"
services:
db:
image: postgres:16
shm_size: 128mb
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_USER: windmill
POSTGRES_DB: windmill
healthcheck:
test: ["CMD-SHELL", "pg_isready -U windmill"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
windmill_server:
image: ghcr.io/windmill-labs/windmill:main
restart: unless-stopped
expose:
- 8000
environment:
- DATABASE_URL=postgres://windmill:changeme@db/windmill
- MODE=server
depends_on:
db:
condition: service_healthy
volumes:
- windmill_cache:/tmp/windmill/cache
windmill_worker:
image: ghcr.io/windmill-labs/windmill:main
restart: unless-stopped
environment:
- DATABASE_URL=postgres://windmill:changeme@db/windmill
- MODE=worker
- WORKER_GROUP=default
depends_on:
db:
condition: service_healthy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- windmill_cache:/tmp/windmill/cache
windmill_worker_native:
image: ghcr.io/windmill-labs/windmill:main
restart: unless-stopped
environment:
- DATABASE_URL=postgres://windmill:changeme@db/windmill
- MODE=worker
- WORKER_GROUP=native
depends_on:
db:
condition: service_healthy
lsp:
image: ghcr.io/windmill-labs/windmill-lsp:latest
restart: unless-stopped
expose:
- 3001
caddy:
image: caddy:2.7.6-alpine
restart: unless-stopped
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
ports:
- "3000:80"
environment:
- BASE_URL=":80"
volumes:
db_data: {}
windmill_cache: {}
Tạo Caddyfile cho reverse proxy:
:80 {
bind 0.0.0.0
reverse_proxy /ws/* windmill_server:8000
reverse_proxy lsp:3001 {
header_up Host lsp
}
reverse_proxy windmill_server:8000
}
Khởi động tất cả:
docker compose up -d
Chờ khoảng 30 giây để database khởi tạo, rồi mở http://localhost:3000. Lần đăng nhập đầu tiên sẽ tạo tài khoản admin — hãy dùng mật khẩu thực sự. Windmill tự động tạo webhook URL cho mỗi script bạn tạo, nghĩa là các endpoint đó có thể tiếp cận được từ bên ngoài mạng nội bộ của bạn. Thông tin đăng nhập yếu trên một HomeLab kết nối internet là rủi ro thực sự.
Viết Script Thực Sự
Script Python Đầu Tiên
Vào phần Scripts, click New Script, chọn Python. Editor sẽ cho bạn một template hàm có kiểu để bắt đầu:
import wmill
def main(name: str = "world"):
return f"Xin chào, {name}!"
Windmill đọc các type annotation đó và tạo ra form trực tiếp — trường text cho string, spinner số cho integer, checkbox cho boolean. Nhấn nút Run và test script trực tiếp trong editor, không cần kết nối flow.
Đây là thứ gì đó thực tế hơn — một công cụ kiểm tra dung lượng ổ đĩa trả về dữ liệu có cấu trúc mà các script khác của bạn có thể dựa vào:
import subprocess
import wmill
def main(threshold_percent: int = 80) -> dict:
result = subprocess.run(
["df", "-h", "/"],
capture_output=True,
text=True
)
lines = result.stdout.strip().split("\n")
usage_line = lines[1].split()
use_percent = int(usage_line[4].replace("%", ""))
return {
"disk_usage_percent": use_percent,
"alert": use_percent > threshold_percent,
"message": f"Ổ đĩa đạt {use_percent}% — {'CẢNH BÁO' if use_percent > threshold_percent else 'OK'}"
}
Lưu và chạy. Bạn sẽ nhận được JSON có kiểu ngay lập tức. Một script cảnh báo downstream có thể tham chiếu result.alert trực tiếp — không cần parsing, không cần khớp chuỗi dễ vỡ, chỉ là một trường có kiểu từ đầu ra của bước trước.
Kết Nối Scripts thành Flow
Tạo một Flow mới, thêm script disk-check làm Bước 1. Cho Bước 2, thêm một TypeScript script gửi cảnh báo Telegram khi flag là true:
import * as wmill from "windmill-client";
export async function main(
alert: boolean,
message: string,
bot_token: string = "$var:telegram_bot_token",
chat_id: string = "$var:telegram_chat_id"
) {
if (!alert) {
return { sent: false, reason: "Không có cảnh báo nào được kích hoạt" };
}
const url = `https://api.telegram.org/bot${bot_token}/sendMessage`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id, text: `Cảnh báo HomeLab: ${message}` }),
});
const data = await response.json();
return { sent: true, telegram_response: data };
}
Cú pháp $var:telegram_bot_token là tham chiếu đến một secret được lưu trong variable manager của Windmill — thông tin đăng nhập không bao giờ xuất hiện trong source code. Kết nối đầu ra của Bước 1 với đầu vào của Bước 2 trong flow editor, rồi gắn lịch cron:
# Chạy mỗi giờ đúng vào đầu giờ
0 * * * *
Xử Lý Sự Cố Khi Mọi Thứ Hỏng Lúc 2 Giờ Sáng
Worker không nhận job? Bắt đầu với logs:
docker compose logs windmill_worker --tail=50
Nguyên nhân phổ biến nhất: DATABASE_URL không khớp hoặc container worker không phân giải được hostname db. Khi hostname không khớp chính xác với tên service, worker sẽ âm thầm thất bại khi kết nối — job xếp hàng nhưng không bao giờ thực thi. Kiểm tra rằng mọi tên service trong compose file đều khớp với những gì được tham chiếu trong biến môi trường từng ký tự một.
Không tìm thấy package Python? Thêm một comment block requirements ở đầu script — Windmill workers sẽ cài đặt tự động trong lần chạy đầu tiên, không cần Docker image tùy chỉnh:
# requirements:
# requests==2.31.0
# psutil==5.9.5
import requests
import psutil
Đó là tất cả những gì bạn cần viết. Không cần rebuild image, không cần quản lý virtual environment — worker sẽ xử lý lockfile và cô lập dependency cho từng script.
LSP autocomplete không hoạt động trong editor? Kiểm tra xem container lsp có đang chạy không và Caddyfile có route đúng /ws/* đến container windmill_server không. Khởi động lại lsp nếu nó đã khởi động trước khi server sẵn sàng:
docker compose restart lsp
Windmill Phù Hợp Ở Đâu Trong Stack HomeLab Của Bạn
Khi coi script tự động hóa như code có version thực sự, cách thư viện phát triển cũng thay đổi. Vài script đầu tiên cảm thấy chậm hơn so với kéo thả node trong GUI — khoảng 20 phút so với 5. Đến tuần thứ ba, khoản đầu tư đó có hiệu quả: disk monitor trở thành module dùng chung cho năm flow khác nhau, và Telegram notifier xử lý cảnh báo từ monitoring job, backup pipeline, và deploy script. Khả năng tái sử dụng tăng nhanh khi các pattern đã được thiết lập.
Git history tạo ra sự khác biệt lúc 2 giờ sáng. Mỗi script có đầy đủ change log. Khi có gì đó hỏng — và luôn có gì đó hỏng lúc 2 giờ sáng — bạn có thể thấy chính xác điều gì đã thay đổi giữa lần chạy hoạt động cuối cùng và hiện tại, rồi roll back bằng một click. JSON export của n8n chỉ cho bạn snapshot, không phải diff.
Windmill không cố thay thế mọi công cụ tự động hóa. Nó lấp đầy khoảng trống giữa “cron job với Bash script” và “CI/CD pipeline đầy đủ.” Nếu bạn đã viết code cho HomeLab, đây là lớp kết nối mọi thứ lại mà không cần dùng băng dính để gắn các công cụ với nhau. Giữ n8n cho các tích hợp đơn giản tốt hơn khi làm trực quan — hai công cụ này giải quyết các vấn đề khác nhau và có thể chạy song song thoải mái.

