Por qué autoalojar Windmill en un VPS
Windmill se ejecuta de forma continua en su servidor y expone cada uno de sus scripts como un endpoint REST autenticado por token, una tarea programada o una interfaz autogenerada. A diferencia de una función serverless, no hay arranque en frío ni límite de ejecución impuesto por un tercero. Sus scripts Python, TypeScript o Bash acceden directamente a las bases de datos y a los servicios internos de su VPS, sin atravesar una pasarela cloud. El backend está escrito en Rust: servidores de API sin estado y workers que extraen los jobs de una cola almacenada en PostgreSQL, lo que permite añadir workers sin tocar el resto.
La elección depende sobre todo de su forma de trabajar. Si lo que necesita es conectar SaaS entre sí en un lienzo visual, n8n sigue siendo más directo; para procesos de negocio muy largos con garantía de reanudación, Temporal está hecho para eso. Windmill apunta a los equipos que ya escriben código y quieren hacerlo ejecutable, programable y utilizable por no desarrolladores en minutos. En cuanto a la licencia, el núcleo es AGPLv3 y la imagen Community Edition es gratuita, con cuotas y funciones reservadas a la edición Enterprise; revenderla u ofrecerla como servicio gestionado a sus clientes exige una licencia comercial.
Beneficios concretos del autoalojamiento
- Ninguna facturación por ejecución: miles de jobs al día sin factura de computación en la nube.
- Acceso directo a las bases de datos y servicios internos del VPS, sin túnel.
- Secretos y claves API almacenados cifrados en Windmill y que nunca salen de su servidor.
- Formularios de entrada y endpoints REST generados automáticamente para cada script, a partir de la firma de su función
main. - Workers adicionales que se pueden añadir en caliente para absorber los picos de carga.
- Varios workspaces aislados en una misma instancia: uno por equipo, por proyecto o por entorno.
- Versionado: cada despliegue de un script se conserva, y Git Sync envía el workspace a un repositorio Git (incluido en Community Edition para workspaces de 2 usuarios como máximo).
Requisitos técnicos
Windmill necesita como mínimo 2 vCPU y 4 GB de RAM. El stack oficial arranca PostgreSQL 18, el servidor, tres workers generalistas limitados a 2 GB de memoria cada uno, un worker nativo para los jobs ligeros, el servicio windmill_extra (autocompletado LSP y depurador del editor) y un Caddy al frente: con 4 GB, reduzca por tanto el número de workers. Cuente con 20 GB de disco para la base de datos, los logs y la caché de dependencias de los workers, que crece con cada paquete pip o npm utilizado. Basta con Ubuntu 22.04/24.04 o Debian 12 con Docker Engine y el plugin Docker Compose v2, más un nombre de dominio que apunte al VPS para el certificado TLS. No se necesita GPU. Si prevé ejecutar workers Python pesados (ML, pandas sobre grandes conjuntos de datos), opte por al menos 4 vCPU y 8 GB de RAM.
Dimensionar el VPS según el uso
- La regla del editor: aproximadamente 1 worker por vCPU y de 1 a 2 GB de RAM por worker. Un job que supera el límite de memoria de su worker (2048 MB por defecto) se termina con el código de salida 137.
- Uso individual o prueba: 2 vCPU, 4 GB de RAM y 20 GB de disco, con un solo worker generalista además del worker nativo. Suficiente para scripts programados y algunos webhooks.
- Equipo pequeño: 4 vCPU, 8 GB de RAM y 40 GB de disco. Los tres workers del archivo oficial y PostgreSQL caben en el mismo host sin saturar la memoria.
- Varios equipos o jobs Python pesados: 8 vCPU, 16 GB de RAM y 80 GB de disco para seis a ocho workers. En ese punto, una base PostgreSQL separada simplifica las copias de seguridad y las actualizaciones.
Desplegar Windmill con Docker Compose
Descargar los archivos oficiales
Conéctese por SSH a su VPS y descargue los tres archivos de referencia publicados en el repositorio de Windmill: el
docker-compose.yml, elCaddyfiledel reverse proxy y el.env:mkdir -p /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 .envNo hace falta clonar todo el repositorio. Estos archivos siguen la rama
main: reléalos antes de cada actualización, porque el stack evoluciona (paso a PostgreSQL 18, serviciowindmill_extra…).Rellenar el archivo .env
El
.envsolo contiene dos variables. Sustituya la contraseña por defecto de PostgreSQL y fije la imagen en una versión publicada en lugar demain(1.864.1 a 06/11/2026, que debe comprobar en la página de releases de GitHub):DATABASE_URL=postgres://postgres:MOT_DE_PASSE_FORT@db/windmill?sslmode=disable WM_IMAGE=ghcr.io/windmill-labs/windmill:1.864.1Copie la misma contraseña en
POSTGRES_PASSWORD, bajo el serviciodbdeldocker-compose.yml. El hostdbes el nombre del servicio PostgreSQL dentro del stack: no lo sustituya porlocalhost. Para usar una base existente, apunteDATABASE_URLhacia ella y ponga el serviciodbenreplicas: 0.Arrancar el stack
Lance el conjunto y siga el arranque del servidor:
docker compose up -d docker compose ps docker compose logs -f windmill_serverdocker compose psdebe listardb,windmill_server, tres réplicas dewindmill_worker,windmill_worker_native,windmill_extraycaddy; el indexador de búsqueda de texto completo (windmill_indexer) se queda en cero réplicas, porque es una función Enterprise. El servidor espera a que PostgreSQL supere su healthcheck y luego aplica él mismo las migraciones de esquema. Escucha en el puerto 8000 de la red Docker, sin publicarse en el host: es Caddy quien recibe el tráfico.Configurar el reverse proxy y el TLS
Lo más sencillo es dejar que el Caddy incluido obtenga el certificado Let's Encrypt. En el servicio
caddydeldocker-compose.yml, descomente el puerto 443 y sustituyaBASE_URL=":80"por su dominio; después vuelva a lanzardocker compose up -d:caddy: ports: - 80:80 - 25:25 - 443:443 environment: - BASE_URL=windmill.example.comMantenga abierto el puerto 80 para la validación. Si un nginx ya sirve otros sitios en el VPS, elimine el servicio
caddy, publique127.0.0.1:8000:8000enwindmill_servery127.0.0.1:3000:3000enwindmill_extra, y reproduzca el enrutamiento delCaddyfile: las rutas/ws/,/ws_mp/y/ws_debug/van awindmill_extra, y todo lo demás al servidor.location ~ ^/ws(_mp|_debug)?/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }Las cabeceras
UpgradeyConnectionson imprescindibles: el editor usa WebSockets para el autocompletado y la depuración. El certificado se obtiene después concertbot --nginx -d windmill.example.com.Conectarse y asegurar
Abra
https://windmill.example.come inicie sesión con la cuenta superadmin por defecto,[email protected]/changeme. El asistente del primer arranque propone sustituirla por su propia cuenta: hágalo, porque si se salta el paso las credenciales por defecto siguen activas, y es el fallo más habitual en las instancias Windmill expuestas públicamente. Después, en Instance settings, fije la Base URL en la dirección pública enhttps://: Windmill la usa para construir las URL de los webhooks y los enlaces de retorno de los flows. Por último, cree su primer workspace con un identificador corto (prod,equipo-datos).Ajustar el número de workers
El archivo oficial arranca tres réplicas de
windmill_worker, cada una limitada a 2048 MB. En un VPS de 4 GB, baje a uno o dos; en una máquina mayor, suba a un worker por vCPU:docker compose up -d --scale windmill_worker=2 docker compose ps windmill_workerPara que el ajuste sea permanente, modifique
deploy.replicasdel servicio en eldocker-compose.yml. El servidor no necesita reiniciarse: los workers se registran solos y aparecen en la página Workers de la interfaz, con su grupo (defaultonative).Actualizar Windmill
Cambie la etiqueta en
WM_IMAGEtras leer las notas de versión y después:docker compose pull docker compose up -d docker image pruneEl
pulldescarga las nuevas imágenes mientras los contenedores antiguos siguen en marcha; al reiniciar, el servidor migra la base por sí solo y los workers terminan sus jobs en curso dentro del plazo de parada de Docker (30 segundos). Haga una copia de seguridad de la base justo antes, y no lancedocker image prune -asalvo si el VPS no aloja nada más: el comando también borra las imágenes de los demás proyectos.
Configuración después de la instalación
Una vez la instancia en línea, seis ajustes deciden cómo se ejecutarán sus scripts en producción. Todos se hacen desde la interfaz, workspace por workspace.
Los ajustes esenciales
- Workspaces: un workspace aísla scripts, flows, secretos y miembros. Cree uno por equipo o por entorno en lugar de mezclarlo todo, y reserve el rol superadmin a la administración de la instancia.
- Variables y secretos: todas las variables se cifran con una clave simétrica propia del workspace; las marcadas como secretas dejan de mostrarse en la interfaz una vez introducidas. Es el lugar de los tokens de API y las contraseñas, nunca el código del script.
- Recursos: objetos JSON tipados (conexión PostgreSQL, cuenta SMTP, credenciales OAuth). Un parámetro tipado
postgresqlen un script solo propone entonces los recursos de ese tipo en el formulario generado. - Programaciones: cada script o flow acepta una o varias programaciones en formato cron de cinco campos, con un campo de segundos opcional al principio. El gestor de errores de programación es una función Enterprise; en Community Edition, vigile las ejecuciones fallidas desde la lista de runs.
- Webhooks: un script se llama de forma asíncrona con
/api/w/<workspace>/jobs/run/p/<ruta>(respuesta: el identificador del job) o síncrona con/api/w/<workspace>/jobs/run_wait_result/p/<ruta>(respuesta: el resultado). Autentique con la cabeceraAuthorization: Bearer <token>en lugar de?token=, que acaba en los registros de acceso. - Git Sync: en los ajustes del workspace, vincule un repositorio para enviar a él scripts, flows y recursos en cada despliegue. Gratuito en Community Edition hasta 2 usuarios por workspace; el despliegue automático desde Git está reservado a la edición Enterprise.
Ejemplo: un script Python de principio a fin
Este script consulta una API, conserva los elementos activos y los inserta en PostgreSQL mediante un recurso de Windmill. Cree un script Python, pegue el código y lánzelo desde el editor:
# requirements:
# httpx
# psycopg2-binary
from typing import TypedDict
import httpx
import psycopg2
class postgresql(TypedDict):
host: str
port: int
user: str
dbname: str
sslmode: str
password: str
def main(api_url: str, db: postgresql) -> dict:
resp = httpx.get(api_url, timeout=10)
resp.raise_for_status()
rows = [r for r in resp.json() if r.get("active")]
conn = psycopg2.connect(
host=db["host"], port=db["port"], user=db["user"],
password=db["password"], dbname=db["dbname"], sslmode=db["sslmode"],
)
with conn, conn.cursor() as cur:
cur.executemany(
"INSERT INTO items (id, name) VALUES (%(id)s, %(name)s) ON CONFLICT DO NOTHING",
rows,
)
conn.close()
return {"candidates": len(rows), "source": api_url}Los comentarios # requirements: sustituyen la deducción automática de dependencias a partir de los imports: útil aquí, porque el import psycopg2 se publica bajo el paquete psycopg2-binary. Windmill resuelve esos paquetes, escribe un lockfile y los guarda en caché en el worker, de modo que una misma versión del script se ejecuta siempre con las mismas dependencias. La clase postgresql, con el nombre del tipo de recurso, hace aparecer un selector de recursos en el formulario. Una vez desplegado bajo la ruta f/etl/import_items, el script se llama desde cualquier herramienta:
curl -X POST "https://windmill.example.com/api/w/prod/jobs/run_wait_result/p/f/etl/import_items" \
-H "Authorization: Bearer $WM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"api_url": "https://api.example.com/items"}'Endurecimiento de la instancia
No publique nunca los puertos 8000 y 3000 en la interfaz pública: solo el reverse proxy debe alcanzarlos, en local. Haga copias de seguridad de la base con docker compose exec -T db pg_dumpall -U postgres | gzip > /backups/windmill-$(date +%F).sql.gz: el volcado debe cubrir todo el clúster, no solo la base windmill, porque Windmill crea bases anexas y vincula sus políticas de seguridad a roles de nivel clúster. Ese volcado contiene sus secretos cifrados: guárdelo fuera del VPS y protéjalo como un secreto. Monte el socket de Docker en los workers solo si confía en todos los usuarios de la instancia, ya que da a los scripts el control del host. Por último, deje REQUIRE_SIGNED_DEBUG_REQUESTS=true en windmill_extra: desactivarlo en una instancia accesible desde Internet expone un depurador capaz de ejecutar código sin autenticación.
Resolución de problemas: los errores de un primer despliegue
La mayoría de los incidentes vienen de la base de datos, de la memoria de los workers o del reverse proxy. Los síntomas y sus correcciones:
Síntomas y correcciones
- El contenedor
dbse detiene justo después de una actualización: el volumen lo creó PostgreSQL 16 y la imagen ha pasado a 18, que se niega a leerlo. Siga el procedimiento de migración de la documentación (volcado completo y restauración), o fijepostgres:16con su antiguo punto de montaje: la versión 16 sigue mantenida hasta noviembre de 2028. - Job interrumpido con el código 137: ha superado el límite de memoria de su worker. Aumente
resources.limits.memorydel serviciowindmill_worker, o reduzca el tamaño de los datos que el script carga en memoria. - Servidor o workers incapaces de alcanzar la base:
DATABASE_URLapunta alocalhosten lugar del serviciodb, o la contraseña difiere dePOSTGRES_PASSWORD. Corrija el.envy después ejecutedocker compose up -dpara recrear los contenedores. ModuleNotFoundErroro instalación de dependencias fallida: el worker descarga los paquetes en la primera ejecución y debe alcanzarpypi.orgyfiles.pythonhosted.orgpor HTTPS saliente. Detrás de un proxy corporativo, las variablesPY_TRUSTED_HOSTyPY_INDEX_CERTgestionan la confianza; un índice privado se declara en los Instance settings (edición Enterprise).- Editor sin autocompletado, depurador mudo: el reverse proxy no enruta
/ws/,/ws_mp/y/ws_debug/haciawindmill_extra, o elimina las cabecerasUpgradeyConnection. Retome el enrutamiento delCaddyfileoficial.
Windmill, n8n o Temporal
Desplace la tabla
| Criterio | Windmill | n8n | Temporal |
|---|---|---|---|
| Escritura de los workflows | Scripts Python, TypeScript, Go, Bash, SQL… ensamblados en flows | Lienzo de nodos; nodo Code en JavaScript o Python | Código de aplicación vía SDK (Go, Java, Python, TypeScript, .NET, PHP, Ruby, Rust) |
| Licencia | AGPLv3 para el núcleo; Community Edition gratuita con cuotas, funciones Enterprise bajo licencia | Sustainable Use License (fair-code), archivos `.ee` bajo licencia comercial | MIT para el servidor |
| Interfaz | Editor web, formularios y apps internas generados | Editor visual de nodos | Interfaz web de observación; el código se escribe fuera de la herramienta |
| Para quién | Equipos que escriben código y quieren exponerlo rápido | Integraciones visuales entre SaaS | Procesos largos con fuertes exigencias de durabilidad |
Instalarlo con un solo clic desde el Marketplace de ServOrbit. Si prefiere evitar la configuración manual, el Marketplace de ServOrbit aprovisiona Windmill automáticamente — servidor, dos workers, PostgreSQL 16, reverse proxy nginx y certificado Let's Encrypt — en unos minutos. La sección «Primer acceso» de su área de cliente indica la URL y las credenciales generadas. La ficha de Windmill, accesible desde el botón al final de este artículo, le permite empezar.