How to Debug Docker Containers That Misbehave Step by Step
Debug a misbehaving Docker container: read logs and exit codes, check OOM kills, open a shell, debug images without a shell and compare configuration.

A container that exits immediately, restarts in a loop or runs but does not respond looks mysterious because you cannot see inside it. In fact Docker keeps nearly everything you need: the output, the exit code, the configuration it was started with and a way in. This post goes through those in order, with the commands and what their output means. It builds on exec and attach and opening a shell inside a container. Examples use Docker Engine 27.
Step 1: what happened
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
docker logs --tail 50 web
docker inspect web --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}} err={{.State.Error}}'NAMES STATUS IMAGE
web Restarting (137) 8s ago registry.example.org/web:1.4.0
exit=137 oom=true restarts=14 err=
Start with the status and the log tail. In a restart loop, docker logs shows the output of the most recent run; add --since 10m to see earlier attempts. The exit code decodes as follows: 0 is a clean exit; 1 is an application error; 126 and 127 mean the command could not be run or was not found (a wrong CMD or entrypoint); 137 is 128 + 9, killed with SIGKILL; 139 is a segmentation fault; 143 is SIGTERM. With oom=true, as above, the kernel killed the process for exceeding the memory limit: raise --memory or fix the leak.
Step 2: what was it started with
docker inspect web --format '{{json .Config.Env}} {{json .Config.Cmd}} {{json .Config.Entrypoint}}' | jq .
docker inspect web --format '{{json .HostConfig.Binds}} {{json .HostConfig.PortBindings}}'
docker inspect web --format '{{json .State.Health}}' | jq .Compare with what you meant: environment variables that are empty because the variable was unset on the host, an entrypoint overridden by Compose, a bind mount to a path that does not exist or has the wrong owner, a published port that differs from the one the app listens on. The health check output shows the last few probe results and their messages, which often name the failing endpoint.
Step 3: get inside
docker exec -it web sh
docker run --rm -it --entrypoint sh registry.example.org/web:1.4.0
docker run --rm -it --entrypoint sh -v "$PWD/config:/app/config:ro" registry.example.org/web:1.4.0exec works only while the container runs. For one that dies at once, start a fresh container from the same image with the entrypoint replaced by a shell, then run the original command by hand and watch it fail with all the context. Mount the same volumes and pass the same variables to reproduce the conditions. Inside, check id, file permissions, DNS (getent hosts db) and whether the process can reach its dependencies.
Step 4: images with no shell
docker run --rm -it --pid container:web --network container:web nicolaka/netshoot
docker debug web
docker cp web:/app/config.yaml ./config.yaml
docker diff webMinimal and distroless images have no shell or tools. A debug container that shares the target's process and network namespaces gives you ps, ss, curl and strace against the live process. docker debug (Docker Desktop and Docker subscription feature) does it with one command. docker cp copies files out for inspection, and docker diff lists files the container changed, which finds unexpected writes.
Step 5: live behavior
docker stats --no-stream web
docker top web
docker events --since 10m --filter container=webstats shows memory and CPU against the limits; top lists the processes in the container, and events shows the lifecycle: die, oom, restart, health_status changes, with timestamps that you can line up with host logs.
Common causes, quickly
- Exits at once with code 0: the main process finished; containers live as long as PID 1.
- Permission denied on a mount: user ID mismatch; see bind mount permissions.
- Cannot resolve a service name: not on the same network; see container DNS.
- Does not stop for ten seconds: PID 1 ignoring SIGTERM; see signals and PID 1.
Where this does not apply
Orchestrators wrap the same containers with their own layers: for Kubernetes use debugging pods from the terminal. A container that works alone and fails in Compose usually differs in networks, dependencies or env files; run docker compose config to see the merged result.
Starting from what the daemon recorded
A container that stops, restarts or misbehaves leaves a record, and reading it first is quicker than guessing. docker ps -a lists stopped containers with their exit status. docker inspect --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}' name prints the exit code, whether the kernel killed it for memory and any error from starting it. The exit code is a strong hint: 0 means the process finished normally, 1 means the application reported an error, 126 and 127 mean the command could not be executed or was not found, 137 means it was killed by SIGKILL, which is the memory killer or a stop that timed out, 139 means a segmentation fault and 143 means it ended on SIGTERM. A restarting container with code 137 and the out-of-memory flag set needs a higher limit or a memory fix, and one with 127 has a typo in the command or a missing binary in the image. Read the last logs with docker logs --tail 100 name for the application's own explanation.
Docker events, with docker events --since 10m, show the sequence of starts, dies and kills, which helps when a restart policy hides the first failure behind later ones.
Checking how it was started
Many container problems are configuration and not code. The same image behaves differently depending on its command, environment, mounts, user, limits and network. docker inspect shows all of it: the entrypoint and command, the environment variables, the bind mounts and volumes, the networks and addresses, the restart policy and the resource limits. Compare it with what you intended, and with a working instance if you have one. Look for an empty variable that should hold a value, a bind mount that points at a path that does not exist on this host, which Docker silently creates as an empty directory owned by root, and a command overridden by Compose. The environment can also hide a secret, so be careful where you paste the output. For Compose projects, docker compose config shows the final merged file, which is often the quickest way to see that an override or a variable substitution did something you did not expect.
Getting inside and what to do when there is no shell
docker exec -it name sh opens a shell in a running container, and it is the first thing to try. For a container that exits too fast to enter, start a new one from the same image with a different entrypoint, docker run --rm -it --entrypoint sh image, and run the real command by hand to watch it fail. Minimal and distroless images have no shell and no tools, so these fail. Two approaches work. Attach a debugging container that shares the target's namespaces, such as docker run --rm -it --pid container:name --network container:name image-with-tools, which gives you a shell and network tools looking at the same processes and network. Or copy files out with docker cp and examine them on the host. For Kubernetes, kubectl debug does the same with an ephemeral container. Use docker top and docker stats for a quick look at processes and resource use without entering.
Check the common causes quickly: a process that ignores signals as PID 1 and takes ten seconds to stop, a dependency that was not ready at start, a permission error on a mounted path, and a full disk on the host.
See docker container inspect and the run exit status reference.


