Vibe monitoring with Last9 MCP: Ask your agent to fix production issues! Setup →
Last9 Last9

Docker Status Unhealthy: What It Means and How to Fix It

What Docker’s “unhealthy” status means, why it happens, and how to debug failing containers with clarity and control.

Jul 4th, ‘25
Docker Status Unhealthy: What It Means and How to Fix It
See How Last9 Works

Unified observability for all your telemetry. Open standards. Simple pricing.

Talk to us

If your container shows Status: unhealthy, Docker's health check is failing. The container is still running, but something inside, usually your app, isn’t responding as expected.

This doesn’t always mean a crash. It just means Docker can’t verify the app is working. Here’s how to debug the issue and restore the container to a healthy state.

How Docker Health Status Works

Docker runs health checks separately from the container's lifecycle. Even if a container is running, Docker can still mark it as unhealthy if the health check command fails.

A health check runs inside the container at set intervals. It typically hits an endpoint or runs a command to check if your app is alive and responding.

There are three possible health states:

  • starting: The container is still in its startup period.
  • healthy: The last few health checks passed.
  • unhealthy: Multiple health checks failed.

The container's health status depends on the exit code from the command:

  • 0: Healthy
  • 1: Unhealthy
  • Anything else: Inconclusive, Docker leaves the status unchanged.
If your container health checks are failing due to memory constraints, see how to configure Docker’s shared memory size to prevent those issues.

Inspect Health Check Logs

1. Check the Health Logs

Start by inspecting the container’s health details. This helps you see exactly what Docker’s health check is doing and why it’s failing.

Use this command:

docker inspect --format "{{json .State.Health }}" container_name | jq

It returns structured output with:

  • Current health status (starting, healthy, unhealthy)
  • Failing streak count
  • A log of recent health checks (with timestamps, exit codes, and output)

If jq isn’t installed, drop it for raw JSON:

docker inspect --format "{{json .State.Health }}" container_name

Look for ExitCode: 0 (success) or ExitCode: 1 (failure). Any output from the failed checks can help narrow down the issue.

Debug Health Check Commands

Test your health check command directly inside the container to isolate the issue:

docker exec -it container_name curl -f http://localhost:8080/health
echo $?

This approach lets you see exactly what your health check encounters. You might discover that the health endpoint returns unexpected status codes, takes too long to respond, or that required tools like curl aren't available in your container.

For more complex health checks, examine the command that Docker runs. Look at your Dockerfile or docker-compose.yml to understand the exact test being performed:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost/api/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

Setup Health Checks in Docker and Docker Compose

Health checks help Docker understand if your containerized app is working, not just running. Here’s how to add them to your setup and make sure they reflect real application health.

Add a Basic Health Check in Your Dockerfile

Use the HEALTHCHECK instruction to define how Docker monitors your container.

For a simple HTTP service:

HEALTHCHECK CMD curl --fail http://localhost:8080 || exit 1

This tells Docker to run that command inside the container. If curl gets a successful response, Docker marks the container as healthy. If not, it sets the status to unhealthy.

You can customize how often Docker runs the check and how sensitive it is:

HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl --fail http://localhost:8080/health || exit 1
  • interval: Time between checks (default: 30s)
  • timeout: Max time Docker waits for the check to finish (default: 30s)
  • start-period: Grace period after startup when failures are ignored
  • retries: How many failures trigger an unhealthy state
If your health check exits are triggering restarts, it helps to understand the difference between docker stop and kill and how each impacts container shutdown.

Use Custom Health Check Scripts

If your app needs more than a single HTTP check, write a script:

COPY healthcheck.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/healthcheck.sh
HEALTHCHECK CMD /usr/local/bin/healthcheck.sh

Example healthcheck.sh:

#!/bin/bash

# Check Postgres
pg_isready -h localhost -p 5432 -U myuser || exit 1

# Check API
curl -f http://localhost:8080/api/health || exit 1

# Check Redis
curl -f http://redis:6379/ping || exit 1

exit 0

Keep the script fast and lightweight. Avoid expensive operations or long-running queries,health checks shouldn’t add overhead.

Write Checks That Reflect Real Application Health

A health check should confirm the app can serve requests, not just that the process exists.

Good examples:

  • HTTP services: curl -f http://localhost:8080/api/health
  • Databases: pg_isready -h localhost -p 5432 -U myuser
  • Message brokers: nc -z rabbitmq 5672

Avoid checks that only confirm the process is alive (e.g., checking PID files or running ps). Your container can look “healthy” even if the app is broken.

Tune Your Health Check Parameters

Health checks are great, until they get too noisy or too slow. Most of that comes down to timing. Set the wrong values, and you’ll either miss real issues or end up restarting healthy containers.

Here’s how to get each setting right.

interval: How Often to Run the Check

This controls how often Docker runs the health check.

  • Set it too low, and your container spends half its time checking itself.
  • Set it too high, and you won’t catch failures quickly.

Tip: 10–30 seconds is a good starting point. Go shorter for critical services that need fast detection.

timeout: How Long to Wait for a Response

If your app takes a while to respond—especially under load—short timeouts can cause false alarms.

Tip: Match this to your app’s real response time. If most endpoints return in under a second, 2–5 seconds should be plenty.

retries: How Many Fails Before Giving Up

Some apps stall occasionally—say, during GC or burst load. One failure doesn’t always mean something’s wrong.

Tip:

  • Use higher retries (3–5) for apps that sometimes hiccup.
  • Lower retries (1–2) are better if you want to fail fast and restart quickly.

start_period: Grace Time After Startup

Some apps need a bit to get going—connecting to databases, loading config, etc. Without a grace period, the health check might fail before the app is even ready.

Tip: If your app takes 30 seconds to boot, set a start_period of 30–60 seconds. It’ll save you from false starts and restart loops.

Some health check failures can be caused by paused containers, this guide explains what happens when you pause a Docker container.

Health Checks in docker-compose

Docker Compose lets you define health checks alongside service dependencies. Here’s an example:

version: '3.8'

services:
  web:
    build: .
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    image: postgres:13
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

Use docker-compose ps to see the health status of each container in your stack.

What to Do When a Container Turns Unhealthy

By default, Docker won’t restart a container just because it’s marked unhealthy. That’s intentional, health checks are diagnostic, not reactive.

If you're running standalone containers, you’ll need to wire up your own restart logic. One common option is the autoheal container, which watches for unhealthy containers and restarts them.

Another approach: combine your health check with a manual process exit. That way, Docker’s built-in restart policies can kick in.

HEALTHCHECK --interval=5m --timeout=2m --start-period=45s \
  CMD curl -f --retry 3 --max-time 5 http://localhost:8080/health || kill -s 15 1

This example uses curl to check an endpoint. If it fails, the container’s main process (PID 1) is terminated, triggering a restart, assuming you’ve set a --restart policy.

In orchestrated setups (like Docker Swarm or Kubernetesthe ), this is usually handled for you. The scheduler watches health states and automatically replaces failing containers as part of service management.

Quick Wins

Short on time? Here's how to get an unhealthy container back on track quickly.

1. Check What Failed

Inspect health logs:

docker inspect --format "{{json .State.Health }}" container_name | jq

Look for recent Output, ExitCode, and error messages.

2. Test the Health Check Inside the Container

Match the container’s environment:

docker exec -it container_name sh -c '<your health check command>'

This catches issues with missing ports, permissions, or dependencies.

3. Fix the Most Common Causes

  • App crash: Logs show connection refused or stack traces
  • Missing dependency: DB or API call fails inside the container
  • Slow startup or load: Health check times out repeatedly
  • Wrong health check config: Mismatched port or URL path

4. Adjust the Health Check Settings

Tweak the timing so Docker doesn’t panic too early:

HEALTHCHECK --interval=30s --timeout=10s --start-period=45s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

5. Let Docker Restart It

To enable restarts:

  • Use autoheal
  • Or exit the process on failure to trigger the restart policy:
CMD curl -f http://localhost:8080/health || kill -s 15 1
Now, fix Docker container health issues instantly, right from your IDE, with AI and Last9 MCP. Bring real-time production context, logs, metrics, and traces, into your local environment to debug and fix failing health checks faster.

Common Causes of Unhealthy Containers

The container's up, but health checks are failing. Here's what to look for and what the symptoms often tell you.

Crashing Applications

The most straightforward case: the app inside the container has crashed. Health checks that hit a port or check a process will fail because nothing is listening anymore.

What you might see:
In the health log:

Output: curl: (7) Failed to connect to localhost port 8080: Connection refused

What to check:

  • Run docker logs <container> to check for stack traces or crash messages.
  • Look for OOM kills (check dmesg if running locally, or container metrics if using a platform).
  • If it’s a language runtime crash (e.g., Node.js, Python), you’ll usually find it in the logs.

Broken Dependencies

Sometimes the app is fine, but a dependency isn't. Maybe your database isn’t reachable. Maybe an API is timing out. From Docker’s point of view, the container looks alive. But your app can’t do its job.

What you might see:

Output: psql: could not connect to server: Connection refused

What to check:

  • Is the dependent service running?
  • Can you connect manually using docker exec + curl or psql?
  • If the issue happens during boot, maybe the health check runs too early—add retries or a delay.

Resource Exhaustion

The app hasn't crashed, but it’s hanging. Not enough memory, high CPU, or a full disk can make the health check time out.

What you might see:

  • Empty Output with a long delay
  • Health check keeps failing with timeouts

What to check:

  • Use docker stats to monitor memory and CPU usage in real-time.
  • Run df -h or free -m inside the container to check the disk and memory.
  • If the container is hitting limits, increase them or reduce the app load.

Misconfigured Health Checks

This one’s easy to miss. The app is working, but the health check is pointing to the wrong place.

What you might see:

Output: curl: (7) Failed to connect to localhost port 8080: Connection refused

when the app is running on port 3000.

What to check:

  • Does the health check use the right port and path?
  • Is the app listening on localhost, 0.0.0.0, or something else?
  • If it needs env vars to start up, make sure they’re available during the health check.
Last9 Review
Last9 Review

Monitor Health Events in Real Time

Docker lets you track health status changes as they happen:

docker events --filter event=health_status

This is useful when you're debugging flapping containers or trying to correlate startup issues with unhealthy states. But it's local, ephemeral, and doesn’t scale beyond your terminal.

If you're running multiple containers in production, you need better visibility.

At Last9, we make it easy to monitor container health over time, alongside other application signals. Here's how we help:

  • Track container health as a time series: Instead of combing through docker eventsLast9 stores health status transitions so you can graph unhealthy periods and correlate with CPU, memory, or downstream failures.
  • Set alerts on flapping health states: Detect containers that frequently toggle between healthy and unhealthy, and notify only when it matters—no noisy alerts.
  • Correlate with traces and logs: Use our OpenTelemetry-native ingestion to link health check failures with slow spans, failed DB calls, or error logs, without needing to jump between tools.
  • Visualize readiness across environments: Whether you're running containers locally, in staging, or across multiple clusters, Last9 shows you which services are consistently healthy (and which ones aren't).

Get started with us today or book sometime with us!

And if you’d like to go deeper or have a specific use case to discuss, our Discord community is open. There’s a dedicated channel where you can talk through details with other developers.

FAQs

Q: Why is my container marked as unhealthy when it's still running?
A: Docker treats the container’s State (running/stopped) separately from its health check results. If your health check fails repeatedly, Docker marks it unhealthy, even while the container stays up. Use docker inspect on .State.Health to see what’s failing.

Q: How do I see which health check command is breaking?
A: Check the full configuration with:

docker inspect container_name

Look for the Healthcheck section to see the exact CMD used. To test it manually, run it inside the container:

docker exec -it container_name sh -c '<healthcheck command>'

Q: Can I make Docker restart an unhealthy container automatically?
A: Not by default. You can either use tools like autoheal or modify your health check to force the container to exit, letting Docker's --restart policy take over. For example:

HEALTHCHECK … CMD curl -f … || kill -s 15 1

If you're in Docker Swarm or Kubernetes, the scheduler handles unhealthy containers automatically.

Q: What’s the difference between a health check timing out and failing?
A:

  • A timeout means the command didn’t complete within the defined timeout window.
  • A failure means it finished but returned a non-zero exit code (usually 1). Docker marks the check as failed in either case.

Q: How often should I run health checks?
A: It depends. A 30‑second interval is a good default. If rapid failure detection is important, go lower—but be cautious about adding too much load. Adjust based on how quickly or slowly your application usually responds.

Q: My health check passes manually, but fails in Docker—why?
A: The context is different. Docker runs the command inside the container, not from your host. That means different network settings, file paths, and environment variables. Always test with:

docker exec container_name sh -c '<healthcheck command>'

Q: Should every container have a health check?
A: Focus on containers where serving requests or data matters—like web apps, databases, message queues, etc. Not every short-lived or job container needs a health check. But for infrastructure services, yes, it’s well worth it.

Q: How do I stop an unhealthy container automatically?
A: Docker won't shut it down on its own. You can either:

  • Add logic to the health check itself to kill PID 1 on failure
  • Run a small host-side script to restart all containers with health=unhealthyFor example:
docker ps -q -f health=unhealthy | xargs -r docker restart

This avoids giving containers full Docker socket access

Contents

Do More with Less

Unlock high cardinality monitoring for your teams.