Windmill es una plataforma de código abierto para convertir scripts en Python, TypeScript, Go, Bash o SQL en tareas reutilizables: genera automáticamente un formulario a partir de los parámetros, los encadena en flujos, los programa como cron y los expone como webhooks. Es una alternativa autoalojada a herramientas como Retool o Zapier. En este tutorial desplegarás Windmill en Ubuntu 24.04 con los archivos oficiales de Docker Compose, lo publicarás con HTTPS gracias a Caddy y crearás un script que podrás ejecutar desde la interfaz, por webhook y según un horario.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 vCPU y 4 GB de RAM. Cada worker tiene un límite de 2 GB y el archivo oficial arranca tres.
- Unos 20 GB libres en disco para imágenes, la base de datos y la caché de dependencias.
- Un usuario no root con privilegios
sudo. - Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker.
- Un dominio o subdominio, por ejemplo
windmill.tu_dominio, con un registro DNS A que apunte a la IP pública del servidor (tu_ip_del_servidor). - Los puertos 80 y 443 accesibles desde Internet, necesarios para que Caddy obtenga el certificado de Let's Encrypt.
Comprueba que Docker y Compose responden:
docker --version
docker compose version
Docker version 28.x.x, build xxxxxxx
Docker Compose version v2.x.x
Paso 1: Descargar los archivos oficiales de Windmill
Windmill publica tres archivos para instalaciones con Docker Compose: docker-compose.yml (base de datos PostgreSQL, servidor, workers, servicio de LSP y Caddy), Caddyfile (proxy inverso) y .env (cadena de conexión e imagen). No hace falta clonar el repositorio completo.
Crea un directorio para la instalación y hazlo propiedad de tu usuario:
sudo mkdir -p /opt/windmill
sudo chown "$USER":"$USER" /opt/windmill
cd /opt/windmill
Descarga los tres archivos:
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
Verifica que están en su sitio:
ls -la /opt/windmill
-rw-rw-r-- 1 tu_usuario tu_usuario ... .env
-rw-rw-r-- 1 tu_usuario tu_usuario ... Caddyfile
-rw-rw-r-- 1 tu_usuario tu_usuario ... docker-compose.yml
Paso 2: Cambiar la contraseña de PostgreSQL
Los archivos oficiales usan changeme como contraseña de PostgreSQL, tanto en POSTGRES_PASSWORD dentro de docker-compose.yml como en DATABASE_URL dentro de .env. Genera una contraseña aleatoria y sustituye ambas apariciones a la vez para que coincidan:
PGPASS=$(openssl rand -hex 24)
sed -i "s/changeme/${PGPASS}/" /opt/windmill/.env /opt/windmill/docker-compose.yml
Se usa una cadena hexadecimal porque no contiene caracteres que haya que escapar en la URL de conexión. Comprueba el resultado:
grep -n "POSTGRES_PASSWORD" /opt/windmill/docker-compose.yml
grep -n "DATABASE_URL" /opt/windmill/.env
40: POSTGRES_PASSWORD: 3f9c...e41a
1:DATABASE_URL=postgres://postgres:3f9c...e41a@db/windmill?sslmode=disable
ImportantePostgreSQL solo aplica
POSTGRES_PASSWORDla primera vez que inicializa el volumen de datos. Haz este cambio antes del primer arranque; si ya arrancaste conchangeme, cambia la contraseña dentro de PostgreSQL o elimina el volumendb_datasi todavía no contiene nada.
Paso 3: Configurar Caddy con tu dominio y HTTPS
Por defecto, el servicio caddy escucha solo en HTTP por el puerto 80 y también publica el puerto 25, que Windmill usa para los disparadores por correo electrónico. Para servir Windmill con HTTPS automático basta con indicar tu dominio en BASE_URL y publicar el puerto 443.
Abre el archivo:
nano /opt/windmill/docker-compose.yml
Localiza el servicio caddy y deja las secciones ports y environment así, sustituyendo windmill.tu_dominio por tu subdominio:
ports:
- 80:80
- 443:443
environment:
- BASE_URL=windmill.tu_dominio
Se ha quitado 25:25 porque en este tutorial no usarás disparadores por correo; si más adelante los necesitas, vuelve a añadir la línea y abre el puerto. Con un nombre de dominio en BASE_URL, Caddy solicita y renueva el certificado de Let's Encrypt por su cuenta y redirige HTTP a HTTPS.
Comprueba que el archivo sigue siendo válido:
cd /opt/windmill
docker compose config --quiet && echo "configuración correcta"
configuración correcta
Si usas UFW, permite SSH, HTTP y HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
NotaDocker añade sus propias reglas de iptables para los puertos publicados, que no pasan por UFW. Por eso solo
caddypublica puertos en este archivo: PostgreSQL y el servidor de Windmill usanexposey solo son accesibles dentro de la red de Docker.
Paso 4: Arrancar Windmill
Descarga las imágenes y levanta todos los servicios en segundo plano:
cd /opt/windmill
docker compose up -d
La primera vez tarda unos minutos porque descarga varias imágenes. Comprueba el estado de los contenedores:
docker compose ps
NAME IMAGE SERVICE STATUS
windmill-caddy-1 ghcr.io/windmill-labs/caddy-l4:... caddy Up 1 minute
windmill-db-1 postgres:18 db Up 1 minute (healthy)
windmill-windmill_extra-1 ghcr.io/windmill-labs/windmill-extra windmill_extra Up 1 minute
windmill-windmill_server-1 ghcr.io/windmill-labs/windmill:main windmill_server Up 1 minute
windmill-windmill_worker-1 ghcr.io/windmill-labs/windmill:main windmill_worker Up 1 minute
windmill-windmill_worker-2 ghcr.io/windmill-labs/windmill:main windmill_worker Up 1 minute
windmill-windmill_worker-3 ghcr.io/windmill-labs/windmill:main windmill_worker Up 1 minute
windmill-windmill_worker_native-1 ghcr.io/windmill-labs/windmill:main windmill_worker_native Up 1 minute
Revisa que Caddy haya obtenido el certificado:
docker compose logs caddy | grep -i "certificate obtained"
Por último, consulta la versión a través de HTTPS:
curl -s https://windmill.tu_dominio/api/version
CE v1.xxx.x
Si recibes la versión, el proxy, el certificado y el servidor de Windmill funcionan.
Consejoen un servidor con 4 GB de RAM puedes bajar los workers de 3 a 1 cambiando
replicas: 3en el serviciowindmill_workery ejecutando de nuevodocker compose up -d.
Paso 5: Primer acceso y creación del workspace
Abre https://windmill.tu_dominio en el navegador. La instalación crea un superadministrador con estas credenciales por defecto:
- Email:
[email protected] - Contraseña:
changeme
Al iniciar sesión, Windmill abre un asistente de configuración inicial. Úsalo para:
- Sustituir el email y la contraseña del superadministrador por los tuyos. No dejes las credenciales por defecto en un servidor expuesto a Internet.
- Revisar los ajustes de la instancia y fijar la Base URL en
https://windmill.tu_dominio. Windmill la usa para construir las URL de webhooks, de aprobación y de los enlaces que envía.
Después crea un workspace para tu equipo, por ejemplo con el identificador operaciones. Los scripts, flujos, recursos, secretos y permisos se aíslan por workspace, así que conviene separar entornos o equipos en workspaces distintos.
Paso 6: Crear un script en Python
En Windmill, cada script define una función main. Sus parámetros tipados se convierten en un formulario en la interfaz y en el cuerpo JSON aceptado por la API. Las dependencias se detectan a partir de los import y se instalan en la caché del worker la primera vez.
En tu workspace, pulsa + Script, elige Python, dale una ruta como u/tu_usuario/comprobar_url y pega este código:
import requests
def main(url: str = "https://example.com", timeout: int = 10):
"""Comprueba si una URL responde y cuánto tarda."""
respuesta = requests.get(url, timeout=timeout)
return {
"url": url,
"estado": respuesta.status_code,
"ok": respuesta.ok,
"segundos": round(respuesta.elapsed.total_seconds(), 3),
}
Pulsa Test en el panel derecho. La primera ejecución tarda algo más porque el worker instala requests; las siguientes usan la caché. Deberías ver un resultado como este:
{
"url": "https://example.com",
"estado": 200,
"ok": true,
"segundos": 0.142
}
Cuando funcione, pulsa Deploy para publicar la versión. Cada despliegue queda versionado y puedes volver a una versión anterior desde el historial del script.
Paso 7: Crear un script en Bash
Los scripts de Bash reciben los argumentos de forma posicional. Windmill detecta los parámetros a partir de las asignaciones del tipo variable="$1" al principio del script y genera el formulario con ellas.
Crea un nuevo script con + Script, elige Bash, usa la ruta u/tu_usuario/uso_disco y pega:
directorio="${1:-/tmp}"
set -euo pipefail
echo "Uso de disco en ${directorio}:"
df -h "$directorio"
du -sh "$directorio" 2>/dev/null || true
Pulsa Test y comprueba la salida en el panel de logs. Ten en cuenta que el script se ejecuta dentro del contenedor del worker, no en el sistema del host: ve el sistema de archivos del contenedor. Para actuar sobre otros servidores, guarda la clave SSH como secreto o recurso de Windmill y conéctate desde el script.
Despliega el script con Deploy.
Paso 8: Ejecutar un script por webhook
Cada script y flujo desplegado tiene endpoints HTTP para ejecutarlo. Primero necesitas un token: abre el menú de tu usuario, entra en la configuración de la cuenta, ve a Tokens y crea uno con una etiqueta descriptiva, por ejemplo ci-webhook. Copia el valor, porque solo se muestra una vez.
Guárdalo en una variable de la sesión para no escribirlo en cada comando:
export WM_TOKEN="tu_token"
El endpoint run_wait_result ejecuta el script y espera a que termine para devolver el resultado. Es la opción más cómoda para tareas cortas:
curl -s -X POST \
"https://windmill.tu_dominio/api/w/operaciones/jobs/run_wait_result/p/u/tu_usuario/comprobar_url" \
-H "Authorization: Bearer ${WM_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.cubepath.com", "timeout": 5}'
{"url":"https://www.cubepath.com","estado":200,"ok":true,"segundos":0.211}
Para tareas largas, usa el endpoint asíncrono run, que encola el trabajo y devuelve inmediatamente su identificador:
curl -s -X POST \
"https://windmill.tu_dominio/api/w/operaciones/jobs/run/p/u/tu_usuario/comprobar_url" \
-H "Authorization: Bearer ${WM_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.cubepath.com"}'
0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b
Puedes seguir ese trabajo en la sección Runs de la interfaz, donde aparecen todos los trabajos con sus argumentos, logs y resultado. La pestaña de webhooks de cada script muestra las URL exactas y te permite crear un token limitado a ese script, lo que es preferible para integraciones externas como un pipeline de CI.
Advertenciaun token de usuario tiene los mismos permisos que su propietario. No lo guardes en repositorios; usa los secretos de tu sistema de CI.
Paso 9: Programar la ejecución
Para ejecutar un script de forma periódica, abre el script desplegado y crea una programación desde su sección de disparadores (Schedules). Indica:
- Expresión cron: Windmill usa cron con un campo inicial de segundos. Por ejemplo,
0 0 2 * * *ejecuta la tarea cada día a las 02:00 y0 */15 * * * *cada 15 minutos. La interfaz muestra las próximas ejecuciones para que confirmes que la expresión es la que quieres. - Zona horaria: por ejemplo
Europe/Madrid, para que el horario no cambie con el horario de verano. - Argumentos: los valores fijos con los que se ejecutará el script en cada disparo.
Activa la programación y guarda. Las ejecuciones programadas aparecen en Runs igual que las manuales, y puedes configurar un script como manejador de errores para recibir un aviso cuando alguna falle.
Paso 10: Encadenar scripts en un flujo con aprobación
Los flujos combinan scripts en pasos, con bifurcaciones, bucles, reintentos y pausas. Un caso típico es preparar un cambio, pedir aprobación a una persona y solo entonces aplicarlo.
- En el workspace, pulsa + Flow y dale una ruta, por ejemplo
f/operaciones/despliegue. - Añade un primer paso que reutilice un script existente o uno nuevo en línea que prepare la operación.
- Añade un paso de aprobación (en el editor aparece como paso de tipo Approval o como opción de suspensión en los ajustes avanzados del paso). Define cuántas aprobaciones hacen falta y el tiempo máximo de espera.
- Añade el paso final que aplica el cambio. Sus entradas pueden referenciar los resultados de pasos anteriores, por ejemplo
results.a.version. - Pulsa Test flow. La ejecución se detendrá en el paso de aprobación y mostrará los botones para aprobar o rechazar; al aprobar, continúa con el último paso.
Los flujos tienen los mismos endpoints de webhook y las mismas opciones de programación que los scripts, así que puedes lanzarlos desde tu CI y dejar que alguien apruebe el paso crítico desde el navegador.
Mantenimiento: actualizaciones y copias de seguridad
Las imágenes de Windmill usan la etiqueta main con pull_policy: always. Para actualizar, descarga las imágenes nuevas y recrea los contenedores:
cd /opt/windmill
docker compose pull
docker compose up -d
Todo el estado (scripts, flujos, historial, recursos y secretos cifrados) vive en PostgreSQL. Haz una copia completa del clúster antes de cada actualización; se usa pg_dumpall porque Windmill crea bases de datos y roles adicionales además de windmill:
mkdir -p ~/backups
docker compose -f /opt/windmill/docker-compose.yml exec -T db pg_dumpall -U postgres | gzip > ~/backups/windmill-$(date +%F).sql.gz
Comprueba que la copia no está vacía:
ls -lh ~/backups/
Solución de problemas
El navegador devuelve error de certificado o Caddy no arranca con HTTPS. Revisa docker compose logs caddy. Las causas habituales son que el registro DNS A todavía no apunta al servidor o que el puerto 80 o 443 está bloqueado por el firewall. Comprueba la resolución con dig +short windmill.tu_dominio.
El servidor se reinicia con errores de autenticación en PostgreSQL. La contraseña de DATABASE_URL en .env no coincide con la del volumen. Recuerda que POSTGRES_PASSWORD solo se aplica al crear el volumen: si lo cambiaste después, actualiza la contraseña del usuario postgres dentro de la base de datos o vuelve a la anterior. Consulta los logs con docker compose logs windmill_server --tail=50.
Los trabajos se quedan en cola sin ejecutarse. No hay workers disponibles para el grupo del trabajo. Comprueba que los contenedores windmill_worker están en marcha con docker compose ps y revisa docker compose logs windmill_worker --tail=50. En la interfaz, la sección Workers muestra los workers conectados.
Un worker se reinicia durante la ejecución. Probablemente alcanza el límite de memoria de 2 GB del servicio. Revisa docker stats y aumenta memory en deploy.resources.limits si el servidor tiene RAM suficiente.
Conclusión
Tienes Windmill funcionando en Ubuntu 24.04 con PostgreSQL, varios workers y HTTPS automático mediante Caddy, y has creado scripts en Python y Bash que puedes ejecutar desde la interfaz, por webhook o con una programación. Como siguientes pasos, puedes guardar credenciales de bases de datos y APIs como recursos del workspace, crear grupos para repartir permisos entre tu equipo y sincronizar tus scripts con un repositorio Git usando la CLI wmill.
