ads

Latest Update

recent

Latest Update

random

Docker Says Healthy but Traffic Still Fails: Fix Health Checks in Compose and Kubernetes

Docker · Kubernetes · Self-hosting

Docker Says "Healthy" but Traffic Still Fails: How to Fix Health Checks in Compose and Kubernetes

A green container status can still leave users staring at errors. This guide shows which health signal controls traffic, how to repair Compose startup dependencies, and how to configure Kubernetes probes.

Reviewed: August 2026

A green Docker health status can be completely real and still fail to protect your users. Docker Engine can run a HEALTHCHECK and record the result, while the system sending traffic uses a different health signal. That is the trap behind many restart-time errors in home labs and small production clusters.[1][5]

The fix is to match the check to the layer that makes the routing or restart decision. Docker Compose can wait for a dependency marked service_healthy. Kubernetes needs its own readinessProbe, livenessProbe, and, for slow applications, startupProbe.[2][3]

Quick answer

If you run Docker Compose, define a real healthcheck and reference it with depends_on: condition: service_healthy when another service must wait for it. If you run Kubernetes, add a readinessProbe to control traffic and a separate livenessProbe to request a restart. Do not assume a Dockerfile HEALTHCHECK automatically becomes a Kubernetes probe. It does not.[2][3][5]

Diagram showing Docker container health checks feeding Kubernetes readiness-based traffic routing
Docker health status and Kubernetes readiness answer different questions before traffic reaches an application.

Why a healthy container can still break requests

A Docker health check is an instruction executed by Docker against a container. Docker stores the result in the container state, which you can inspect with docker inspect.[1][4] That result is useful, but it is not a universal routing protocol.

Compose can use health status when you explicitly configure a dependency condition. Compose otherwise waits for a dependency to be running, not necessarily ready to accept requests.[2] Kubernetes has its own probe fields. Its readiness result controls whether a Pod receives traffic through a Kubernetes Service, while liveness failures tell the kubelet to restart a container.[3]

This creates a familiar failure pattern. An application process starts, Docker reports healthy because its internal command succeeds, but the application is still warming a cache or waiting for a database. If the routing layer is not reading that exact signal, requests arrive too early.

A recent DEV Community post made the same distinction after observing Docker's state alongside Kubernetes behavior. It is a useful community signal, not proof of how every deployment behaves.[5]

Step 1: Find out which layer owns the decision

Before changing YAML, identify who should stop traffic or restart the process.

Environment Signal that controls traffic Signal that controls restart First place to inspect
Docker Compose Compose dependency conditions, or your proxy Restart policy and operator action docker compose ps
Docker Engine alone Your proxy or client Docker restart policy docker inspect
Docker Swarm Swarm service state Swarm service policy docker service ps
Kubernetes readinessProbe livenessProbe kubectl describe pod
Slow Kubernetes startup Readiness after startup probe succeeds Liveness after startup probe succeeds Pod events and probe settings

Do not use a liveness check as a traffic check just because both call /health. A process can be alive but unable to serve a request safely. That is exactly why Kubernetes documents liveness and readiness as separate mechanisms.[3]

Step 2: Inspect Docker's actual health state

Start with the container name:

docker compose ps
docker ps --format 'table {{.Names}}\\t{{.Status}}'

Then inspect the health record directly:

docker inspect --format='{{json .State.Health}}' app

Replace app with the real container name. docker inspect returns low-level information about Docker objects and supports Go templates for selecting fields.[4]

If the container has no health check, the command prints <no value> or an empty result. If a check exists, inspect its status and recent log entries. A health check can be failing because the image does not contain curl, because it checks the wrong port, or because the endpoint returns success before its dependencies are usable.

A minimal Compose check might look like this:

services:
  app:
    image: ghcr.io/example/app:1.4
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/health || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 20s

Use a command present in the image. Alpine, Debian, and distroless images do not necessarily contain the same diagnostic tools. If the image has no suitable client, use an executable health endpoint or build the check into the image rather than assuming curl exists.

The endpoint should test the state you care about. A check that only confirms that the HTTP process is listening will not catch a database connection that the application cannot use.

Step 3: Make Compose wait for readiness

A health check alone does not make every dependent service wait. Compose's documented solution is a dependency condition:

services:
  web:
    image: ghcr.io/example/web:1.4
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:18
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: change-this-value
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

The doubled dollar sign matters in Compose because it prevents Compose from expanding the variable before the command reaches the container. Use a real secret mechanism for production credentials rather than leaving a password in a file committed to a repository.

Bring the stack up and watch the state:

docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail=100 web db

Compose starts services in dependency order and waits for a dependency marked service_healthy to pass its health check before creating the dependent service.[2] docker compose config catches YAML and interpolation mistakes before the restart makes the problem harder to read.

If the web service still fails, check the database logs and run the health command manually:

docker compose exec db pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"

A green database check only proves the check succeeded. It does not prove that your application's migrations, permissions, or queries are correct.

Step 4: Translate the check for Kubernetes

Kubernetes does not use the image's Docker health status as its readiness decision. Put the required behavior in the Pod specification instead.[3][5]

Here is a small deployment fragment for an HTTP service:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: ghcr.io/example/web:1.4
          ports:
            - containerPort: 8080
          startupProbe:
            httpGet:
              path: /startup
              port: 8080
            periodSeconds: 5
            failureThreshold: 30
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /live
              port: 8080
            periodSeconds: 15
            timeoutSeconds: 3
            failureThreshold: 3

Use the probes for different questions. The startup probe gives a slow application time to initialize. The readiness probe removes a Pod from Service traffic when the application cannot safely serve requests. The liveness probe asks whether the process needs a restart.[3]

Do not point all three probes at an endpoint that always returns 200. That configuration only proves that the web server responds. Design /ready to include the dependencies that must be available for a request to succeed, but keep the liveness endpoint narrow enough that a temporary database outage does not restart every replica.

Apply the manifest and inspect the result:

kubectl apply -f web-deployment.yaml
kubectl get pods -l app=web -o wide
kubectl describe pod -l app=web
kubectl get endpointslice -l kubernetes.io/service-name=web

Kubernetes documents that a failed readiness probe prevents a Pod from receiving traffic through Services, while a failed liveness probe can cause the kubelet to kill and restart the container.[3] Pod events are the quickest way to spot a wrong path, port, timeout, or image that does not listen where the manifest says it does.

Step 5: Diagnose the common failures

Docker reports unhealthy

Read the health log:

docker inspect --format='{{range .State.Health.Log}}{{println .Start "exit=" .ExitCode .Output}}{{end}}' app

If the output says curl: not found, install the tool only if it belongs in the image's support model, or switch to a check that the image can run. If the check receives connection refused, confirm the application listens on the container port, not just on a host-published port.

Compose starts the app too early

Check that the dependency contains both a healthcheck and condition: service_healthy. A plain depends_on expresses startup order, but Compose's documentation distinguishes starting a container from waiting until its service is ready.[2]

Kubernetes sends traffic to a starting Pod

Check readinessProbe, not the Dockerfile. If readiness is absent, the Service has no application-specific readiness result to use. Confirm the probe path and port from inside the Pod where practical:

kubectl describe pod web-xxxxx
kubectl logs deploy/web --tail=100
kubectl get pod web-xxxxx -o jsonpath='{.status.conditions[*]}'

Kubernetes restarts a slow application

Add a startupProbe or increase the startup budget rather than weakening liveness until it becomes meaningless. Kubernetes uses startup probes to protect slow-starting containers from liveness checks during initialization.[3]

The check is green but users still see errors

The check may be too shallow. Test the dependency path that matters to users, then check the proxy, Service endpoints, and application logs. A health endpoint can be correct for the process and still incomplete for the transaction.

A safer health-check design

Keep health checks cheap, local, and specific. A liveness check should answer whether restarting the process may recover it. A readiness check should answer whether this instance should receive traffic now. A Compose dependency check should answer whether the dependent service can begin its own startup.

Avoid putting destructive actions in a health command. The command runs repeatedly, often with elevated container privileges. It should observe state and return an exit code, not mutate a database or delete temporary files.

Set a restart policy deliberately. Docker documents unless-stopped, always, and on-failure as different behaviors, and restart policies do not replace health checks.[6] A restart policy can bring back a process that exited; it does not decide whether a running but broken process should receive requests.

FAQ

Does Kubernetes read a Dockerfile HEALTHCHECK?

No. Kubernetes uses the probe fields in the Pod specification. Define a readinessProbe, livenessProbe, or startupProbe explicitly for the behavior you need.[3][5]

Is a Docker health check enough for Docker Compose?

Not always. Compose can wait for a health check when you use depends_on with condition: service_healthy. Without that condition, a dependency may be running before it is ready to accept requests.[2]

Should readiness and liveness use the same URL?

They can, but separate endpoints usually make the failure policy clearer. Readiness can include dependencies that must be available for traffic, while liveness should avoid restarting a healthy process because an external dependency is temporarily unavailable.

Why does docker ps say healthy while Kubernetes still routes traffic?

Docker and Kubernetes maintain separate health mechanisms. Docker records its own result, while Kubernetes routes according to its readiness probe and Pod conditions. A Docker result does not substitute for a Kubernetes readiness probe.[3][5]

How do I see the last Docker health-check error?

Run docker inspect --format='{{json .State.Health}}' container-name and read the Log entries. Docker's inspect command exposes low-level object information and supports formatted output.[4]

Final recommendation

Treat health checks as contracts between layers, not as a single green badge. In Compose, pair healthcheck with service_healthy when startup dependencies matter. In Kubernetes, write separate startup, readiness, and liveness behavior, then verify the result with Pod events and Service endpoints. The few minutes spent matching each check to the decision-maker will save a long night of debugging traffic that should never have arrived.

Sources

[1] https://docs.docker.com/reference/dockerfile — Dockerfile reference [2] https://docs.docker.com/compose/how-tos/startup-order — Compose startup order [3] https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes — Kubernetes probes [4] https://docs.docker.com/reference/cli/docker/inspect — Docker inspect [5] https://dev.to/jtorchia/docker-says-healthy-and-the-pod-keeps-sending-broken-traffic-ghj — DEV trend signal [6] https://docs.docker.com/engine/containers/start-containers-automatically — Start containers automatically

No comments:

Please Don't Spam Comment Box !!!!

All Rights Reserved by Bikram Bhujel © 2019 - 2030
Powered By Bikram Bhujel, Designed by Bikram Bhujel
Powered by Blogger.