/bin/sh: 1: curl: not found in a container's health log, with
(unhealthy) in docker ps, means the HEALTHCHECK command calls a
program the image does not contain. The mcr.microsoft.com/dotnet/aspnet:10.0
image has no curl, so the check failed while the app itself answered Hello
World! on its published port. Install curl in
the image, or write a probe that does not need it.
In the test, one apt-get install line in the runtime stage turned the same
container from (unhealthy) into (healthy) with no change to the
HEALTHCHECK line itself.
The error
PS> docker ps --filter name=fix10-dc-h --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
NAMES IMAGE STATUS
fix10-dc-h2 fix10-dc-health:good Up 20 seconds (healthy)
fix10-dc-h1 fix10-dc-health:bad Up 21 seconds (unhealthy)
PS> docker inspect --format "{{json .State.Health}}" fix10-dc-h1
{"Status":"unhealthy","FailingStreak":4,"Log":[{"Start":"2026-11-13T05:21:31.549031859Z","End":"2026-11-13T05:21:31.588035043Z","ExitCode":1,"Output":"/bin/sh: 1: curl: not found\n"},...
The HEALTHCHECK in the broken image:
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD curl -f http://localhost:8080/ || exit 1
Why it happens
Docker stored the check as ["CMD-SHELL","curl -f http://localhost:8080/ || exit 1"]
(docker inspect --format "{{json .Config.Healthcheck}}"), so it runs through
/bin/sh inside the container. The shell finds no curl, and the
|| exit 1 turns that into exit code 1. The captures show the final state, not
the moment it changed: "Status":"unhealthy" with "FailingStreak":4
and the same output in every log entry, and the stored check with
"Retries":3.
The aspnet:10.0 image tested here is Ubuntu 24.04.5 LTS (its /etc/os-release)
with the .NET runtime and without curl. The health log is the place where the missing
program shows up. In a second run of the same broken image, published on
127.0.0.1:15937, the app logged normal startup lines and answered the request from
Windows, while docker ps said (unhealthy):
> docker logs fix10-dc-h3
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:8080
...
> curl.exe -sS http://127.0.0.1:15937/
Hello World!
...
fix10-dc-h3 fix10-dc-health:bad Up 26 seconds (unhealthy)
The fix
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
RUN dotnet new web -n FixLab -o . && dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:10.0
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app .
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD curl -f http://localhost:8080/ || exit 1
ENTRYPOINT ["dotnet", "FixLab.dll"]
PS> docker inspect --format "{{.State.Health.Status}} {{(index .State.Health.Log 0).ExitCode}} {{(index .State.Health.Log 0).Output}}" fix10-dc-h2
healthy 0 % Total % Received % Xferd Average Speed Time Time Time Current
...
Hello World!
The RUN line goes in the final stage, the one the container runs, not in the
SDK build stage. The image sets no USER (the container ran as
uid=0(root)), so apt-get works there; put the line above any
USER $APP_UID line of your own. The check
itself can keep localhost: it runs inside the container, next to Kestrel.
The trade-off is a larger image and one more package to keep patched. If you would rather not add curl, the alternative is a probe built into your own app, for example a small console command in the image that calls the health endpoint; that was not part of this test.
How it was reproduced
A two-stage Dockerfile ran dotnet new web and dotnet publish on
mcr.microsoft.com/dotnet/sdk:10.0 and copied the output into
mcr.microsoft.com/dotnet/aspnet:10.0 (runtime 10.0.13), with the HEALTHCHECK
above. The fixed image added the apt-get line and nothing else. Both containers
ran side by side on Docker 29.9.3 on Windows; the container and image names are the test's
own.
Frequently asked
- Does the ASP.NET Core Docker image include curl?
- No. In the test, a HEALTHCHECK calling curl on mcr.microsoft.com/dotnet/aspnet:10.0 failed with /bin/sh: 1: curl: not found until curl was installed with apt-get in the final stage.
- How do I see why a Docker container is unhealthy?
- Run docker inspect --format "{{json .State.Health}}" with the container name. The Log array holds the exit code and output of the last health checks. In the test docker logs showed only the app's startup lines and nothing about the failing check.
- Should the HEALTHCHECK use localhost or the published port?
- localhost and the container port. The check runs inside the container, so in the test curl -f http://localhost:8080/ answered Hello World! with no port published at all.
More decoded errors in the Fixes category. When Compose
waits on this check with service_healthy, the same failure surfaces as
dependency
failed to start: container is unhealthy.