Por qué autoalojar Apache Airflow en un VPS
Airflow es la herramienta que eligen los equipos de datos cuando quieren describir sus pipelines en Python puro: cada DAG es un archivo .py que define tareas, sus dependencias y su planificación. El ecosistema de providers es inmenso (bases SQL, S3, BigQuery, dbt, Spark…) y la comunidad, enorme. Los servicios gestionados facturan caro el entorno por hora; al autoalojar Airflow en un VPS obtienes el mismo motor por el coste de un servidor, y tus DAG se ejecutan cerca de tus fuentes de datos internas. También es una cuestión de control: versiones de los providers, dependencias Python propias, variables y conexiones, todo queda en tus manos. Airflow 3, versión vigente desde abril 2025 (3.3.0 en julio 2026, 3.3.1 en agosto 2026), aporta una arquitectura orientada a API: los workers ya no acceden a PostgreSQL directamente.
Beneficios concretos del autoalojamiento
- DAG en Python puro, versionados en Git, con dependencias y providers bajo control.
- Enorme ecosistema de operadores: SQL, cloud, dbt, Spark, HTTP, Kubernetes y más.
- Scheduler robusto: cron, dependencias entre tareas, backfill y catchup nativos.
- Workers Celery escalables para absorber cientos de tareas en paralelo.
- Ejecución cerca de tus bases internas, sin tránsito por una nube de terceros.
- Coste de un VPS en lugar de un entorno gestionado facturado por hora.
- Control total de las versiones: quédate en Airflow 2.x o migra a 3 a tu ritmo.
Airflow 2 o Airflow 3: qué versión elegir en 2026
Airflow 3.0 introdujo rupturas importantes. Se han retirado los atributos schedule_interval, los SubDAGs y execution_date; los providers exigen Airflow 3.1 como mínimo desde mayo 2026. Si empiezas un proyecto nuevo, instala Airflow 3 directamente: el archivo docker-compose.yaml oficial ya está adaptado. Si mantienes DAG existentes que usan schedule_interval, execution_date o SubDAGs, quédate en la última versión 2.x y planifica la migración: la documentación oficial ofrece una guía de actualización desde Airflow 2.7 como mínimo. La imagen Docker sigue el mismo esquema: apache/airflow:3.3.1 para la rama estable actual, apache/airflow:2.11.0 para mantenerte en la rama 2.
Requisitos técnicos
Airflow es la pila más exigente de esta serie en configuración CeleryExecutor, porque ejecuta a la vez webserver, scheduler, worker(s), triggerer, un servidor de API interno, Redis y PostgreSQL. La documentación oficial recomienda 4 GB de RAM como mínimo, pero en la práctica cuenta con 4 vCPU y 8 GB para una carga cómoda. Prevé 16 GB si tus DAG cargan pandas o grandes volúmenes. Opta por el modo LocalExecutor (sin Celery ni Redis) si empiezas en un VPS de 4 GB: la pila se reduce a la mitad. También necesitarás Docker 24+ y Docker Compose v2 (no el antiguo docker-compose v1), 40 GB de disco SSD y un dominio apuntando a tu servidor.
Desplegar Apache Airflow 3 con Docker Compose
Preparar las carpetas y descargar el compose oficial
Crea el directorio de trabajo y descarga el archivo de referencia:
mkdir -p /opt/airflow && cd /opt/airflow && curl -LfO 'https://airflow.apache.org/docs/apache-airflow/stable/docker-compose.yaml'. Crea después las subcarpetas que esperan los volúmenes montados:mkdir -p ./dags ./logs ./plugins ./config.Ajustar el UID y los permisos (paso crítico)
Airflow exige que los volúmenes montados pertenezcan al mismo UID que el usuario interno del contenedor. Genera el archivo de entorno:
echo "AIRFLOW_UID=$(id -u)" > .env. Sin este archivo, el scheduler no puede escribir sus logs y falla al arrancar.Inicializar la base de datos
Airflow debe aplicar primero sus migraciones sobre PostgreSQL y crear la cuenta de administrador. Ejecuta:
docker compose up airflow-init. El contenedor aplica las migraciones, crea el usuarioairflowcon contraseñaairflowy termina con código0. Espera ese código0antes de continuar.Arrancar la pila completa
Lanza todos los servicios en segundo plano:
docker compose up -d. Comprueba el estado condocker compose ps— todos los contenedores deben pasar ahealthyen 1 a 3 minutos. Si uno se queda enstarting, consulta sus logs:docker compose logs airflow-scheduler. El webserver escucha por defecto en el puerto 8080.Montar el proxy inverso HTTPS
No expongas nunca el puerto 8080 directamente: la interfaz de Airflow muestra conexiones, variables y logs potencialmente sensibles. Instala Caddy:
apt install -y caddy. Crea/etc/caddy/Caddyfilecon:airflow.tudominio.com { reverse_proxy localhost:8080 }. Recarga:systemctl reload caddy.Desplegar tu primer DAG
Coloca un archivo Python en
./dags/. Un DAG mínimo compatible con Airflow 3 usascheduleen lugar deschedule_interval(eliminado en 3.0):from airflow.sdk import DAG, task, luegowith DAG('hello', schedule='@daily', start_date=datetime(2025, 1, 1), catchup=False). El scheduler lo detecta en unos segundos.Construir una imagen propia para tus dependencias Python
Para providers o bibliotecas propias (pandas, sqlalchemy, boto3…), no las instales en tiempo de ejecución: construye tu propia imagen. Crea un
Dockerfile:FROM apache/airflow:3.3.1, luegoUSER airflow(obligatorio — los paquetes instalados comorootson inaccesibles para el usuario airflow) yRUN pip install --no-cache-dir apache-airflow-providers-amazon==9.0.0 pandas==2.2.0. Endocker-compose.yaml, sustituyeimage: apache/airflow:3.3.1porbuild: .. Reconstruye condocker compose build && docker compose up -d.
CVE-2026-58076: corregir la RCE de deserialización (Airflow 3.0.0 → 3.3.0)
Si has seguido los pasos anteriores con una imagen anterior a 3.3.1, estás afectado. CVE-2026-58076 (CVSS 8.8) afecta a Apache Airflow desde 3.0.0 hasta 3.3.0 incluida; la corrección está en 3.3.1. La capa de serialización reconstruía los nodos de excepción (airflow_exc_ser / base_exc_ser) importando una clase cuyo nombre procede del propio blob serializado y luego instanciándola con argumentos de esos mismos datos, sin restricción útil sobre lo que podía importarse. El executor_config de un operador alcanza esa ruta: un autor de DAG puede hacer que se cargue y ejecute un callable arbitrario. El scheduler pasa por ese código en su bucle normal al reconstruir los DAG serializados; el servidor de API pasa por él en una lectura autenticada, por ejemplo al mostrar el detalle de un DAG. La corrección de 3.3.1 restringe la clase importada a subclases de BaseException.
Para actualizar: cambia la etiqueta a apache/airflow:3.3.1 en tu docker-compose.yaml *y* en el FROM de tu imagen propia, después docker compose pull && docker compose up -d. Verifica con docker compose exec airflow-apiserver airflow version.
⚠️ Una actualización hecha por CVE-2026-33264 no basta: ese aviso cubría solo la rama trigger del mismo deserializador — es un punto de entrada distinto y su corrección no cierra este.
⚠️ El vector es el autor del DAG, no un visitante anónimo. Actualizar es necesario; la pregunta añadida es *quién puede dejar un archivo en ./dags y quién tiene cuenta en la interfaz*.
Migrar de Airflow 2 a Airflow 3: qué se rompe
Si tienes DAG existentes, revisa estos puntos antes de actualizar la imagen. schedule_interval se ha eliminado: sustitúyelo por schedule. execution_date ya no está disponible en el contexto de las tareas: usa logical_date. Los SubDAGs se han retirado: migra a TaskGroups o a dynamic task mapping. Los XComs en pickle están desactivados por defecto: tus XComs deben ser tipos serializables en JSON. El acceso directo a la base de metadatos desde el código de tarea (vía Session o settings.engine) ya no funciona: pasa por los hooks oficiales de Airflow. El comando airflow db upgrade sustituye a airflow db init para actualizar un esquema existente.
Solución de problemas: mensajes de error habituales
PermissionError: [Errno 13] Permission denied: '/opt/airflow/logs/scheduler' — falta el archivo .env o AIRFLOW_UID no coincide con el UID del propietario de las carpetas montadas. Corrige con sudo chown -R $(id -u):0 ./dags ./logs ./plugins ./config y reinicia. The scheduler does not appear to be running (banner naranja en la interfaz) — comprueba que el contenedor airflow-scheduler está healthy (docker compose ps); si el banner persiste pero desaparece al recargar, es un falso positivo documentado en el tracker de Airflow, sin impacto funcional. error: invalid command 'webserver' — una dependencia Python instalada ha introducido un conflicto de importación al arrancar: reconstruye la imagen fijando las versiones.
No pongas nunca lógica pesada directamente en el archivo del DAG: el scheduler analiza todos los .py a intervalos regulares (min_file_process_interval, 30 s por defecto), y un import costoso a nivel de módulo ralentiza todo el orquestador. Mantén ligero el código de parsing y traslada el trabajo a las tareas. Con Airflow 3, prefiere el decorador @task (TaskFlow API) a los operadores clásicos: el código es más legible y los XComs se gestionan solos. Activa catchup=False en cada DAG en desarrollo para evitar cientos de ejecuciones retroactivas en el primer arranque. Purga con regularidad la carpeta ./logs.
Supervisión y mantenimiento de la pila Airflow
Airflow 3 expone de forma nativa trazas OpenTelemetry: actívalas con AIRFLOW__METRICS__OTEL_ON=True y apunta a tu colector (Grafana Tempo, Jaeger). Para un panel rápido, Airflow también expone métricas Prometheus mediante el plugin airflow-exporter. Configura docker compose con una política de reinicio (restart: unless-stopped) para que la pila vuelva sola tras reiniciar el VPS. Mantén PostgreSQL actualizado por separado. Purga con regularidad los metadatos antiguos con docker compose exec airflow-scheduler airflow db clean --clean-before-timestamp 2025-01-01 para evitar el crecimiento de la base.
LocalExecutor frente a CeleryExecutor: cuál elegir
Desplace la tabla
| Criterio | LocalExecutor | CeleryExecutor |
|---|---|---|
| RAM recomendada | 4 GB | 8 GB (16 GB ideal) |
| Servicios Docker adicionales | Ninguno (+ PostgreSQL) | Redis + worker(s) Celery |
| Paralelismo de tareas | Limitado al VPS | Escalable horizontalmente |
| Adecuado para | Pipelines ligeros, equipos individuales | Cargas grandes, multi-worker |
| Complejidad operativa | Baja | Media |
| Comando para activarlo | `AIRFLOW__CORE__EXECUTOR=LocalExecutor` | Por defecto en el compose oficial |