Un contenedor puede estar en ejecución y aun así no funcionar: el proceso sigue vivo, pero la aplicación no responde o ha perdido la conexión con la base de datos. Los healthchecks de Docker ejecutan periódicamente un comando dentro del contenedor y marcan su estado como healthy o unhealthy. En este tutorial añadirás un healthcheck a una aplicación propia, provocarás un fallo para ver cómo cambia el estado y usarás Docker Compose para que un servicio no arranque hasta que su base de datos esté lista, todo en Ubuntu 24.04.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios
sudo. - Docker Engine 25 o posterior y el plugin de Docker Compose instalados desde el repositorio oficial de Docker.
- Tu usuario en el grupo
docker, o anteponersudoa los comandosdocker.
Comprueba las versiones antes de empezar:
docker version --format '{{.Server.Version}}'
docker compose version
Cómo funciona un healthcheck
Docker ejecuta el comando de comprobación dentro del contenedor cada cierto intervalo. Si el comando sale con código 0, la comprobación es correcta; si sale con 1, es un fallo. Tras varios fallos seguidos el contenedor pasa a unhealthy.
Un contenedor con healthcheck tiene uno de estos tres estados:
| Estado | Significado |
|---|---|
starting | El contenedor acaba de arrancar y aún no ha superado ninguna comprobación. |
healthy | La última comprobación ha sido correcta. |
unhealthy | Han fallado retries comprobaciones seguidas. |
Estas son las opciones disponibles y sus valores por defecto:
| Opción | Por defecto | Uso |
|---|---|---|
interval | 30s | Tiempo entre comprobaciones. |
timeout | 30s | Tiempo máximo que puede tardar el comando; si se supera, cuenta como fallo. |
retries | 3 | Fallos seguidos necesarios para marcar el contenedor como unhealthy. |
start_period | 0s | Margen de arranque: los fallos durante este periodo no cuentan. |
start_interval | 5s | Intervalo entre comprobaciones durante el start_period (Docker 25 o posterior). |
ImportanteDocker Engine por sí solo solo informa del estado. Un contenedor
unhealthyno se reinicia automáticamente, ni siquiera con--restart always. Quien actúa sobre ese estado es Docker Swarm (que sustituye la tarea), Docker Compose condepends_ono tu sistema de monitorización.
Paso 1: Probar un healthcheck desde la línea de comandos
Antes de tocar ningún Dockerfile, puedes definir un healthcheck al lanzar un contenedor con las opciones --health-* de docker run. Este ejemplo arranca Redis y comprueba que responde a PING:
docker run -d --name redis-test \
--health-cmd "redis-cli ping || exit 1" \
--health-interval 10s \
--health-timeout 3s \
--health-retries 3 \
--health-start-period 5s \
redis:7
Consulta el estado con docker ps. En los primeros segundos verás health: starting y después healthy:
docker ps --filter name=redis-test
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f2a1c9d8e7b redis:7 "docker-entrypoint.s…" 15 seconds ago Up 14 seconds (healthy) 6379/tcp redis-test
El || exit 1 convierte cualquier código de error en 1, que es el único valor de fallo que Docker espera. Elimina el contenedor de prueba:
docker rm -f redis-test
Paso 2: Añadir HEALTHCHECK a un Dockerfile
Lo habitual es declarar el healthcheck en la propia imagen, para que cualquiera que la ejecute lo herede. Vas a crear una pequeña aplicación HTTP en Python con un endpoint /health que devuelve 200 mientras no exista el archivo /tmp/unhealthy, lo que te permitirá simular un fallo en el paso 4.
Crea un directorio para el proyecto:
mkdir -p ~/healthcheck-demo && cd ~/healthcheck-demo
Crea el archivo de la aplicación:
nano app.py
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
if os.path.exists("/tmp/unhealthy"):
self.send_response(503)
self.end_headers()
self.wfile.write(b"unhealthy\n")
return
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok\n")
return
self.send_response(200)
self.end_headers()
self.wfile.write(b"Hola desde el contenedor\n")
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
Ahora crea el Dockerfile:
nano Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
USER nobody
EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \
CMD python -c "import urllib.request, sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2).status == 200 else 1)" || exit 1
CMD ["python", "app.py"]
La imagen python:3.12-slim no incluye curl ni wget, así que el healthcheck usa el propio intérprete de Python. Es una buena práctica general: usa una herramienta que ya exista en la imagen en lugar de instalar curl solo para esto. Si la petición devuelve un 503, urlopen lanza una excepción, Python sale con un código distinto de cero y || exit 1 lo normaliza a 1.
Construye la imagen:
docker build -t healthcheck-demo .
Verifica que el healthcheck ha quedado registrado en la imagen:
docker image inspect healthcheck-demo --format '{{json .Config.Healthcheck}}'
{"Test":["CMD-SHELL","python -c \"import urllib.request, sys; ...\" || exit 1"],"Interval":10000000000,"Timeout":3000000000,"StartPeriod":10000000000,"Retries":3}
Los tiempos aparecen en nanosegundos.
Paso 3: Ejecutar el contenedor y consultar su estado
Arranca un contenedor a partir de la imagen:
docker run -d --name web -p 8000:8000 healthcheck-demo
Espera unos segundos y comprueba el estado:
docker inspect web --format '{{.State.Health.Status}}'
healthy
Docker guarda el resultado de las últimas cinco comprobaciones, con su código de salida y la salida del comando. Es lo primero que debes mirar cuando un contenedor aparece como unhealthy:
docker inspect web --format '{{json .State.Health}}' | python3 -m json.tool
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "2026-09-25T10:12:31.402118Z",
"End": "2026-09-25T10:12:31.498233Z",
"ExitCode": 0,
"Output": ""
}
]
}
Paso 4: Simular un fallo
Crea el archivo que hace que /health devuelva 503:
docker exec web touch /tmp/unhealthy
Con interval=10s y retries=3, el contenedor pasará a unhealthy en unos 30 segundos. Puedes seguir el cambio en tiempo real con los eventos de Docker (pulsa Ctrl+C para salir):
docker events --filter container=web --filter event=health_status
2026-09-25T10:14:02.118Z container health_status: unhealthy 8c1d... (image=healthcheck-demo, name=web)
Confirma el estado y el contador de fallos:
docker inspect web --format '{{.State.Health.Status}} {{.State.Health.FailingStreak}}'
unhealthy 3
Observa que el contenedor sigue en ejecución (docker ps muestra Up ... (unhealthy)). Docker no lo reinicia. Borra el archivo y en la siguiente comprobación volverá a healthy:
docker exec web rm /tmp/unhealthy
Cuando termines, elimina el contenedor:
docker rm -f web
Paso 5: Healthchecks en Docker Compose
En Compose el healthcheck se define por servicio con la clave healthcheck, y otros servicios pueden esperar a que esté sano con depends_on y condition: service_healthy. Así evitas el clásico error de una aplicación que arranca antes de que PostgreSQL acepte conexiones.
Crea el archivo compose.yaml en el mismo directorio:
nano compose.yaml
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: your_strong_password
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "app", "-d", "app"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
start_interval: 2s
web:
build: .
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
volumes:
db_data:
Sustituye your_strong_password por una contraseña robusta. Algunos detalles de este archivo:
pg_isreadyviene incluido en la imagen oficial de PostgreSQL y sale con0solo cuando el servidor acepta conexiones.- La forma
["CMD", ...]ejecuta el comando directamente, sin shell. Usa["CMD-SHELL", "..."]cuando necesites operadores como||o variables de entorno. - El servicio
webno definehealthcheck, porque hereda elHEALTHCHECKde su Dockerfile. Si quisieras desactivarlo en Compose usaríashealthcheck: { disable: true }.
Levanta la pila con --wait, que hace que Compose no devuelva el control hasta que todos los servicios estén en ejecución y sanos:
docker compose up -d --build --wait
✔ Container healthcheck-demo-db-1 Healthy
✔ Container healthcheck-demo-web-1 Healthy
Comprueba el estado de ambos servicios:
docker compose ps
NAME IMAGE SERVICE STATUS PORTS
healthcheck-demo-db-1 postgres:16 db Up 40 seconds (healthy) 5432/tcp
healthcheck-demo-web-1 healthcheck-demo-web web Up 28 seconds (healthy) 0.0.0.0:8000->8000/tcp
El servicio web arrancó solo cuando db pasó a healthy. --wait es también útil en scripts de despliegue: si algún servicio acaba en unhealthy, el comando devuelve un código de error.
Paso 6: Buenas prácticas para el comando de comprobación
Un healthcheck mal diseñado genera falsos positivos o carga innecesaria. Ten en cuenta lo siguiente:
- Comprueba la aplicación, no solo el proceso. Un endpoint
/healthque valide lo imprescindible (por ejemplo, que la aplicación puede abrir una conexión a su base de datos) es mejor que comprobar que un puerto está abierto. - Mantén el comando ligero. Se ejecuta cada
intervalen cada réplica. Evita consultas pesadas o llamadas a servicios externos. - Ajusta
start_periodal arranque real. Aplicaciones Java o con migraciones pueden tardar un minuto en estar listas; sin margen, se marcarán comounhealthyantes de terminar de arrancar. - Usa
timeoutmenor queinterval. Si no, las comprobaciones se solapan con la siguiente. - No dependas de herramientas ausentes. Si la imagen es mínima (distroless,
scratch), incluye un pequeño binario de comprobación en la propia aplicación, por ejemplo un subcomandoapp healthcheck.
Solución de problemas
El contenedor está siempre en starting. El comando nunca ha tenido éxito y el start_period todavía no ha terminado. Revisa .State.Health.Log con docker inspect: la salida suele indicar el problema.
exec: "curl": executable file not found. La imagen no incluye curl. Usa una herramienta que exista en la imagen (wget en imágenes Alpine, python en imágenes de Python, pg_isready en PostgreSQL) o instálala en el Dockerfile.
El healthcheck funciona a mano pero falla en Docker. El comando se ejecuta con el usuario definido en USER y sin tu entorno de shell. Pruébalo exactamente igual con docker exec nombre_contenedor sh -c 'comando'.
Compose arranca el servicio aunque la base de datos no esté lista. Revisa que depends_on use la forma larga con condition: service_healthy; la lista simple (depends_on: [db]) solo espera a que el contenedor arranque.
Conclusión
Has añadido un healthcheck a una imagen propia, has visto cómo pasa de healthy a unhealthy y cómo diagnosticarlo con docker inspect, y has usado depends_on con service_healthy y docker compose up --wait para ordenar el arranque de una pila. Como siguientes pasos, puedes desplegar la pila en Docker Swarm para que sustituya automáticamente las tareas que fallen, enviar los eventos health_status a tu sistema de alertas o combinar los healthchecks con un proxy inverso que solo envíe tráfico a contenedores sanos.
