executable file not found in $PATH means the container was created successfully and then the runtime could not find the program it was told to start. The image is fine. The start command is wrong, or the file it names is not where the runtime is looking.
Note that this is a different failure from exec format error, which means the file was found and the kernel could not run it. Found-but-unrunnable is an architecture problem; not-found is this page.
Look inside the image
You cannot docker run your way in when the entrypoint is what is broken, so override it:
docker run --rm -it --entrypoint sh myapp:1.0
Then check the two things that matter:
ls -l /app
echo "$PATH"
If the binary is not in /app, or /app is empty, the COPY in your Dockerfile did not put it where you think. That is the most common root cause and it is invisible until you look.
If the image has no shell either, see the distroless section below.
Check what the image is configured to run
docker image inspect myapp:1.0 \
--format 'ENTRYPOINT={{.Config.Entrypoint}} CMD={{.Config.Cmd}}'
ENTRYPOINT=[/app/server] CMD=[--port 8080]
The runtime runs ENTRYPOINT followed by CMD. Getting this combination wrong produces surprising results: with an ENTRYPOINT set, a command you pass to docker run replaces CMD and becomes an argument to the entrypoint, not a new command. That is why
docker run myapp:1.0 bash
on an image with ENTRYPOINT ["/app/server"] tries to run /app/server bash rather than giving you a shell.
Cause 1: The COPY destination is not what you assumed
WORKDIR /app
COPY build/server .
CMD ["./server"]
Looks right, and the trailing . is the trap. COPY src . copies into the working directory when the source is a file, so this produces /app/server and works. But:
COPY build/ /app
copies the contents of build/ into /app, whereas
COPY build /app
copies the directory itself, producing /app/build/server. One character of difference, and CMD ["/app/server"] then points at nothing.
Be explicit and verify:
COPY build/server /app/server
RUN test -x /app/server
That RUN test -x fails the build rather than shipping an image that cannot start, which is a much cheaper place to find out.
Cause 2: Exec form cannot use the shell
CMD ["./start.sh"] # exec form: no shell involved
CMD ./start.sh # shell form: runs via /bin/sh -c
Exec form passes the string straight to execve. There is no shell, which means no $VAR expansion, no &&, no globbing, no PATH search of a relative path. These all fail in exec form:
CMD ["$APP_HOME/server"] # $APP_HOME is literal
CMD ["./server && ./worker"] # && is not a thing here
CMD ["node server.js --port $PORT"] # one giant argv[0]
The last is worth dwelling on: CMD ["node server.js"] looks for a program literally named node server.js, space included. Exec form takes one array element per argument:
CMD ["node", "server.js"]
If you genuinely need shell features, be explicit about it:
CMD ["sh", "-c", "exec ./server --port ${PORT:-8080}"]
The exec matters. Without it the shell stays as PID 1 and does not forward SIGTERM, so your container ignores docker stop and gets killed after the timeout. That shows up later as slow deploys and dropped connections, and it is easy to miss.
Cause 3: The shell or tool is not in the image
Alpine has no bash. It ships BusyBox ash as /bin/sh.
FROM alpine:3.20
CMD ["bash", "-c", "echo hi"] # exec: "bash": executable file not found
Use sh, or install bash if you need it:
RUN apk add --no-cache bash
The same applies to curl, wget, ps and most of what you assume is present. BusyBox provides cut-down versions of some of these and not others, and the cut-down ones do not always accept the flags you expect.
Cause 4: Distroless and scratch images
gcr.io/distroless/* and scratch contain no shell at all. That is the point: less to exploit. It also means:
docker run --entrypoint shfails, because there is nosh.docker exec -it <container> shfails for the same reason.- Any
CMD ["sh", "-c", ...]fails. - Health checks that shell out fail.
Only exec form works, naming the binary directly:
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/server /server
ENTRYPOINT ["/server"]
To inspect one, use the :debug variant, which includes BusyBox:
docker run --rm -it --entrypoint sh gcr.io/distroless/static-debian12:debug
Or look at the filesystem from outside without running anything:
docker create --name tmp myapp:1.0
docker export tmp | tar -tv | head -30
docker rm tmp
docker export works on a created-but-never-started container, so it works even when the image cannot start at all. It is the most reliable way to answer "what is actually in this image".
The two errors people confuse with this one
| Message | Meaning |
|---|---|
executable file not found in $PATH | The file is not there, or not on PATH |
permission denied | The file is there without the executable bit |
exec format error | The file is there and built for another architecture |
For the middle one:
COPY --chmod=0755 entrypoint.sh /entrypoint.sh
--chmod on COPY avoids a separate RUN chmod layer, and it survives checkouts on Windows where the source file may arrive without the bit set.
A checklist
docker run --rm -it --entrypoint sh <image>andlsthe path.docker image inspect --format 'ENTRYPOINT={{.Config.Entrypoint}} CMD={{.Config.Cmd}}'.- Not where you expected → your
COPYdestination is wrong. AddRUN test -x. - Exec form with spaces in one element → split into separate array elements.
- Need
$VARor&&→CMD ["sh", "-c", "exec ..."], with theexec. - Alpine → there is no
bash. Useshor install it. - Distroless or scratch → no shell exists. Use
:debug, ordocker exportto look inside. permission deniedinstead →COPY --chmod=0755.
Frequently Asked Questions
What does "executable file not found in $PATH" mean in Docker?
The container was created and the runtime then could not find the program it was told to execute. Either the file is not at the path given, or the name was given without a path and is not on PATH. It is specifically not an image-pull or permissions problem, and it is distinct from exec format error, which means the file was found and the kernel could not run it. Inspect the image with docker run --entrypoint sh and list the directory you expect the binary to be in.
Why does CMD ["node server.js"] fail?
Because exec form takes one array element per argument, so that asks for a program literally named node server.js, space included. Write CMD ["node", "server.js"]. The same mistake produces confusing results whenever people write a whole command line as a single string in exec form. Shell form, CMD node server.js, does work because it runs through /bin/sh -c, but then the shell becomes PID 1 and will not forward SIGTERM to your process unless you use exec.
Why can I not use bash in an Alpine image?
Alpine does not ship bash. It provides BusyBox ash as /bin/sh, which covers POSIX shell but not bash extensions such as arrays or [[ ]]. Any CMD ["bash", ...], RUN bash ... or docker exec ... bash fails with exactly this error. Either write POSIX-compatible shell and use sh, or add RUN apk add --no-cache bash. The same applies to tools you may assume are present, such as curl, since Alpine's BusyBox provides only a subset and sometimes with different flags.
How do I debug an image with no shell, like distroless?
Two options. Use the :debug tag of the distroless image, which includes BusyBox, so --entrypoint sh works. Or inspect the filesystem from outside without running anything: docker create --name tmp <image> then docker export tmp | tar -tv. The second works even when the image cannot start at all, because docker create never executes the entrypoint, which makes it the most dependable way to answer what is actually inside a broken image.
What is the difference between "executable file not found" and "permission denied"?
Not found means the path does not resolve to a file at all, usually a wrong COPY destination or a name that is not on PATH. Permission denied means the file exists but does not have the executable bit set, which happens frequently with scripts checked out on Windows or copied from an archive that did not preserve the mode. Fix the second with COPY --chmod=0755 script.sh /script.sh, which avoids an extra RUN chmod layer and does not depend on the mode of the file on the build machine.