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 anteponer sudo a los comandos docker.

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:

EstadoSignificado
startingEl contenedor acaba de arrancar y aún no ha superado ninguna comprobación.
healthyLa última comprobación ha sido correcta.
unhealthyHan fallado retries comprobaciones seguidas.

Estas son las opciones disponibles y sus valores por defecto:

OpciónPor defectoUso
interval30sTiempo entre comprobaciones.
timeout30sTiempo máximo que puede tardar el comando; si se supera, cuenta como fallo.
retries3Fallos seguidos necesarios para marcar el contenedor como unhealthy.
start_period0sMargen de arranque: los fallos durante este periodo no cuentan.
start_interval5sIntervalo entre comprobaciones durante el start_period (Docker 25 o posterior).

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_isready viene incluido en la imagen oficial de PostgreSQL y sale con 0 solo 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 web no define healthcheck, porque hereda el HEALTHCHECK de su Dockerfile. Si quisieras desactivarlo en Compose usarías healthcheck: { 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 /health que 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 interval en cada réplica. Evita consultas pesadas o llamadas a servicios externos.
  • Ajusta start_period al arranque real. Aplicaciones Java o con migraciones pueden tardar un minuto en estar listas; sin margen, se marcarán como unhealthy antes de terminar de arrancar.
  • Usa timeout menor que interval. 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 subcomando app 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.