When it breaks — a diagnostic path
A repeatable order for debugging Docker problems, and what the most common error messages actually mean, rather than what they appear to say.
Debugging containers is easier than it feels, because there are only a few places a problem can be. What makes it hard is that Docker’s error messages often describe the symptom rather than the cause.
The order to check things
Work top to bottom. Each step narrows the surface.
1. Is it even running?
docker ps -a
Look at STATUS. Exited (0) means the process finished normally — usually your CMD was a
command that ends rather than a server. Exited (1) means it crashed. Restarting means it is
crash-looping.
2. What did it say on the way out?
docker logs api
docker logs --tail 50 --timestamps api
The answer is in here far more often than people expect. A crash-looping container has written the same stack trace fifty times.
3. Does the configuration match what you think?
docker inspect api --format '{{json .Config.Env}}' | python3 -m json.tool
docker inspect api --format '{{json .Mounts}}' | python3 -m json.tool
docker inspect api --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool
A surprising share of “it doesn’t work” is an environment variable that is empty because a .env
file was not where you thought.
4. Can you reproduce it by hand?
docker run --rm -it --entrypoint sh myapp:latest
This drops you into the image with its entrypoint bypassed. You can now check whether the files are where you expect, run the start command manually and read the real error.
Errors and what they actually mean
Cannot connect to the Docker daemon
The CLI cannot reach the daemon. Either it is not running, or you lack permission.
docker version # only a Client block confirms it
sudo systemctl status docker # Linux
On Linux, permission denied on /var/run/docker.sock means you are not in the docker group,
or you are but have not started a new session since being added.
port is already allocated
Something else is on that host port.
lsof -i :3000 # macOS / Linux
docker ps --format '{{.Names}}\t{{.Ports}}'
Often a container you forgot from yesterday. Either stop it or publish on a different host port —
-p 3001:3000.
no such file or directory when the file is clearly there
Three usual causes, in order of likelihood:
.dockerignoreexcluded it. The build context never received it. Check the file.- A wrong
WORKDIR. The path is relative to the working directory, not the repo root. - The binary needs glibc and you are on Alpine. This one is misleading: the message names
the binary that exists, because the missing file is actually its dynamic linker. Use a
-slimDebian base instead of Alpine, or build a static binary.
exec format error
The image was built for a different CPU architecture. Common when an Apple Silicon Mac builds an
arm64 image that then runs on amd64 servers.
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:1.0 --push .
Container exits immediately with code 0
The main process ended. A web server that starts as a daemon and returns control will do this —
in a container, the process must stay in the foreground. Look for a -d, --daemon or
--background flag in your CMD and remove it.
Build is slow, or “it works but takes forever”
docker build --progress=plain . 2>&1 | grep CACHED
If nothing is cached, a layer near the top is being invalidated — usually a COPY . . above the
dependency install. See mistake 2 in the previous chapter.
On macOS, also check whether you are bind-mounting a large node_modules. That is the other
common cause.
Out of disk space
docker system df
docker builder prune
The build cache is very often the largest consumer and the least obvious one. It is safe to delete; the next build is just slower.
Getting a shell in an image with no shell
Distroless and scratch images have no sh. Attach a debugging toolkit instead:
docker run -it --rm --pid container:api --network container:api \
--cap-add SYS_PTRACE nicolaka/netshoot
This puts a fully-equipped container in the same process and network namespaces as the broken
one, so ps, netstat, dig and curl all see what it sees.
Networking, specifically
# From inside the container: can it resolve the other service?
docker compose exec api getent hosts db
# Is the other service actually listening?
docker compose exec db netstat -tlnp
# Are they on the same network?
docker network inspect $(docker compose ls -q)_default
If DNS resolves but the connection is refused, the target service is bound to 127.0.0.1 inside
its own container instead of 0.0.0.0. That is the same mistake from chapter four, seen from the
other side.
You have finished the guide
You can now containerise an application, read a Dockerfile critically, run a multi-service stack locally, and debug the failures that actually happen. That is genuinely most of what day-to-day Docker use requires.
Where to go next depends on what you need. If you are deploying to more than one machine, that is orchestration — the Kubernetes guide starts from the same place this one did, with the problem rather than the YAML.