This error occurs when a Docker container's HEALTHCHECK command references an executable (like curl or wget) that doesn't exist in the container image. The fix involves using an alternative tool that's already installed, installing the required tool, or building a custom healthcheck binary for minimal images.
The "OCI runtime exec failed: executable file not found in $PATH" error occurs when Docker tries to run a health check command but cannot find the specified executable inside the container. This is extremely common with minimal container images like Alpine, distroless, or scratch-based images that don't include common utilities like curl or wget. Docker's health check mechanism runs commands inside your container to verify the service is working correctly. When the health check specifies a command like `curl -f http://localhost:8080/health`, Docker attempts to execute curl from within the container's filesystem. If curl isn't installed, the OCI runtime (runc) fails with this error. This error is particularly prevalent because many developers copy health check configurations from tutorials or documentation without realizing their base image doesn't include the required tools. Alpine images, for example, don't include curl by default - only wget is available. Distroless and scratch images contain no shell utilities at all.
First, check the health check configuration and error details:
# View container health status and recent errors
docker inspect --format='{{json .State.Health}}' <container_name> | jq
# Check the Dockerfile HEALTHCHECK instruction
grep -i healthcheck Dockerfile
# Check docker-compose.yml health check configuration
grep -A 5 healthcheck docker-compose.ymlThe error message will tell you exactly which executable is missing. Common ones include:
- curl - Not installed by default in Alpine or minimal images
- wget - Available in Alpine but not in distroless/scratch
- nc (netcat) - Available in Alpine but not in all images
Alpine Linux includes wget by default, so you can use it without installing anything:
In Dockerfile:
# Instead of curl
HEALTHCHECK CMD wget -qO- http://localhost:8080/health || exit 1
# Spider mode (doesn't download content, just checks URL exists)
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1In docker-compose.yml:
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40swget options explained:
- -q: Quiet mode (no progress output)
- -O-: Output to stdout (useful for piping)
- --no-verbose: Less output without being completely quiet
- --tries=1: Don't retry on failure
- --spider: Don't download, just check if URL exists
If you only need to verify a port is open (not a full HTTP check), netcat is available in Alpine:
In Dockerfile:
HEALTHCHECK CMD nc -z localhost 8080 || exit 1In docker-compose.yml:
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "8080"]
interval: 10s
timeout: 5s
retries: 3This is simpler and faster than HTTP checks when you just need to verify the service is listening on the expected port. Note that this doesn't verify the application is responding correctly - only that the port is open.
If you need curl specifically (for features wget doesn't have), install it in your Dockerfile:
For Alpine-based images:
FROM node:20-alpine
# Install curl
RUN apk add --no-cache curl
# Your application setup...
COPY . .
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1For Debian/Ubuntu-based images:
FROM node:20
# Install curl (often already included, but just in case)
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1Note: Adding curl increases image size by ~2-5 MB depending on the base image. Consider using wget if it's already available.
Instead of installing external tools, use the runtime your application already has:
Node.js example:
HEALTHCHECK CMD node -e "require('http').get('http://localhost:8080/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"Python example:
HEALTHCHECK CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" || exit 1PHP example:
HEALTHCHECK CMD php -r "file_get_contents('http://localhost:8080/health');" || exit 1This approach has zero additional dependencies since your runtime is already in the image.
For minimal images without any shell or utilities, compile a small static binary:
Go healthcheck binary (healthcheck.go):
package main
import (
"net/http"
"os"
"time"
)
func main() {
client := &http.Client{Timeout: 5 * time.Second}
url := "http://localhost:8080/health"
if len(os.Args) > 1 {
url = os.Args[1]
}
resp, err := client.Get(url)
if err != nil || resp.StatusCode != 200 {
os.Exit(1)
}
os.Exit(0)
}Build and use in Dockerfile:
# Build stage
FROM golang:1.21 AS healthcheck-builder
WORKDIR /build
COPY healthcheck.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-s -w' -o healthcheck healthcheck.go
# Final stage (distroless)
FROM gcr.io/distroless/static:nonroot
COPY --from=healthcheck-builder /build/healthcheck /healthcheck
COPY --from=builder /app/myapp /app/myapp
HEALTHCHECK CMD ["/healthcheck"]Alternatively, use a pre-built tool like microcheck:
COPY --from=ghcr.io/tarampampam/microcheck:1 /bin/httpcheck /healthcheck
HEALTHCHECK CMD ["/healthcheck", "http://localhost:8080/health"]After updating your health check, rebuild and test:
# Rebuild the image
docker build -t myapp:latest .
# Run with health check enabled
docker run -d --name test-healthcheck myapp:latest
# Wait for health check to run and check status
sleep 45
docker ps --format "table {{.Names}}\t{{.Status}}"
# View detailed health check results
docker inspect --format='{{json .State.Health}}' test-healthcheck | jq
# If unhealthy, check the logs for errors
docker inspect test-healthcheck | jq '.State.Health.Log[-3:]'Expected output shows "healthy" status:
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [...]
}Why minimal images don't have curl/wget: Distroless and scratch images are designed to contain only your application and its runtime dependencies. They intentionally exclude package managers, shells, and utility programs to reduce attack surface and image size. This is a security best practice, but it means traditional health checks won't work.
Comparing health check options for minimal images:
| Approach | Image Size Impact | Complexity | Works in Distroless |
|----------|------------------|------------|---------------------|
| wget (Alpine) | 0 (built-in) | Low | No |
| curl (install) | +2-5 MB | Low | No |
| Runtime healthcheck | 0 | Medium | Yes (if runtime exists) |
| Custom Go binary | +1-2 MB | High | Yes |
| microcheck | +500 KB | Low | Yes |
Kubernetes alternative: If you're deploying to Kubernetes, consider using HTTP liveness/readiness probes instead of Docker HEALTHCHECK. Kubernetes probes run from outside the container, so they don't need any tools inside:
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5ECS and Fargate: AWS ECS health checks can also be configured at the load balancer level, avoiding the need for in-container tools:
{
"healthCheck": {
"command": ["CMD-SHELL", "exit 0"],
"interval": 30,
"timeout": 5,
"retries": 3
}
}Then configure the ALB/NLB target group health check to hit your health endpoint.
Security consideration: While it's tempting to install curl "just for health checks," each additional binary increases your attack surface. If your container is compromised, tools like curl and wget can be used to exfiltrate data or download malware. Consider whether a port check with netcat or a custom binary is sufficient.
image operating system "linux" cannot be used on this platform
How to fix 'image operating system linux cannot be used on this platform' in Docker
manifest unknown: manifest unknown
How to fix 'manifest unknown' in Docker
cannot open '/etc/passwd': Permission denied
How to fix 'cannot open: Permission denied' in Docker
Error response from daemon: failed to create the ipvlan port
How to fix 'failed to create the ipvlan port' in Docker
toomanyrequests: Rate exceeded for anonymous users
How to fix 'Rate exceeded for anonymous users' in Docker Hub