Guía de despliegue

Instalar Windmill en un VPS: automatización self-hosted

Desplegar en un VPS Cloud →

Tutorial

Automatización

Instalar Windmill en un VPS: automatización self-hosted

Windmill es una plataforma open source que convierte código (Python, TypeScript, Go, Bash, SQL) en workflows, API e interfaces internas. Pensada para desarrolladores, combina la flexibilidad del código con la rapidez del no-code. Autoalojada en un VPS, se convierte en su motor de automatización y en su app builder interno, sin depender de un SaaS.

13 min de lectura7 pasos
Contenido· Por qué autoalojar Windmill en un VPS1/12
  1. 01Por qué autoalojar Windmill en un VPS
  2. 02Beneficios concretos del autoalojamiento
  3. 03Requisitos técnicos
  4. 04Dimensionar el VPS según el uso
  5. 05Desplegar Windmill con Docker Compose
  6. 06Configuración después de la instalación
  7. 07Los ajustes esenciales
  8. 08Ejemplo: un script Python de principio a fin
  9. 09Endurecimiento de la instancia
  10. 10Resolución de problemas: los errores de un primer despliegue
  11. 11Síntomas y correcciones
  12. 12Windmill, n8n o Temporal

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

  1. 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, el Caddyfile del 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 .env

    No 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, servicio windmill_extra…).

  2. Rellenar el archivo .env

    El .env solo contiene dos variables. Sustituya la contraseña por defecto de PostgreSQL y fije la imagen en una versión publicada en lugar de main (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.1

    Copie la misma contraseña en POSTGRES_PASSWORD, bajo el servicio db del docker-compose.yml. El host db es el nombre del servicio PostgreSQL dentro del stack: no lo sustituya por localhost. Para usar una base existente, apunte DATABASE_URL hacia ella y ponga el servicio db en replicas: 0.

  3. Arrancar el stack

    Lance el conjunto y siga el arranque del servidor:

    docker compose up -d
    docker compose ps
    docker compose logs -f windmill_server

    docker compose ps debe listar db, windmill_server, tres réplicas de windmill_worker, windmill_worker_native, windmill_extra y caddy; 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.

  4. 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 caddy del docker-compose.yml, descomente el puerto 443 y sustituya BASE_URL=":80" por su dominio; después vuelva a lanzar docker compose up -d:

      caddy:
        ports:
          - 80:80
          - 25:25
          - 443:443
        environment:
          - BASE_URL=windmill.example.com

    Mantenga abierto el puerto 80 para la validación. Si un nginx ya sirve otros sitios en el VPS, elimine el servicio caddy, publique 127.0.0.1:8000:8000 en windmill_server y 127.0.0.1:3000:3000 en windmill_extra, y reproduzca el enrutamiento del Caddyfile: las rutas /ws/, /ws_mp/ y /ws_debug/ van a windmill_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 Upgrade y Connection son imprescindibles: el editor usa WebSockets para el autocompletado y la depuración. El certificado se obtiene después con certbot --nginx -d windmill.example.com.

  5. Conectarse y asegurar

    Abra https://windmill.example.com e 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 en https://: 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).

  6. 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_worker

    Para que el ajuste sea permanente, modifique deploy.replicas del servicio en el docker-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 (default o native).

  7. Actualizar Windmill

    Cambie la etiqueta en WM_IMAGE tras leer las notas de versión y después:

    docker compose pull
    docker compose up -d
    docker image prune

    El pull descarga 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 lance docker image prune -a salvo 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 postgresql en 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 cabecera Authorization: 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 db se 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 fije postgres:16 con 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.memory del servicio windmill_worker, o reduzca el tamaño de los datos que el script carga en memoria.
  • Servidor o workers incapaces de alcanzar la base: DATABASE_URL apunta a localhost en lugar del servicio db, o la contraseña difiere de POSTGRES_PASSWORD. Corrija el .env y después ejecute docker compose up -d para recrear los contenedores.
  • ModuleNotFoundError o instalación de dependencias fallida: el worker descarga los paquetes en la primera ejecución y debe alcanzar pypi.org y files.pythonhosted.org por HTTPS saliente. Detrás de un proxy corporativo, las variables PY_TRUSTED_HOST y PY_INDEX_CERT gestionan 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/ hacia windmill_extra, o elimina las cabeceras Upgrade y Connection. Retome el enrutamiento del Caddyfile oficial.

Windmill, n8n o Temporal

Desplace la tabla

CriterioWindmilln8nTemporal
Escritura de los workflowsScripts Python, TypeScript, Go, Bash, SQL… ensamblados en flowsLienzo de nodos; nodo Code en JavaScript o PythonCódigo de aplicación vía SDK (Go, Java, Python, TypeScript, .NET, PHP, Ruby, Rust)
LicenciaAGPLv3 para el núcleo; Community Edition gratuita con cuotas, funciones Enterprise bajo licenciaSustainable Use License (fair-code), archivos `.ee` bajo licencia comercialMIT para el servidor
InterfazEditor web, formularios y apps internas generadosEditor visual de nodosInterfaz web de observación; el código se escribe fuera de la herramienta
Para quiénEquipos que escriben código y quieren exponerlo rápidoIntegraciones visuales entre SaaSProcesos 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.

Despliegue Windmill con un clic en su VPS ServOrbit

El Marketplace de ServOrbit aprovisiona automáticamente Windmill con PostgreSQL 16, dos workers y un reverse proxy — sus scripts pasan a producción en unos minutos, sin una sola línea de configuración.