La alternativa a WatchTower para Docker

Vistas: 2
0:00 / 0:00
La alternativa a WatchTower para Docker

Hace tiempo que empecé a notar que mi servidor se me estaba yendo de las manos. No es que no supiera lo que tenía, era más bien que cada vez que quería saber el estado de todos mis contenedores tenía que hacer ssh, ejecutar docker ps, luego docker logs de los que me sonaban raros, y al final acababa con un watch en el terminal como un poseso. Y la gota que colmó el vaso fue cuando me di cuenta de que WatchTower, esa herramienta que llevaba años usando para mantener las imágenes actualizadas, había dejado de tener mantenimiento. Sí, hay forks y gente manteniéndolo, pero la cuestión es que llegados a este punto, a lo mejor es cuestión de darle una vuelta. Y eso precisamente es lo que he hecho. Te presento Alloy, el dashboard Docker que necesitabas.

El problema de gestionar contenedores sin volverte loco

Ponte en situación. Tienes un servidor en casa, o un VPS, con Docker corriendo. Empiezas con cuatro contenedores: un WordPress, una base de datos, un reverse proxy, un par de cosillas más. Todo controlado. Haces docker ps y lo ves todo verde.

Pasan los meses. Descubres el self-hosting, y empiezas a instalar de todo: Nextcloud, Jellyfin, Home Assistant, Grafana, InfluxDB, Mosquitto, Zigbee2MQTT, Paperless-ngx, un par de bots de Telegram… De repente tienes treinta contenedores, y el docker ps ya no cabe en una pantalla. En mi caso particular, tengo aproximadamente unos sesenta y tantos stacks de Docker Compose, cada uno con varias imágenes distintas. Te puedes imaginar el follón.

Entonces te das cuenta de los problemas. El primero, saber qué contenedor se ha caído sin tener que mirar los logs de cada uno es un coñazo. docker ps --filter status=exited te lo dice, pero tienes que acordarte de ejecutarlo. El segundo, las actualizaciones. ¿Cuántos de esos treinta contenedores tienen una imagen más nueva en Docker Hub? Podrías usar WatchTower, sí, pero WatchTower te actualiza todo sin preguntar. Y a veces no quieres que te actualice ese contenedor concreto porque la versión nueva ha roto algo. El tercero, los stacks de Docker Compose. Cuando tienes servicios que dependen unos de otros —un WordPress con su base de datos, o un stack de la suite de casa—, gestionarlos individualmente no tiene sentido. Quieres ver el stack como una unidad, y poder reiniciar todos los servicios del tirón. Y el cuarto, las notificaciones. Si un contenedor se cae a las tres de la mañana, quieres enterarte. Pero tampoco quieres que te avisen cada vez que reinicias algo a propósito.

Y aquí es donde entran las herramientas visuales. Portainer es la más conocida, y es potente, pero es pesada. Necesita su propia base de datos, consume sus recursos, y la versión gratuita te limita funcionalidades que antes eran libres. Dockge está muy bien para stacks, pero no te da visión global de todos los contenedores. Hay soluciones parciales, pero ninguna me terminaba de convencer.

Así que, como hago siempre, me lo hice yo mismo. Y así nació Alloy.

Qué es Alloy: arquitectura y filosofía

Alloy es un dashboard de gestión Docker en tiempo real. Backend en Rust con Axum, frontend en React con TypeScript y Mantine UI. Y lo más importante: no necesita base de datos externa. El estado se persiste en SQLite embebido. Cero dependencias externas.

La arquitectura es sencilla, pero efectiva. Por un lado tienes el backend en Rust, que se conecta al socket de Docker —o de Podman— a través de la crate Bollard. Bollard es el cliente oficial de la API de Docker para Rust, y es una maravilla. Te da acceso a todo: listar contenedores, inspeccionarlos, arrancarlos, pararlos, ver eventos en tiempo real…

El backend expone una API REST para todo el CRUD, y tres canales SSE —Server-Sent Events— para la parte en tiempo real. Un canal para eventos de contenedores —cuando uno se para, arranca, se reinicia—, otro para el progreso de las actualizaciones, y un tercero para notificaciones y alertas.

Por el lado del frontend, React con Mantine UI. Elegí Mantine porque me parece el mejor framework de componentes para React hoy en día. Es limpio, tiene todo lo que necesitas —tablas, modales, notificaciones, formularios— y el tema oscuro viene de serie. Que para un dashboard que vas a tener abierto todo el día, el dark mode no es un lujo, es una necesidad.

Los workers de fondo

Alloy ejecuta cuatro workers en segundo plano que son el corazón de la automatización. Te los detallo porque cada uno tiene una función muy específica:

  • El state worker escucha el API de eventos de Docker y, como fallback, refresca la lista de contenedores cada 30 segundos. También monitoriza transiciones de estado para disparar alertas. Está implementado con un tokio::select! que escucha tanto el stream de eventos de Docker como un timer de 30 segundos, por si el stream se cae.
  • El auto-update worker comprueba el registro de imágenes periódicamente —por defecto cada 6 horas— y aplica las políticas de actualización que hayas configurado. Es el que se encarga de que no te olvides de mantener tus imágenes al día.
  • El scheduler worker evalúa expresiones cron cada 60 segundos y ejecuta las tareas programadas. Aquí es donde puedes configurar esos reinicios programados a las 6 de la mañana.
  • El OIDC cleanup limpia estados de autenticación expirados cada 5 minutos. Es un worker pequeño pero importante para la seguridad.

¿Y por qué Rust? Pues porque para un servicio que tiene que estar funcionando 24/7, que gestiona el socket de Docker, que hace peticiones HTTP al registro de imágenes, que mantiene canales SSE abiertos con múltiples clientes… necesitas algo que sea rápido, fiable, y que no se coma la RAM. Con Rust, Alloy arranca y consume prácticamente nada. He hecho pruebas en una Raspberry Pi 4 con 2 GB de RAM, y Alloy va sobrado. Y la imagen Docker compilada con multi-stage pesa unos 60 megas. No es coña.

Características principales

Vale, vamos a lo que interesa: ¿qué puede hacer Alloy? Te voy a contar las características que lo hacen especial, y créeme, he puesto mucho cariño en cada una de ellas.

Monitorización en tiempo real de contenedores

Abres Alloy y ves la lista de todos tus contenedores: nombre, imagen, estado —con su colorcito verde, amarillo o rojo—, puertos expuestos, tiempo de actividad, tamaño. Y todo se actualiza solo gracias a los SSE. Si un contenedor se cae, en menos de un segundo ves el cambio en el dashboard. Sin refrescar la página, sin F5, sin nada.

Puedes filtrar por nombre, por imagen, por estado o por stack. Si tienes treinta contenedores y quieres ver solo los que están caídos, un clic. Esa sensación de tenerlo todo controlado desde una sola pantalla es difícil de explicar hasta que lo pruebas.

Control del ciclo de vida

Desde el dashboard puedes iniciar, parar, reiniciar y eliminar cualquier contenedor. También puedes inspeccionarlo y ver toda la información: mounts, redes, puertos, variables de entorno, labels. Todo lo que te da docker inspect, pero en una interfaz clara y navegable.

Y si usas Traefik como reverse proxy —que deberías—, Alloy te enlaza directamente a los contenedores gestionados por Traefik. Un clic y ves la configuración del router, del middleware, del servicio.

Gestión de stacks

Alloy descubre automáticamente los proyectos de Docker Compose que tienes en tu servidor. Detecta los contenedores que pertenecen al mismo stack —por el proyecto de compose— y los agrupa. Puedes ver el stack como una unidad, arrancar todos los servicios, pararlos, reiniciarlos, o actualizar el stack entero con docker compose pull + docker compose up -d.

Esto es clave cuando tienes servicios que dependen unos de otros. No quieres actualizar el contenedor de la base de datos sin actualizar también la aplicación que la usa. Con Alloy, seleccionas el stack y aplicas la actualización a todo el conjunto.

Actualizaciones automáticas e inteligentes

Aquí es donde Alloy brilla. Y te diría que esta es la característica que más me ha costado implementar, pero también la que más orgulloso me hace sentir. Tienes varios niveles:

  • Actualización individual: seleccionas un contenedor, pulsas Actualizar, y Alloy hace docker pull de la imagen más reciente y reinicia el contenedor. Todo desde la interfaz.
  • Actualización masiva con control: pulsas Check all, y Alloy compara el digest local de cada imagen con el digest remoto en el registro. Te devuelve una lista con los contenedores que tienen actualizaciones disponibles. Tú seleccionas cuáles quieres actualizar, y el proceso se lanza en segundo plano con una barra de progreso en tiempo real vía SSE.
  • Políticas de actualización por contenedor: puedes configurar por cada contenedor qué política quieres: none —no actualizar nunca—, pull —solo descargar la imagen sin reiniciar—, pull+restart —descargar y reiniciar—, o pull+restart+stack —descargar, reiniciar y si forma parte de un stack, actualizar todo el stack.
  • Worker automático: un worker en segundo plano, configurable, que cada cierto tiempo comprueba el registro y aplica las políticas automáticamente. Así te olvidas de las actualizaciones.
  • Historial de actualizaciones: todo queda registrado: qué imagen se actualizó, de qué digest a qué digest, cuándo, y si fue exitoso. Un audit trail completo.

Y ojo, que cuando hace una actualización con reinicio, Alloy verifica que el contenedor arranca correctamente. Si tiene una política de rollback activada —que también es configurable— y el contenedor no responde después de la actualización, etiqueta la imagen anterior y la restaura automáticamente. Cero downtime si todo va bien, y si algo falla, vuelta atrás sin que te enteres.

Alertas configurables

Puedes crear reglas de alerta para contenedores concretos. Por ejemplo: Si el contenedor de Nextcloud se para, notifícame por Telegram. O Si cualquier contenedor con la etiqueta ‘critical’ cambia a estado exited, notifícame. Las alertas funcionan con el worker de estado, que cada 30 segundos comprueba transiciones.

Tareas programadas

¿Quieres que un contenedor se reinicie todos los días a las 6 de la mañana? ¿O que se pare a las 2 AM y arranque a las 8? Creas una tarea programada con expresión cron, seleccionas la acción —start, stop, restart, update— y el contenedor, y Alloy se encarga. El scheduler worker evalúa las expresiones cada minuto y ejecuta lo que toque.

Notificaciones

Telegram, Matrix y Webhook. Configuras los tokens, las URLs, y Alloy te avisa cuando hay cambios de estado, actualizaciones completadas, o cuando se dispara una alerta. Todo desde el panel de administración.

Limpieza del sistema

Prune de contenedores detenidos, imágenes no usadas, networks y volúmenes huérfanos. Todo desde un botón. Te avisa de cuánto espacio vas a liberar antes de hacer la purga.

Y todo esto con autenticación OIDC. No hay formulario de login propio, no hay tabla de usuarios. Te autenticas contra tu proveedor OIDC —Authentik, PocketID, Keycloak, Google, el que quieras— y Alloy valida el token JWKS. Sesiones con cookies httponly firmadas. Y para las conexiones SSE, puedes pasar el token como cookie, como Bearer header, o como query parameter. Flexible pero seguro.

Fíjate en lo que no necesita Alloy: no necesita PostgreSQL, no necesita Redis, no necesita ninguna base de datos externa. El estado de las alertas, el historial de actualizaciones, las tareas programadas, la configuración… todo se guarda en SQLite embebido. Si tienes que hacer backup, copias data/alloy.db. Si tienes que restaurar, pegas el archivo. Punto.

Instalación: cómo poner Alloy en marcha

Vamos a la práctica. Te voy a enseñar cómo se instala Alloy y qué te encuentras cuando entras.

Requisito número uno, obligatorio: un proveedor OIDC

Como te he dicho, Alloy no tiene login propio. Necesitas un proveedor OIDC. Si ya usas Authentik o PocketID para otras aplicaciones, ya lo tienes. Si no, este es el momento de montarlo. No es complicado, tengo episodios dedicados a ello —el 666 y el 667—, y hay guías en el blog.

Instalación con Docker Compose

El método más rápido. Creas una carpeta, clonas el repositorio y configuras las variables de entorno:

git clone https://github.com/atareao/alloy.git /opt/alloy
cd /opt/alloy

Alloy se configura con variables de entorno. Las cuatro obligatorias son:

PORT=3066
OIDC_ISSUER_URL=https://auth.tudominio.com/application/o/alloy/
OIDC_CLIENT_ID=alloy
OIDC_CLIENT_SECRET=tu-secreto-super-seguro
OIDC_REDIRECT_URL=https://alloy.tudominio.com/api/auth/callback

Y arrancas:

docker compose up -d

En menos de diez segundos, Alloy está corriendo. Para comprobarlo:

docker compose logs -f

Verás algo parecido a esto:

📦 Alloy 0.20.7 starting up...
🔌 Connected to Docker daemon
🔐 OIDC provider reachable at auth.tudominio.com
📁 SQLite persistence at /app/data
🌐 Listening on http://[::]:3066

El archivo compose.yml completo lo tienes disponible en este gist:

Y si prefieres un script automatizado que haga todo el trabajo sucio, aquí tienes el lanzador que comprueba prerequisitos, genera el compose y arranca todo:

Instalación con Podman y Quadlet

Para los que usáis Podman —que sois muchos en la comunidad—, Alloy incluye un archivo .container de Quadlet.

cp alloy.container ~/.config/containers/systemd/
mkdir -p ~/.local/share/alloy/data
systemctl --user daemon-reload
systemctl --user start alloy
systemctl --user enable alloy

Y a correr. El Quadlet monta el socket de Docker —o de Podman si descomentas la variable DOCKER_HOST—, persiste los datos en ~/.local/share/alloy/data, y se reinicia automáticamente si falla.

Primer acceso

Abres https://alloy.tudominio.com y te redirige a tu proveedor OIDC. Te autenticas —con tu usuario de Authentik, por ejemplo— y ya estás dentro.

La interfaz tiene varias pestañas:

  • Dashboard: la vista principal. Todos tus contenedores en una tabla con estado en tiempo real, ordenables, filtrables. Cada contenedor tiene botones de acción: start, stop, restart, inspect, update. Y una columna con el estado de actualización: si hay una imagen más nueva, te aparece un indicador.
  • Stacks: los proyectos de Docker Compose agrupados. Ves los servicios de cada stack, su estado, y botones para operar sobre el stack completo.
  • Schedule: las tareas programadas. Creas, editas, eliminas tareas cron. Ves el historial de ejecuciones.
  • Alerts: las reglas de alerta. Configuras qué contenedores monitorizar y qué acción tomar.
  • History: el historial completo de actualizaciones. Cada pull, cada restart, con timestamp y digests.
  • Settings: la configuración global. Notificadores —Telegram, Matrix, Webhook—, políticas de actualización por defecto, intervalo del auto-update worker, etc.

Demo rápida: Alloy en acción

Vamos a hacer una demo rápida. Abro el Dashboard. Veo mis treinta contenedores. Filtro por exited y veo que tengo uno caído: un contenedor de pruebas que se me olvidó arrancar. Pulsas Start y en menos de un segundo el contenedor está running. La interfaz se actualiza sola sin que hagas nada.

Ahora voy a la pestaña de Stacks. Veo tres stacks: el de media con Jellyfin, Sonarr, Radarr, Transmission; el de monitoring con Grafana, Prometheus, Node Exporter; y el de web con WordPress y MariaDB. En el stack de media veo que hay actualizaciones pendientes. Pulsas Update stack, y Alloy tira del compose, hace pull de las imágenes nuevas, y recrea los servicios. Mientras tanto, una barra de progreso en el frontend te muestra cómo va cada contenedor. Cuando termina, recibes una notificación en el dashboard y, si lo has configurado, en Telegram.

Vuelvo al Dashboard y pulso Check all. Alloy empieza a comparar los digests locales con los remotos. En unos segundos, me dice que 5 de mis 30 contenedores tienen actualizaciones. Selecciono los que quiero —tres críticos que quiero mantener al día— y pulso Update selected. El proceso se lanza en segundo plano, veo el progreso en tiempo real, y cuando termina, el historial queda registrado.

Todo esto, sin tocar el terminal ni una sola vez.

El código: aspectos interesantes de la implementación

Y ahora, para los que os gusta el código, os cuento algunos detalles jugosos de cómo está implementado Alloy.

13 módulos, unas 9.000 líneas de Rust

No es un proyecto pequeño, pero tampoco es un monstruo. Y está todo en un solo binario. El frontend se compila y se incrusta dentro del ejecutable de Rust usando include_dir!. Cuando sirves la aplicación, no hay que servir archivos estáticos por separado. Es un único proceso, un único binario. Lo copias y funciona.

Bollard para la comunicación con Docker

Bollard es la crate de referencia para interactuar con el API de Docker desde Rust. Y está muy bien hecha. Conectas al socket Unix, y tienes acceso a todo: listar contenedores, escuchar eventos, inspeccionar imágenes, gestionar redes y volúmenes.

let docker = if let Ok(host) = std::env::var("DOCKER_HOST") {
    if let Some(path) = host.strip_prefix("unix://") {
        bollard::Docker::connect_with_socket(path, 120, bollard::API_DEFAULT_VERSION)
    } else {
        bollard::Docker::connect_with_http(&host, 120, bollard::API_DEFAULT_VERSION)
    }
} else {
    bollard::Docker::connect_with_local_defaults()
};

Para la parte de actualizaciones, Alloy habla directamente con el registro de Docker Hub. No usa Docker para eso —bueno, para el pull sí—, pero la comparación de digests se hace contra el API de registro. El flujo es:

  1. Parseas la referencia de la imagen → obtienes repo y tag.
  2. Pides un token Bearer a auth.docker.io.
  3. Consultas el manifest en registry-1.docker.io/v2/{repo}/manifests/{tag}.
  4. Extraes el config digest remoto.
  5. Comparas con el digest local.

Todo esto en paralelo para todos los contenedores, usando tokio::join! o futures::future::join_all. Por eso el Check all es tan rápido.

SSE con tokio::broadcast

Los canales de Server-Sent Events están implementados con tokio::broadcast, que es un canal de difusión con buffer. Cuando un worker detecta un cambio de estado, envía un mensaje al canal, y todos los clientes conectados lo reciben. El frontend tiene un EventSource por cada canal —events, updates, notifications— y reacciona en consecuencia.

async fn sse_events_h(
    State(tx): State<broadcast::Sender<StateEvent>>,
) -> Sse<impl futures::Stream<Item = Result<Event, Infallible>>> {
    let stream = BroadcastStream::new(tx.subscribe()).filter_map(|r| match r {
        Ok(evt) => future::ready(Some(Ok(Event::default()
            .event("containers")
            .json_data(evt)
            .unwrap()))),
        Err(_) => future::ready(None),
    });
    Sse::new(stream).keep_alive(KeepAlive::default())
}

OIDC desde cero

La autenticación OIDC está implementada manualmente, sin usar crates de terceros. El flujo es el estándar: redirect al provider, callback con code, intercambio por token, validación JWKS, y sesión con cookie firmada. El middleware de autenticación en Axum es limpio: si la ruta empieza por /api/, verifica la cookie o el header. Si es la SPA, sirve el index.html y deja que el frontend redirija al login.

Políticas de actualización por contenedor

Esto fue interesante de implementar. Cada contenedor tiene una política: None, Pull, PullRestart, PullRestartStack. La política se resuelve primero a nivel de contenedor —mirando labels de Docker— y si no está definida, se usa la política global. Cuando se ejecuta una actualización masiva, el sistema itera sobre los contenedores pendientes, aplica la política correspondiente, y para PullRestartStack resuelve el archivo compose a partir de los labels del contenedor.

Persistencia en SQLite embebido

No hay base de datos externa. Todo el estado persistente —alertas, tareas programadas, historial de actualizaciones, configuración— se guarda en SQLite dentro del archivo /app/data/alloy.db. La lógica de persistencia está en db.rs usando deadpool-sqlite y rusqlite con modo WAL. Es simple, funciona, y el backup es copiar un solo archivo.

Y una curiosidad: el proyecto tiene unos 140 tests en el backend y 18 en el frontend. Todos pasando en CI. Y el linting con clippy está configurado para que no se pueda mergear nada que no pase. No es que sea un purista del testing, pero para un proyecto que gestiona tus contenedores —literalmente el control de tu infraestructura—, prefiero tener tests.

Reflexiones sobre el proceso

He probado Portainer, Dockge, y mil más. Portainer es la navaja suiza, pero pesa. Dockge es fantástico para stacks, pero se queda corto en visión global. Y todas las soluciones que he probado tienen algo en común: o son demasiado pesadas, o dependen de una base de datos externa, o no tienen actualizaciones automáticas, o no tienen alertas configurables, o no tienen OIDC.

Al final, cuando sabes lo que quieres, te lo haces tú mismo.

Y eso es Alloy. No es que sea mejor que Portainer en todo —Portainer tiene décadas de desarrollo y un equipo enorme detrás—, pero Alloy hace exactamente lo que yo necesito, de la forma en que yo lo necesito, y con el stack tecnológico que yo quiero.

Rust en el backend me da la tranquilidad de saber que no se va a caer por un error de memoria, que no va a consumir recursos de más, y que va a responder rápido incluso cuando tengo treinta contenedores con actualizaciones pendientes. He tenido el servidor funcionando semanas sin tener que reiniciar el proceso de Alloy, y eso es algo que con otras herramientas no podía decir.

React con Mantine me da una interfaz que no da vergüenza enseñar. Porque una cosa es que funcione bien, y otra es que sea agradable de usar. Alloy es de esas herramientas que abres y dices esto mola. La experiencia de usuario ha sido una prioridad desde el primer día, y se nota en los detalles: las transiciones suaves, los colores de estado que cambian en tiempo real, las barras de progreso de las actualizaciones…

Y la decisión de no usar base de datos externa —todo en SQLite embebido— es deliberada. Cuando tienes un servidor self-hosted, cuantas menos dependencias tengas, mejor. Si Alloy petara —que no peta—, borras el archivo data/alloy.db, reinicias, y vuelves a empezar. No hay migraciones de base de datos, no hay schemas que mantener, no hay nada que configurar. Es casi como una appliance.

Una cosa que me gusta especialmente es el flujo de actualización con rollback. En producción, esto es oro. Tener la posibilidad de que Alloy, tras actualizar un contenedor, verifique que responde correctamente, y si no, restaure la imagen anterior automáticamente… Eso te quita el miedo a pulsar Actualizar. Porque todos hemos tenido esa actualización que rompe algo un viernes por la tarde.

Y las tareas programadas con cron abren un montón de posibilidades. ¿Quieres que el contenedor de descargas se pare a las 2 de la madrugada y arranque a las 8? Lo configuras en dos clics. ¿Quieres que un contenedor de base de datos se reinicie cada domingo a las 5 AM para liberar memoria? Una tarea cron y listo.

¿Y las alertas? Poder configurar que si un contenedor crítico se cae, te llegue un mensaje a Telegram al instante… eso es tener el servidor controlado sin estar mirándolo constantemente. Te puedo asegurar que la tranquilidad que da saber que te vas a enterar si algo falla no tiene precio.

Por cierto, si usas Authentik como proveedor OIDC —que es mi recomendación—, la integración es directa. Creas un provider OAuth2/OpenID en Authentik, le pones la redirect URI a https://alloy.tudominio.com/api/auth/callback, y Alloy hace el discovery automático del issuer. JWKS validation incluida. No tienes que tocar nada más.

Para terminar

Hoy hemos visto el problema de gestionar decenas de contenedores sin un dashboard visual. Hemos visto qué es Alloy: un dashboard Docker en tiempo real con backend en Rust/Axum y frontend React/Mantine. Hemos recorrido sus características principales: monitorización SSE, control de ciclo de vida, stacks, actualizaciones inteligentes con políticas y rollback, alertas, tareas programadas, notificaciones y limpieza del sistema. Hemos visto cómo instalarlo con Docker Compose y Podman Quadlet. Y hemos visto algunos detalles jugosos de la implementación: Bollard, tokio::broadcast, OIDC, persistencia SQLite.

Si te ha picado la curiosidad, todo está en GitHub: github.com/atareao/alloy. Allí encuentras el código, la documentación, el compose de ejemplo, y el Quadlet para Podman. Si te gusta, dale una estrella —que siempre hace ilusión—, y si encuentras algo que mejorar, abre un issue o un PR.

Como siempre, todas las referencias, los comandos y los enlaces los tienes a continuación.


Más información

Deja una respuesta