DevOps

Fix OCI runtime exec failed: executable file not found

The container is fine; the command you asked for is not in it. Find out what the image really ships, then install the tool or bring it in from outside.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

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. If the missing binary is a native .node addon rather than a shell tool, and the error says Exec format error instead of executable file not found, the library was built for the wrong CPU architecture instead — a different fix.

The error#

The report on Stack Overflow is the classic form of it: two nginx containers, and an attempt to ping one from the other.

bash
docker container exec -it nginx1 ping nginx2
text
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:

text
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.

Why it happens#

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. When the same lookup fails as docker run creates the container, the message reads OCI runtime create failed instead, and one common cause is a docker run option written after the image name: Docker 'OCI runtime create failed': a flag after the image.

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.

Fix#

Pick the step that matches why you ran exec in the first place.

1. Check what the image actually has#

Open the shell the image ships. Debian and Ubuntu based images have bash; Alpine based images only have sh:

bash
docker exec -it nginx1 sh

Then check for the tool and look at the PATH the runtime searched:

bash
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.

2. Install the tool in the running container (for a quick check)#

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):

bash
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.

3. Use a tool the image already ships#

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:

bash
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:

bash
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.

4. Bring the tool in from a sidecar container#

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.

bash
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.

5. Make it permanent in the Dockerfile#

If a script or health check really needs the binary, install it at build time so every container from the image has it:

dockerfile
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.

Verify the fix#

Run the original command again. With the tool present and both containers on a shared network, ping prints replies instead of the OCI error:

bash
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:

bash
docker exec nginx1 sh -c 'command -v ping' && echo "ping available"

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

When the error changes after installing the tool#

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:

bash
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.
  • A client tool that belongs to another image. 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.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote