Mahdi BEN RHOUMAdocker exec fails with executable file not found in $PATH when the image lacks that binary. Install it, use a shell the image has, or use a sidecar.
docker exec starts a new process inside the container, and the container runtime looks the command up in the container's own PATH, not the host's. When the image does not ship that binary, the exec fails with executable file not found in $PATH. The container is not broken. Either install the tool in the container (or, durably, in its Dockerfile), call a program the image does have (sh instead of bash, curl instead of ping), or run the tool from a throwaway container that shares the target's network.
The report on Stack Overflow is the classic form of it: two nginx containers, and an attempt to ping one from the other.
docker container exec -it nginx1 ping nginx2
OCI runtime exec failed: exec failed: container_linux.go:344: starting container process caused "exec: \"ping\": executable file not found in $PATH": unknown
Current versions of the runtime word the middle part differently. This is the same failure for bash, exactly as it appears in a trivy-action issue:
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown
The container_linux.go:344 part is only a source file and line number inside the runtime binary that produced the message. It moves from one build to the next (in runc's current source, the "unable to start container process" error is returned from libcontainer/container_linux.go at a different line), so do not search for the number: the useful part is the quoted command name and executable file not found in $PATH.
Docker does not start container processes itself. The request travels through containerd to runc, the OCI runtime, which enters the container's namespaces and resolves the command there. In runc's source, the exec path does this with Go's exec.LookPath on the first argument, before anything is started (see setns_init_linux.go). LookPath searches the directories listed in PATH, and when nothing matches it returns Go's exec.ErrNotFound, whose text is exactly executable file not found in $PATH; Go's exec.Error prefixes it with exec: and the quoted command name. runc wraps that error (exec failed: unable to start container process: ...), containerd's runc shim adds the OCI runtime exec failed prefix (see process/exec.go), and Docker passes it on with the gRPC status : unknown at the end.
So the message is literal: inside that container's filesystem, no directory on PATH holds a file called ping.
Why would an nginx container have no ping? Because official images ship only what the service needs. The nginx image's Dockerfile starts from debian:trixie-slim and installs nginx and its modules, gettext-base and curl, then removes its build helpers. No ping utility is installed at any step. The same is true of most application images: a node or python image has the language runtime, not a network toolbox.
The docker exec reference adds a second trap that produces the same error: "The command must be an executable. A chained or a quoted command doesn't work." Passing "echo a && echo b" as one argument makes the runtime look for a single file with that whole string as its name, which does not exist either.
Pick the step that matches why you ran exec in the first place.
Open the shell the image ships. Debian and Ubuntu based images have bash; Alpine based images only have sh:
docker exec -it nginx1 sh
Then check for the tool and look at the PATH the runtime searched:
command -v ping || echo "ping is not installed"
echo "$PATH"
If command -v prints a full path, the binary exists and the problem is the way it was called (see the quoted-command trap above). If it prints nothing, the image does not contain it and one of the next steps applies.
This is what the accepted answer on the source question does. On the Debian based nginx image, as root (its Dockerfile sets no USER, so exec runs as root):
docker exec -it nginx1 sh -c 'apt-get update && apt-get install -y --no-install-recommends iputils-ping'
docker exec -it nginx1 ping -c 3 nginx2
The accepted answer installs inetutils-ping; iputils-ping is the other Debian package that provides ping, and either works. If the container runs as a non-root user, add -u 0 to the exec so apt-get can write to the system directories. On an Alpine based image you rarely need this step: its BusyBox already provides ping (see step 3).
Treat this as disposable. The package lives in the container's writable layer: it survives docker stop and docker start, but the next docker compose up that re-creates the container starts from the image again, without it.
Often the goal is not really ping; it is "can container A reach container B?". The nginx image ships curl, so test the thing that matters, an HTTP request to the other container, without installing anything:
docker exec nginx1 curl -sS -o /dev/null -w '%{http_code}\n' http://nginx2/
A 200 proves name resolution, routing and the target's listener in one go, which ping never did. To test name resolution alone, getent is part of Debian's libc-bin, which Debian based images carry:
docker exec nginx1 getent hosts nginx2
The second answer on the source question takes a variant of this route: switch to nginx:alpine. The Alpine nginx image is built, through its alpine-slim variant, on alpine, whose BusyBox configuration enables the ping, nslookup and wget applets, so those commands exist there out of the box.
When you must not touch the running container, as in production, start a throwaway container that joins its network namespace. The Docker networking documentation describes the --network container:<name|id> mode for exactly this: the new container shares the target's network stack.
docker run --rm -it --network container:nginx1 busybox ping -c 3 nginx2
busybox brings its own ping, sees the same interfaces and DNS configuration as nginx1, and disappears on exit. Nothing is installed in the service container.
If a script or health check really needs the binary, install it at build time so every container from the image has it:
FROM nginx:stable
RUN apt-get update \
&& apt-get install -y --no-install-recommends iputils-ping \
&& rm -rf /var/lib/apt/lists/*
Rebuild and re-create the container (docker compose up -d --build) for the change to take effect. Keep this for tools the service genuinely uses; a debugging toolbox is better served by step 4.
Run the original command again. With the tool present and both containers on a shared network, ping prints replies instead of the OCI error:
docker exec nginx1 ping -c 3 nginx2
For a check that also runs cleanly in CI, test the exit status of the lookup instead of reading output:
docker exec nginx1 sh -c 'command -v ping' && echo "ping available"
Once ping exists, a second failure often follows: ping: nginx2: Name or service not known. That is no longer an exec problem. It is name resolution, and the cause is almost always the network the containers are on. The bridge network documentation is explicit: "User-defined bridges provide automatic DNS resolution between containers", while "Containers on the default bridge network can only access each other by IP addresses, unless you use the --link option", which is legacy. Two containers started with a plain docker run land on the default bridge, so the fix is a user-defined network:
docker network create web
docker network connect web nginx1
docker network connect web nginx2
docker exec nginx1 getent hosts nginx2
Compose avoids this by default. According to the Compose networking documentation, each service joins the project's default network and is "discoverable by its service name", which is why docker compose exec app sh in a Compose based development environment can reach the database as db. The same rule is the usual answer when an app container cannot reach its database: use the service name and the container port, never localhost, as covered for Prisma failing to reach database:5432.
Three look-alikes are worth separating from this error:
exec: "bash": executable file not found on an Alpine image. The third answer on the source question simply switches to /bin/sh, which is the right move: Alpine's BusyBox build is configured with no bash, and sh is its ash shell.psql is present inside the official postgres image but not in your app image; running it where it is shipped is covered in the guide to fixing psql: command not found.exec format error instead of "not found". The binary exists but was built for another CPU architecture, a different failure with a different fix, described in the post on exec format error on AWS Fargate.The runtime reports exactly what it saw: a name it could not find on the container's PATH. Check what the image ships before reaching for a fix, prefer a tool that is already there or a throwaway sidecar for debugging, and put anything the service truly depends on into the Dockerfile, where it survives the next re-creation.
Originally published at https://www.iloveblogs.blog