Problema
En muchos homelabs y entornos de desarrollo se ejecutan decenas de servicios que apenas se usan. Mantener todos los contenedores activos consume RAM, CPU y, en algunos casos, licencias de red. La práctica habitual es apagar los servicios manualmente o confiar en scripts de cron que los inician a horas predefinidas. Ese enfoque falla cuando un visitante inesperado necesita acceso inmediato: la primera petición llega a un puerto que no responde, el usuario recibe un error y el administrador tiene que intervenir.
El patrón que surge es “servicio siempre disponible vs. consumo de recursos”. La solución ideal es que el contenedor se inicie automáticamente al primer request y se apague tras un periodo de inactividad, sin que el usuario final tenga que refrescar o contactar al admin. Además, para aplicaciones que usan TCP puro (por ejemplo, servidores de juegos) el proxy debe poder mantener la conexión mientras el contenedor arranca.
Causa
- Proxy estático – La mayoría de los reverse proxies (NGINX, Traefik) asumen que el backend está siempre arriba. Cuando el backend está detenido, la conexión se cierra inmediatamente.
- Falta de orquestación de arranque – Docker Compose no incluye lógica de “wake‑on‑connect”. Sólo define dependencias estáticas y políticas de reinicio.
- Ausencia de métricas de arranque – Sin historial de tiempos de inicio, el proxy no puede estimar cuánto tardará el contenedor y el usuario recibe solo un “502”.
- Gestión de puertos TCP – Los proxies HTTP no interceptan tráfico raw TCP, por lo que servicios como Minecraft quedan fuera del modelo “wake‑on‑demand”.
- Hooks inexistentes – Operaciones previas al arranque (montar discos, validar licencias) y posteriores al apagado (backup, notificaciones) son necesarias en entornos reales, pero Docker Compose no las expone de forma genérica.
Solución
Implementar un proxy lazy‑loading que actúe como puerta de enlace para cualquier servicio, sea HTTP o TCP. El proxy debe:
- Detectar la primera conexión entrante.
- Lanzar el contenedor (o cualquier proceso) mediante
docker compose up -d <service>o un script personalizado. - Mantener la conexión abierta mientras el contenedor arranca.
- Emitir una página de estado que muestre logs en tiempo real y una barra de progreso basada en históricos de arranque.
- Cambiar a la aplicación real tan pronto como el puerto del contenedor esté disponible.
- Registrar la última actividad y programar un apagado después de un timeout configurable.
- Ejecutar hooks antes y después del ciclo de vida del servicio.
Arquitectura mínima
Cliente ──► Proxy (HTTP/TCP) ──► Docker daemon
El proxy se ejecuta como contenedor independiente, lo que simplifica la instalación y permite versionarlo con Docker Compose. Dentro del proxy se usan:
- WebSocket para transmitir logs a la página de arranque.
- Docker SDK (Python o Go) para lanzar y detener servicios.
- Persistencia ligera (SQLite) para guardar tiempos de arranque y calcular estimaciones.
- Configuración YAML donde cada servicio declara: nombre, puerto externo, puerto interno, timeout, hooks.
Implementación práctica
-
Crear el compose del proxy
version: "3.8" services: wakeproxy: build: ./wakeproxy ports: - "80:80" - "25565:25565" # ejemplo TCP para Minecraft volumes: - /var/run/docker.sock:/var/run/docker.sock - ./hooks:/app/hooks environment: - DEFAULT_TIMEOUT=300El contenedor monta el socket de Docker, lo que le permite lanzar cualquier stack definido en el host.
-
Definir los servicios objetivo en un archivo
services.ymlque el proxy leerá:services: immich: compose_path: /home/user/immich/docker-compose.yml http_port: 8080 startup_estimate: 40 hooks: pre_start: hooks/immich_pre.sh post_stop: hooks/immich_post.sh minecraft: compose_path: /home/user/mc/docker-compose.yml tcp_port: 25565 startup_estimate: 20 -
Hooks
Cada hook es un script ejecutable que recibe el nombre del servicio como argumento. Por ejemplo, montar un NAS antes de iniciar Immich:#!/usr/bin/env bash mount -t nfs nas:/share /mnt/immich -
Lógica de arranque (pseudo‑código resumido):
- Al recibir una petición, buscar el servicio en
services.yml. - Si el contenedor está detenido, ejecutar
docker compose -f <compose_path> up -d. - Abrir un WebSocket que lea
docker logs -fy lo reenvíe al cliente. - Cada 2 s comprobar si el puerto interno está escuchando (
nc -z localhost <port>). - Cuando el puerto responde, cerrar la página de estado y redirigir al cliente.
- Registrar la hora de última actividad.
- Un watchdog revisa los timestamps y ejecuta
docker compose -f <compose_path> downcuando supera el timeout.
- Al recibir una petición, buscar el servicio en
-
Modo TCP
Para puertos raw, el proxy abre un socket listener, inicia el contenedor y, mientras el backend no está listo, mantiene el socket abierto y reenvía bytes una vez que el puerto interno responde. No hay página HTML, pero la conexión del cliente no se corta.
Cuándo aplicar esta solución
- Homelabs con servicios esporádicos – fotos, medios, juegos que solo usan recursos cuando alguien los visita.
- Entornos de pruebas donde se necesita lanzar stacks rápidamente sin consumir todo el hardware.
- Equipos pequeños que no pueden invertir en orquestadores más complejos (Kubernetes) pero quieren evitar contenedores siempre activos.
No es recomendable cuando:
- Los SLA exigen disponibilidad al 100 % y el tiempo de arranque supera los segundos críticos.
- Los servicios dependen de hardware que tarda mucho en inicializar (por ejemplo, GPUs).
- La exposición de logs en la página de arranque representa un riesgo de seguridad; en ese caso desactive la transmisión o limite el acceso.
Código
# Lanzar el proxy (asumiendo que el Dockerfile ya está preparado)
docker compose up -d --build
# Comprobar que el proxy está escuchando
curl -I http://localhost
# Debería devolver 200 con la página de estado si el backend está dormido
# Forzar el arranque de un servicio manualmente (útil para pruebas)
docker compose -f /home/user/immich/docker-compose.yml up -d immich
# Simular inactividad y apagado automático
sleep 310 && docker compose -f /home/user/immich/docker-compose.yml down immich
Verificación
-
Petición HTTP
Abrir el navegador enhttp://<proxy>/immich- La página muestra “Starting…”, barra de progreso y logs en tiempo real.
- Tras ~40 s (según histórico) la URL cambia a la aplicación Immich y los logs desaparecen.
-
Conexión TCP
Ejecutarnc <proxy_ip> 25565mientras el contenedor está dormido.- La consola queda bloqueada, indicando que el proxy mantiene la conexión.
- Cuando Minecraft está listo, la sesión de juego arranca sin que el cliente tenga que reconectar.
-
Hook de post‑stop
Revisar que el scripthooks/immich_post.shse ejecutó creando un archivo de log en/var/log/immich_shutdown.log. -
Timeout
Dejar el servicio sin actividad > 5 min y confirmar que el contenedor desaparece condocker ps -a | grep immich.
Notas adicionales
- Seguridad de logs – La página de estado expone
docker logs. Limite su acceso con autenticación básica o IP whitelist. - Persistencia de estimaciones – El proxy guarda los tiempos de arranque en SQLite; respalde ese archivo si el host se reinicia.
- Escalado – En entornos con cientos de servicios, considere separar la tabla de configuración en una base de datos externa y usar workers asíncronos para lanzar contenedores.
- Compatibilidad con Docker Compose v2 – El comando
docker compose(espacio) es la versión recomendada; si su host solo tiene la CLI clásica (docker-compose), ajuste los scripts en consecuencia. - Depuración – Active el modo verbose del proxy (
PROXY_LOG=debug) para obtener trazas de decisiones de arranque y shutdown.
Con esta arquitectura se consigue un equilibrio entre disponibilidad bajo demanda y consumo mínimo de recursos, aplicable a cualquier homelab que necesite mantener varios servicios “dormidos” sin sacrificar la experiencia del usuario.