Estrategia de actualización Docker: actualizaciones sin tiempo de inactividad en un VPS
Cuatro métodos progresivos para actualizar contenedores Docker en un VPS, desde pull-and-replace hasta despliegues blue-green sin interrupciones con Traefik. Fijación de imágenes, rollback, notificaciones con Diun y docker-rollout.
Actualizar contenedores Docker en un VPS no tiene que significar tiempo de inactividad. El enfoque correcto depende de lo que estés ejecutando y cuánta interrupción puedas tolerar. Un blog personal aguanta unos segundos de inactividad durante un docker compose up -d. Un producto SaaS con clientes de pago, no.
Esta guía cubre cuatro métodos, del más simple al más resiliente. Cada uno se construye sobre el anterior. Empieza con el que se ajuste a tu situación y pasa al siguiente nivel cuando lo necesites.
Requisitos previos: Un VPS con Debian 12 o Ubuntu 24.04 con Docker Engine 27+ y Docker Compose v2 instalados. Todos los comandos usan la sintaxis del plugin docker compose (no el binario obsoleto docker-compose v1). Docker en producción en un VPS: qué falla y cómo solucionarlo
¿Cómo fijo las imágenes Docker a una versión específica?
Fija tus imágenes a una versión minor o patch en tu archivo compose. El tag latest es un blanco móvil que puede introducir cambios que rompan cosas sin previo aviso. La fijación te da control sobre cuándo ocurren las actualizaciones y hace posible el rollback al conservar la imagen anterior localmente.
Diferentes estrategias de tags conllevan diferentes niveles de riesgo:
| Formato del tag | Ejemplo | Nivel de riesgo | Comportamiento de actualización |
|---|---|---|---|
latest |
nginx:latest |
Alto | Cualquier versión, en cualquier momento. No puedes saber qué cambió. |
| Solo major | nginx:1 |
Medio-alto | Puede saltar de 1.25 a 1.27. Las versiones minor pueden cambiar el comportamiento. |
| Minor | nginx:1.27 |
Bajo | Recibe actualizaciones patch (1.27.0 a 1.27.3). Seguro para la mayoría de workloads. |
| Patch | nginx:1.27.3 |
Muy bajo | Versión exacta. Sin actualizaciones sorpresa. Actualizas manualmente. |
| Digest | nginx:1.27.3@sha256:6f12... |
Mínimo | Imagen idéntica byte a byte cada vez. Inmune a la mutación de tags. |
Para la mayoría de los servicios en producción, fija a la versión minor (image: postgres:16.6). Es el equilibrio correcto entre parches de seguridad y estabilidad. Para servicios donde la reproducibilidad importa (CI, entornos regulados), fija al digest completo.
services:
app:
image: myapp:2.4.1
# Not: image: myapp:latest
db:
image: postgres:16.6
Registra los digests actuales de tus imágenes antes de actualizar. Los necesitarás para un rollback:
docker image inspect --format='{{index .RepoDigests 0}}' $(docker compose images app -q)
myapp@sha256:a1b2c3d4e5f6...
¿Cómo configuro health checks en Docker Compose?
Los health checks le indican a Docker si tu contenedor está realmente funcionando, no solo ejecutándose. Todos los patrones de actualización sin downtime dependen de ellos. Sin un health check, Docker no tiene forma de saber si el nuevo contenedor está listo antes de eliminar el antiguo.
Añade un bloque healthcheck a cada servicio en tu archivo compose. El comando test se ejecuta dentro del contenedor en el intervalo especificado. Docker marca el contenedor como healthy solo después de que el test pase.
services:
app:
image: myapp:2.4.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
Qué hace cada campo:
- test: El comando a ejecutar.
CMDlo ejecuta directamente. UsaCMD-SHELLsi necesitas funcionalidades de shell como pipes. - interval: Tiempo entre verificaciones. 15s es razonable para servicios web.
- timeout: Cuánto esperar a que el comando termine antes de considerarlo fallido.
- retries: Número de fallos consecutivos antes de que Docker marque el contenedor como
unhealthy. - start_period: Periodo de gracia tras el inicio del contenedor. Los health checks durante esta ventana no cuentan para el umbral de fallos. Configúralo con tiempo suficiente para que tu aplicación arranque.
Para servicios que no tienen curl instalado, usa la verificación nativa que ofrece el servicio:
db:
image: postgres:16.6
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
cache:
image: redis:7.4
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
Después de iniciar tus servicios, comprueba que los health checks están pasando:
docker compose ps
NAME IMAGE STATUS PORTS
app myapp:2.4.1 Up 2 minutes (healthy) 0.0.0.0:8080->8080/tcp
db postgres:16.6 Up 2 minutes (healthy) 5432/tcp
El estado (healthy) significa que tu health check está configurado y pasando. Si ves (health: starting), el contenedor aún está en su start_period. Si ves (unhealthy), revisa los logs del health check:
docker inspect --format='{{json .State.Health}}' $(docker compose ps -q app) | python3 -m json.tool
¿Cómo actualizo un contenedor Docker en mi VPS?
Ejecuta docker compose pull para obtener la nueva imagen, luego docker compose up -d para reemplazar el contenedor. Docker Compose detiene el contenedor antiguo, lo elimina e inicia uno nuevo desde la imagen actualizada. Esto causa una breve interrupción (normalmente 2-10 segundos) mientras el nuevo contenedor arranca y pasa su health check.
Paso a paso: la actualización simple
Antes de actualizar, haz una copia de seguridad de tus volúmenes. Una actualización fallida con datos corruptos es mucho peor que unos segundos de inactividad.
Lee el changelog de la nueva versión. Busca cambios que rompan cosas, opciones de configuración obsoletas y pasos de migración necesarios. Lleva cinco minutos y ahorra horas de debugging.
# Pull the new image
docker compose pull app
# Check what changed
docker compose up -d --dry-run
El flag --dry-run (Docker Compose v2.20+) muestra lo que Compose hará sin ejecutarlo realmente. Verás qué contenedores se recrearán:
DRY RUN MODE - service "app" - Pull
DRY RUN MODE - Container app-1 - Recreate
DRY RUN MODE - Container app-1 - Started
Aplica la actualización:
docker compose up -d app
[+] Running 1/1
✔ Container app-1 Started 0.8s
Comprueba que el nuevo contenedor está healthy:
docker compose ps app
NAME IMAGE STATUS PORTS
app myapp:2.5.0 Up 15 seconds (healthy) 0.0.0.0:8080->8080/tcp
Luego prueba desde fuera del servidor para asegurarte de que el servicio es accesible:
curl -s -o /dev/null -w "%{http_code}" https://app.example.com/health
200
¿Cuándo actualizar tus contenedores Docker?
No todas las actualizaciones tienen la misma urgencia. Un calendario de actualizaciones uniforme lleva a riesgos innecesarios o a parches de seguridad perdidos.
- Parches de seguridad (CVEs): Aplícalos inmediatamente. Suscríbete a los avisos de seguridad de tus imágenes. Un CVE conocido en un contenedor público se explota en horas tras su divulgación, no días.
- Versiones patch (ej.: 2.4.1 a 2.4.2): Planifícalas semanal o quincenalmente. Son correcciones de bugs. Lee el changelog, actualiza, verifica.
- Versiones minor (ej.: 2.4 a 2.5): Planifícalas mensualmente. Prueba primero en un entorno de staging si tienes uno. Revisa el changelog por cambios de comportamiento.
- Versiones major (ej.: 2.x a 3.x): Planifica y prueba. Las versiones major rompen cosas. Lee la guía de migración. Prueba en un VPS separado o en local antes de tocar producción.
¿Cómo hago rollback de un contenedor Docker a una imagen anterior?
Docker Compose no tiene un comando de rollback integrado. Para revertir: edita tu archivo compose para fijar el tag o digest de la imagen anterior, luego ejecuta docker compose up -d. El contenedor se reinicia con la imagen antigua. Esto solo funciona si conservaste la imagen antigua localmente (no ejecutes docker image prune justo después de actualizar).
Rollback paso a paso
Supongamos que actualizaste myapp de 2.4.1 a 2.5.0 y la nueva versión está rota.
- Comprueba que la imagen antigua sigue disponible localmente:
docker images myapp
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp 2.5.0 abc123def456 2 hours ago 185MB
myapp 2.4.1 789fed654cba 2 weeks ago 182MB
- Edita tu archivo compose para fijar la versión anterior:
services:
app:
image: myapp:2.4.1
- Ejecuta el rollback:
docker compose up -d app
[+] Running 1/1
✔ Container app-1 Started 0.7s
- Comprueba que el rollback funcionó:
docker compose ps app
NAME IMAGE STATUS PORTS
app myapp:2.4.1 Up 10 seconds (healthy) 0.0.0.0:8080->8080/tcp
Si ya purgaste la imagen antigua, Docker la descargará de nuevo del registro (asumiendo que el tag todavía existe). Para máxima seguridad, anota el digest completo (sha256:...) antes de actualizar. Los digests son inmutables. Los tags pueden ser sobrescritos.
Automatízalo con un script de pre-actualización
Guarda el estado actual antes de cada actualización para que el rollback esté siempre a un comando de distancia:
#!/bin/bash
# save-state.sh - Run before every update
COMPOSE_FILE="${1:-docker-compose.yml}"
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="./rollback/${DATE}"
mkdir -p "${BACKUP_DIR}"
cp "${COMPOSE_FILE}" "${BACKUP_DIR}/"
docker compose ps --format json > "${BACKUP_DIR}/containers.json"
docker compose images --format json > "${BACKUP_DIR}/images.json"
echo "State saved to ${BACKUP_DIR}"
chmod 700 save-state.sh
¿Watchtower sigue mantenido en 2026?
Watchtower fue archivado el 17 de diciembre de 2025. Los mantenedores ya no usan Docker y detuvieron el desarrollo. La última versión es la v1.7.1. Más importante aún, el SDK Docker de Watchtower usa la API v1.25, pero Docker Engine 29 elevó la versión mínima de la API a v1.44. Watchtower es incompatible con las versiones actuales de Docker a menos que bajes manualmente el mínimo de la API con DOCKER_MIN_API_VERSION=1.25 en la configuración de tu daemon. Eso es un parche, no una solución.
Si usas Watchtower hoy, planifica la migración. Para notificaciones de actualizaciones automatizadas sin reinicios automáticos, usa Diun. Para actualizaciones automatizadas sin interrupciones, usa docker-rollout detrás de un reverse proxy.
¿Cómo te notifica Diun sobre actualizaciones de imágenes Docker?
Diun (Docker Image Update Notifier) vigila tus registros Docker y envía notificaciones cuando hay nuevas versiones de imágenes disponibles. No actualiza contenedores. Te avisa de que existe una actualización para que puedas leer el changelog y actualizar bajo tus propios términos. Es el enfoque de «saber antes de actuar».
Añade Diun a tu archivo compose existente o crea uno dedicado:
services:
diun:
image: crazymax/diun:4
command: serve
volumes:
- "diun-data:/data"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
environment:
TZ: "Europe/Berlin"
DIUN_WATCH_WORKERS: "10"
DIUN_WATCH_SCHEDULE: "0 6 * * *"
DIUN_PROVIDERS_DOCKER: "true"
DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
DIUN_NOTIF_SLACK_WEBHOOKURL_FILE: "/run/secrets/slack_webhook"
secrets:
- slack_webhook
restart: unless-stopped
secrets:
slack_webhook:
file: ./secrets/slack_webhook.txt
volumes:
diun-data:
La URL del webhook de Slack va en un archivo de secrets, no en una variable de entorno, porque Docker secrets la mantiene fuera de la salida de docker inspect y los listados de procesos. Crea el archivo de secrets con permisos restringidos:
mkdir -p secrets
echo "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" > secrets/slack_webhook.txt
chmod 600 secrets/slack_webhook.txt
Configuraciones principales:
- DIUN_WATCH_SCHEDULE: Expresión cron.
0 6 * * *comprueba cada día a las 06:00. Ajústalo a tu ventana de mantenimiento. - DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: Cuando es
true, Diun vigila todos los contenedores en ejecución. Ponlo afalsey usa labels para monitorización selectiva. - Montaje del socket Docker: Solo lectura (
:ro) porque Diun solo lee metadatos de contenedores. Nunca inicia ni detiene contenedores.
Para monitorización selectiva (recomendado para stacks más grandes), pon WATCHBYDEFAULT a false y añade labels a los contenedores que quieras monitorizar:
services:
app:
image: myapp:2.4.1
labels:
- "diun.enable=true"
- "diun.watch_repo=true"
Inicia Diun y revisa los logs:
docker compose up -d diun
docker compose logs diun --tail 20
diun | Thu, 19 Mar 2026 06:00:01 CET INF Starting Diun version=v4.31.0
diun | Thu, 19 Mar 2026 06:00:01 CET INF Configuration loaded from 5 environment variable(s)
diun | Thu, 19 Mar 2026 06:00:02 CET INF Cron triggered
diun | Thu, 19 Mar 2026 06:00:03 CET INF New image found image=docker.io/myapp:2.5.0 provider=docker
Cuando Diun encuentra una nueva imagen, envía un mensaje de Slack con el nombre de la imagen, el tag actual y el nuevo tag. Tú decides si y cuándo actualizar.
¿Cómo logra docker-rollout actualizaciones sin interrupciones?
docker-rollout es un plugin CLI de Docker que realiza despliegues blue-green para servicios Compose. Inicia un nuevo contenedor desde la imagen actualizada, espera a que el health check pase y luego elimina el contenedor antiguo. El tráfico nunca alcanza un contenedor no operativo porque el reverse proxy solo enruta hacia contenedores healthy.
Requisitos:
- Un reverse proxy (Traefik, Caddy o nginx-proxy) que enrute tráfico a tu servicio
- Health checks definidos en tu archivo compose
- Sin directiva
container_nameen el servicio (docker-rollout gestiona los nombres de contenedores) - Sin mapeo directo de
portsen el servicio (el reverse proxy gestiona la exposición de puertos)
Instalar docker-rollout
mkdir -p /usr/local/lib/docker/cli-plugins
curl -fsSL https://raw.githubusercontent.com/wowu/docker-rollout/main/docker-rollout \
-o /usr/local/lib/docker/cli-plugins/docker-rollout
chmod +x /usr/local/lib/docker/cli-plugins/docker-rollout
Debería mostrar su versión una vez instalado:
docker rollout --version
docker-rollout version v0.13
Ejemplo: Traefik + docker-rollout
Un archivo compose mínimo para una aplicación web detrás de Traefik con health checks. La app no tiene ports ni container_name porque docker-rollout necesita gestionar el escalado.
services:
traefik:
image: traefik:3.3
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
restart: unless-stopped
app:
image: myapp:2.4.1
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=web"
- "traefik.http.services.app.loadbalancer.server.port=8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
restart: unless-stopped
Desplegar sin interrupciones
Obtén la nueva imagen y usa docker rollout en lugar de docker compose up -d:
docker compose pull app
docker rollout app
==> Scaling 'app' to '2' instances
Container myproject-app-2 Creating
Container myproject-app-2 Created
Container myproject-app-2 Starting
Container myproject-app-2 Started
==> Waiting for new containers to be healthy (timeout: 60 seconds)
==> Stopping and removing old containers
Durante este proceso, Traefik detecta el nuevo contenedor a través del socket Docker, enruta tráfico hacia él una vez que está healthy y deja de enrutar al contenedor antiguo antes de eliminarlo. Tus usuarios no ven ninguna interrupción.
Si el nuevo contenedor falla su health check, docker-rollout aborta y el contenedor antiguo sigue funcionando. Sin necesidad de intervención manual.
¿Qué es el despliegue blue-green con Docker y Traefik?
El despliegue blue-green ejecuta dos copias de tu servicio (blue y green). Una sirve el tráfico en vivo mientras la otra permanece inactiva. Para desplegar, actualizas la copia inactiva, verificas que funciona y luego cambias el tráfico. Esto te da rollback instantáneo cambiando de vuelta a la copia anterior.
Este es el concepto detrás de docker-rollout, pero puedes implementarlo manualmente para más control. Un ejemplo mínimo usando la configuración dinámica de Traefik:
services:
traefik:
image: traefik:3.3
command:
- "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- "./traefik/dynamic:/etc/traefik/dynamic:ro"
restart: unless-stopped
app-blue:
image: myapp:2.4.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
app-green:
image: myapp:2.4.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
Un archivo de configuración dinámica de Traefik controla qué copia recibe el tráfico:
# traefik/dynamic/app.yml
http:
routers:
app:
rule: "Host(`app.example.com`)"
service: app
entryPoints:
- web
services:
app:
loadBalancer:
servers:
- url: "http://app-blue:8080"
Para desplegar: actualiza la imagen de app-green, inícialo, espera a que esté healthy y luego edita app.yml para apuntar a app-green. Traefik recoge el cambio automáticamente porque watch=true. Para hacer rollback, edita el archivo para apuntar de vuelta a app-blue.
Este enfoque requiere más trabajo que docker-rollout. Úsalo cuando necesites control explícito sobre el cambio, quieras ejecutar smoke tests contra la nueva versión antes de cambiar el tráfico, o cuando varios servicios deban cambiar juntos.
¿Qué método de actualización debería usar?
Elige el método que se ajuste a tu tolerancia de inactividad y complejidad de infraestructura.
| Método | Inactividad | Complejidad | Rollback | Reverse proxy necesario | Adecuado para |
|---|---|---|---|---|---|
docker compose pull + up |
2-10 segundos | Baja | Manual (editar archivo compose) | No | Proyectos personales, herramientas internas |
| Diun + actualización manual | Igual | Baja | Igual | No | Equipos que quieren visibilidad antes de actualizar |
| docker-rollout | Ninguna | Media | Automático (aborta en caso de fallo) | Sí | Servicios de producción en un VPS único |
| Blue-green (manual) | Ninguna | Alta | Instantáneo (cambiar archivo de config) | Sí | Stacks multi-servicio, entornos regulados |
Flujo de decisión:
- ¿Son aceptables 2-10 segundos de inactividad? Usa
docker compose pull && docker compose up -d. - ¿Quieres saber de las actualizaciones antes de aplicarlas? Añade Diun.
- ¿Se requiere zero downtime? ¿Tienes un reverse proxy? Usa docker-rollout.
- ¿Necesitas control explícito sobre el cambio de tráfico? Implementa blue-green manualmente.
¿Algo salió mal?
El contenedor arranca pero muestra (unhealthy)
Revisa el comando del health check. Ejecútalo manualmente dentro del contenedor:
docker compose exec app curl -f http://localhost:8080/health
Si falla, el problema está en tu aplicación, no en Docker. Revisa los logs de la aplicación:
docker compose logs app --tail 50
La imagen antigua fue purgada, no se puede hacer rollback
Si el tag todavía existe en el registro, docker compose pull la descargará. Si fijaste por digest, Docker descarga la imagen exacta sin importar los cambios de tags:
image: myapp:2.4.1@sha256:789fed654cba...
docker-rollout se queda colgado durante el despliegue
El health check no pasa dentro del timeout. Revisa el intervalo y los retries del health check. Aumenta el timeout:
docker rollout -t 120 app
Watchtower dejó de funcionar tras actualizar Docker
Docker Engine 29 requiere la API v1.44 como mínimo. Watchtower usa la API v1.25. Migra a Diun para notificaciones o docker-rollout para actualizaciones automatizadas sin interrupciones.
Diun no detecta nuevas imágenes
Revisa el horario cron en DIUN_WATCH_SCHEDULE. Lanza un escaneo manual:
docker compose exec diun diun image list
Revisa los logs de Diun por errores de autenticación con el registro:
docker compose logs diun --tail 30
Copyright 2026 Virtua.Cloud. Todos los derechos reservados. Este contenido es una obra original del equipo de Virtua.Cloud. La reproducción, republicación o redistribución sin permiso escrito está prohibida.
¿Listo para probarlo?
Despliega tu propio servidor en segundos. Linux, Windows o FreeBSD.
Ver planes VPS