Skip to content
Latchkey LogoLatchkey home

Docker OCI runtime exec failed

Docker OCI runtime exec failed comes from docker exec, not from starting a container, so to fix docker exec failures you look at the command you asked for and at the state of the container, never at the build. The runtime found the container and could not run the thing you named inside it.

Runner log: one exec succeeding with sh and the next failing on a missing bash
The recorded run: the same running container accepts an sh exec and refuses a bash exec. The container never changed, the command did.
Diagram separating exec failures from create failures and the checks for each
Exec errors and create errors read alike and mean different things. The word exec after OCI runtime is what tells you which one you have.

What this error means

A step that pokes at an already running container fails, while the container itself keeps running and the rest of the job looks healthy. The line names the command in quotes and ends in "executable file not found in $PATH", and on older daemons it ends with a further ": unknown". Our recorded run exited 127. The most common version of this in CI is a script that reaches for bash inside an Alpine image, which ships sh and ash and no bash at all, and the same run below shows an sh exec working seconds earlier in the same container.

Actions log, exec step, Docker 29.7.2
--- docker exec exec-demo sh -c 'echo alpine has sh'
alpine has sh
--- docker exec exec-demo bash -lc 'echo unreachable'
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH

Reproduced on a Latchkey runner

Run 2026-09-20·Runner latchkey-small·Exit code 127

Docker version 29.7.2, build a7dcaa6
Unable to find image 'alpine:3' locally
3: Pulling from library/alpine
e2de96513ba9: Pulling fs layer
e2de96513ba9: Download complete
6d0606d1815c: Download complete
797dd00a0fc7: Download complete
e2de96513ba9: Pull complete
Digest: sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6
Status: Downloaded newer image for alpine:3
--- container status: running
--- docker exec exec-demo sh -c 'echo alpine has sh'
alpine has sh
--- docker exec exec-demo bash -lc 'echo unreachable'
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)

The runner diagnosed the failure and did not retry it; this failure needs the fix below.

Exec failed and create failed are different failures

Both wordings start with "OCI runtime" and both end with a missing executable, which is why they get treated as one problem and fixed in the wrong place. The word after the runtime name is the whole diagnosis. "exec failed" means an already running container was asked to run something extra. "create failed" means the container itself could not be started, which is failed to create shim task and a different fix.

The Docker documentation adds the second half of the diagnosis: the command you exec "only runs while the container's primary process (PID 1) is running", and "the command must be an executable", because a chained or quoted command does not work without a shell around it.

What the line saysWhat to check first
OCI runtime exec failedThe binary you named, and whether the container is still up
OCI runtime create failedThe image CMD or ENTRYPOINT, because the container never started
executable file not found in $PATHWhether that tool is in the image at all, not whether it is on the host
no such file or directory on a path you gaveThe absolute path inside the container, and the architecture of the binary

Common causes

The binary is not in the image

The ordinary case, and the one the recorded run shows. bash on Alpine is the classic, but curl, ps, git and even sh go the same way on smaller bases. The message names the command in quotes, so the diagnosis is in the error; what it cannot tell you is that the tool was never there rather than removed.

The container is not running any more

Exec needs a live PID 1 to join. A service that exited on a bad config, a compose service in a restart loop, or a container from an earlier step that has already finished all produce a failure at exec time that is really a startup failure. In our experience this is the half of the error people spend the longest on, because the logs they read are the exec logs.

The binary is there and not on PATH

A tool installed into an unusual prefix resolves only by absolute path, and an exec that names it bare gets the same "not found" as a tool that does not exist. The PATH inside the container is the image PATH, which is not the PATH of the step that ran the exec.

The command was a shell line rather than an executable

A pipe, a redirect or a quoted chain needs a shell to interpret it. Docker documents this directly: the command must be an executable, and a chained or quoted command does not work. Wrap it in sh -c and the same line runs.

How to fix it

Exec a shell the image actually ships

  1. Use sh by default; reach for bash only in images you know carry it.
  2. If you need bash for real, install it in the image rather than hoping.
  3. Check once with command -v, which tells you before the pipeline does.
Terminal
docker exec app sh -c 'command -v bash || echo "no bash in this image"'
# in the Dockerfile, if you decide you need it
# RUN apk add --no-cache bash

Prove the container is running first

Inspect the status and dump the logs when it is not what you expect. This turns an exec error into the startup error that caused it, which is the thing you actually need to fix, and it costs one step.

Terminal
docker ps --filter name=app --format '{{.Names}} {{.Status}}'
docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' app

Name the binary by absolute path when PATH is in doubt

An absolute path removes the resolution question entirely. Find it once inside the container, then use it in the workflow, and keep the discovery command in the job so a future image change is visible rather than silent.

Terminal
docker exec app sh -c 'command -v mytool || ls -l /usr/local/bin'
docker exec app /usr/local/bin/mytool --version

Wrap shell syntax in a shell

Pipes, redirects, globs and && are shell features, so they need a shell process. Pass the whole line to sh -c as one argument, and quote it so the outer shell in your workflow leaves it alone.

Terminal
docker exec app sh -c 'pg_isready -h localhost && echo ok'

The image decides which shell you get

A slim base image is a deliberate choice to ship less, and bash is one of the things it ships less of. Alpine gives you sh, provided by BusyBox; distroless images may give you no shell at all; a scratch image has nothing but your binary. A script written against bash works on the developer laptop, where the image was never the environment, and fails the first time CI runs it against the real container.

Write the exec against the smallest shell you can rely on, or install the one you need in the image on purpose. Both are fine; guessing is not.

Terminal
# works on Alpine, Debian and most slim images
docker exec app sh -c 'echo "$PATH"'
# only if the image really has bash
docker exec app bash -lc 'echo "$PATH"'

Half of these are a container that already exited

The other reading of the same failure is that there is nothing left to exec into. A service container that crashed on startup, a compose service still in its restart loop, or a container started in an earlier step that finished while the workflow moved on all leave you execing into a container id that is no longer running a process.

Check the state before the exec rather than after the error, and print the logs when it is not up. Two lines turn a confusing runtime error into the startup failure it actually is.

.github/workflows/ci.yml
- name: The container has to be up before we exec
  run: |
    state=$(docker inspect -f '{{.State.Status}}' app)
    echo "app is $state"
    [ "$state" = running ] || { docker logs --tail 50 app; exit 1; }
    docker exec app sh -c 'echo ready'

What the runner does about it

No repair, and none is claimed. On the recorded run the wrapper posted the failure to the sidecar and the sidecar answered, and nothing was repaired: the image had no bash before the diagnosis and it had no bash after. Latchkey carries no pattern for this failure, which is correct, because the missing binary is inside an image the runner does not build.

What a runner can do is make the failure cheap to read. The run above prints the container state and a working exec next to the failing one, so the log answers the first two questions anybody asks. Put the same two lines in your own job and this error stops needing a page.

How to prevent it

  • Standardize on sh for anything you exec in CI, whatever the base image is.
  • Check container state before every exec in a job, and print logs when it is down.
  • Bake the tools your scripts need into the image, rather than into the assumption.
  • Pin the base image, so a shell that is present today is still present next month.

Frequently asked questions

Why does docker exec bash fail on Alpine?
Because Alpine has no bash. It ships BusyBox, which provides sh and ash, and bash is a separate package nobody installed. The runtime resolves the name you gave against the container PATH, finds nothing, and reports the executable as not found. aquasecurity/trivy-action#415 is the same line reported against a GitHub Action that expected bash.
What is the difference between OCI runtime exec failed and create failed?
Exec failed comes from docker exec against a container that is already running: the container is fine and the command is not. Create failed comes from starting the container at all, so the image CMD or ENTRYPOINT is what cannot be run. The fix is in the workflow for the first and in the image for the second.
Why does the exec fail when the tool is clearly installed?
Because it is installed somewhere the container PATH does not cover, or it was installed in a builder stage that the final image does not carry. Run command -v inside the container to see what the container can resolve, and use the absolute path if the tool lives outside the standard directories.
Can I exec into a container that has stopped?
No. Docker documents that the command runs only while the container primary process is running, so a stopped or restarting container has no process namespace to join. Start a new container from the same image with the command you wanted, or read the logs of the one that exited.

Related guides

References

Latchkey: managed runners at $0.0025/min at 2 vCPU, free minutes on every plan, 30-day trial. Start free → 30-day trial · No credit card