Docker healthcheck marks container unhealthy because curl is missing from the slim base image

2026-11-14 · 3 min read

Docker healthcheck marks container unhealthy because curl is missing from the slim base image

Your container starts, serves traffic, and then flips to unhealthy after 30 seconds. Nothing in the app logs explains it. In the health log you find a one-liner: sh: 1: curl: not found, exit code 127. The app was fine the whole time — the probe was broken. Worse, if you run on Swarm the task gets killed and rescheduled on a loop, and if you use Compose depends_on: condition: service_healthy, every dependent service stays blocked.

The Docker healthcheck curl missing problem is almost always a base-image change: someone moved to debian:*-slim, alpine, or a distroless image for size and CVE count, and the HEALTHCHECK line still calls curl.

Why the probe fails in slim and distroless images

HEALTHCHECK runs inside the container's own filesystem namespace. It does not use your host's tooling. A shell-form healthcheck like HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1 is executed as /bin/sh -c "...", and sh resolves curl via PATH. If the binary isn't in the image, you get 127, which Docker records as a failed probe and, after --retries, marks the container unhealthy.

Full debian, ubuntu, and node images ship curl. Their slim variants usually don't; distroless images have no shell at all. Note that a plain docker run only *reports* unhealthy — it doesn't restart. Swarm replaces unhealthy tasks, Compose uses the status for dependency ordering [link needed].

Diagnose a Docker healthcheck curl missing failure

1. Read the health state, don't guess:

docker inspect --format '{{json .State.Health}}' <container> | jq

Look for "ExitCode": 127 and the Output field containing not found.

2. Confirm the binary is absent, using the same entrypoint context:

docker run --rm --entrypoint sh <image> -c 'command -v curl; echo $?'

Empty output plus a nonzero status confirms it.

3. List what *is* available: docker run --rm --entrypoint sh <image> -c 'command -v wget busybox nc python3'.

4. Reproduce by hand before editing anything: docker exec <container> curl -f http://localhost:8080/health. A "not found" here matches the probe failure and rules out an app problem.

Fix it: three options, ordered by preference

1. Use a tool the image already has. Alpine ships BusyBox wget; many runtimes ship a native HTTP client. For a Python image: HEALTHCHECK CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8080/health').status==200 else 1)". For BusyBox: HEALTHCHECK CMD wget -q --spider http://localhost:8080/health || exit 1. No image growth.

2. Install curl in a build stage or as a single layer. Only worth it if other tooling needs it:


RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

Pin the version and check the resulting size delta — the curl package pulls CA certificates and libcurl [link needed].

3. Ship nothing and probe from outside. Keep the container scratch-thin and let the orchestrator or a sidecar do the HTTP check. Kubernetes httpGet probes need no in-image binary; ECS and Compose support external checks too.

Whatever you pick, verify the transition: rebuild, start, and confirm docker inspect shows "Status": "healthy", then stop the app process and confirm the probe actually flips to unhealthy. A probe that never fails is not a probe.

When NOT to do this

Checklist