Debugging a Container That Will Not Start
A method for the five failures you will actually hit: exited immediately, port already allocated, image not found, permission denied, and the container that runs but answers nothing.
A method for the five failures you will actually hit: exited immediately, port already allocated, image not found, permission denied, and the container that runs but answers nothing.
Sooner or later a container refuses to run, and the output is one line that does not obviously say why. This is the lesson you will come back to more than any other, because it is less about Docker than about having a method — a fixed order of questions that narrows any container failure down in a couple of minutes.
By the end you will have that method, plus the five failures you will genuinely hit and what each one looks like before you know its name.
The instinct when something fails is to change something. Resist it for two minutes and ask these instead, in this order. Each one is one command, and the answer usually means you can stop.
docker ps -a — does it exist, and what happened?
A failed container looks like it vanished, because docker ps
shows only running ones. The STATUS column is your first
real clue.
docker logs — what did it print?
A stack trace, a missing environment variable, a connection refused. Read all of it, not the last line.
docker inspect — what was it told to do?
The command it ran, its environment, its mounts, its ports,
and an Error field. Use it when the logs are empty.
A shell — what does it look like inside?
docker exec -it <name> sh while it runs, or
docker run -it --rm --entrypoint sh myapp when it exits too
fast to catch.
1. Does it exist, and what does it say happened?
docker ps -adocker ps shows only running containers, so a container that
failed appears to have vanished. It has not — -a shows everything,
and the STATUS column is your first real clue:
STATUS
Exited (0) 3 seconds ago # finished normally, had nothing to do
Exited (1) 3 seconds ago # the program errored
Exited (127) 3 seconds ago # command not found
Exited (137) 2 minutes ago # killed — usually out of memory
Restarting (1) 5 seconds ago # crash loop
Created # never started at all2. What did it print?
docker logs <name-or-id>Most of the time the answer is here: a stack trace, a missing environment variable, a connection refused. Read all of it, not the last line.
3. What was it actually configured to do?
docker inspect <name-or-id>The whole truth about the container: the command it ran, its
environment, its mounts, its ports, and an Error field. Narrow it
when you know what you want:
docker inspect -f '{{.State.ExitCode}} {{.State.Error}}' web
docker inspect -f '{{json .Config.Cmd}}' web
docker inspect -f '{{json .Mounts}}' web4. What does it look like from inside?
docker exec -it <name> shOnly possible while the container is running. For one that exits immediately, you replace its command with a shell and look around the image instead:
docker run -it --rm --entrypoint sh myappThat last trick is the one worth memorising. It gives you the image's filesystem with none of its start-up behaviour, which is how you check whether the file you expected to copy is actually there.
A container runs one command and lives exactly as long as that command does. When the command finishes, the container exits — and exit code 0 means it finished successfully.
docker run ubuntu
docker ps -a # Exited (0)Nothing is wrong. ubuntu's default command is bash, and bash
with no terminal attached and nothing to read reaches end of input
immediately and exits happily.
Two fixes, depending on what you meant. For an interactive session, give it a terminal:
docker run -it ubuntu bash-i keeps standard input open, -t allocates a pseudo-terminal.
Together they are what makes a shell behave like a shell.
For a service, make sure the command runs in the foreground. This is the mistake that catches people packaging existing software:
Bad — nginx starts, detaches itself, and the container has nothing left to do:
CMD ["nginx"]Good — nginx stays in the foreground, so the container lives as long as the server:
CMD ["nginx", "-g", "daemon off;"]A container is not a machine that stays up; it is a wrapper around
one process. Any program that daemonises — nginx, Apache, many
init-script-era services — exits its foreground process on purpose,
and the container follows it down within milliseconds, with exit
code 0 and an empty log. Almost every service has a flag for this:
daemon off, --foreground, -D, or similar.
A non-zero exit code means your program decided to stop. docker logs almost always has the answer, and the most common shapes are:
KeyError: 'DATABASE_URL'A missing environment variable. Check what the container actually
received — docker exec api env | sort, or docker inspect if it
already exited. With Compose, docker compose config shows what
Compose resolved.
psycopg.OperationalError: could not connect to server: dbA hostname that does not resolve or a service that is not ready. Two
different problems with the same message. Confirm the two containers
share a network with docker network inspect, and check the name is
the service name. If both are correct, it is the start-up race
from the Compose lesson — the database is not accepting connections
yet, and the fix is a health check.
exec /app/entrypoint.sh: no such file or directoryMisleading, and worth recognising: the file often does exist. This
message also appears when the file has Windows line endings, so the
kernel reads the interpreter on the first line as /bin/sh\r and
cannot find it. It also appears when a shell script is not
executable, and when an image built for arm64 is run on amd64.
Exited (127)Command not found. The binary is not in the image or not on PATH.
Check with docker run -it --rm --entrypoint sh myapp and then
which python or ls /app.
137 is 128 + 9, meaning the process received SIGKILL. Something
killed it rather than it choosing to stop, and there are two
plausible somethings.
One command tells you which:
docker inspect -f '{{.State.OOMKilled}}' api # true or falseIt hit its memory limit.
The kernel's out-of-memory killer ended it. Either the limit is too low or the program uses more than you thought.
docker stats api shows live memory use; -m 512m is the
limit it was measured against.
It was asked to stop and did not.
docker stop sends SIGTERM, waits ten seconds, then sends
SIGKILL.
A program that ignores SIGTERM dies this way — and so does
one that never receives it, because a shell is sitting between
Docker and your process.
Error response from daemon: driver failed programming external
connectivity: Bind for 0.0.0.0:8080 failed: port is already
allocatedSomething already has that host port. Usually a container you forgot about, occasionally a program running natively.
docker ps -a --filter publish=8080 # which container holds it
lsof -i :8080 # macOS/Linux: which processThen either stop the holder, or publish on a different host port —
-p 8081:80. Remember that only the host side of the mapping has
to be unique; a hundred containers can all listen on 80 internally.
The related error is quieter:
docker: Error response from daemon: Conflict. The container name
"/api" is already in use by container "c9a1f4e2b8d7".docker run always creates a new container, and names are unique.
You meant docker start api, or you want docker rm api first.
Two errors that happen before the container exists at all.
Error response from daemon: manifest for myapp:v2 not foundThe image or tag does not exist. Check the tag with
docker images myapp — a typo, or a build that tagged something
else. For a private registry, this also appears when you are not
logged in, because the registry declines to admit the image exists.
Error response from daemon: pull access denied for myapp,
repository does not exist or may require 'docker login'Same cause, stated more helpfully. docker login first.
PermissionError: [Errno 13] Permission denied: '/data/app.db'A permissions problem on a mount. Almost always a container running as a non-root user writing to a bind-mounted host directory whose owner does not match. Find out who the container is, then who owns the directory:
docker exec api id
ls -ln ./dataThe fix is to align them — --user "$(id -u):$(id -g)" on the run,
or chown on the host directory — and for data that does not need
to be readable from the host, a named volume avoids the problem
entirely because Docker manages its ownership.
If every command fails the same way, the problem is one level up:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock.
Is the docker daemon running?The CLI is fine and the engine is not. Start Docker Desktop, or
sudo systemctl start docker on Linux. On Linux this also appears
when your user is not in the docker group.
no space left on deviceDocker has filled its disk with images, stopped containers and build cache. Find out what is using it before deleting anything:
docker system df
docker system prune # safe sweep
docker builder prune # build cache, often the biggest# The method, in order
docker ps -a # 1. does it exist, what is its STATUS
docker logs --tail 50 <name> # 2. what did it print
docker inspect <name> # 3. what was it configured to do
docker exec -it <name> sh # 4. what does it look like inside
# When it exits too fast to exec into
docker run -it --rm --entrypoint sh myapp # image, without its CMD
docker logs --timestamps <name> # order of events
# Targeted inspection
docker inspect -f '{{.State.ExitCode}}' api
docker inspect -f '{{.State.Error}}' api
docker inspect -f '{{.State.OOMKilled}}' api
docker inspect -f '{{json .Config.Cmd}}' api
docker inspect -f '{{json .Mounts}}' api
docker exec api env | sort # what config it got
docker stats api # live CPU and memory
# Reading exit codes
# 0 finished normally — often "nothing to do", or a daemonising
# process; run the service in the foreground
# 1 the program errored — read the logs
# 126 found but not executable — chmod +x, or CRLF line endings
# 127 command not found — not in the image, or not on PATH
# 137 SIGKILL — OOMKilled=true, or ignored SIGTERM for 10s
# 139 segfault — often a platform mismatch (arm64 vs amd64)
# Common blockers
docker ps -a --filter publish=8080 # who holds the port
docker network inspect appnet # who is on the network
docker system df # what is eating the disk
docker compose config # what Compose resolvedWork through the method once on a failure you cause deliberately: run a container with a required environment variable missing, and follow the four questions until the logs name it. Doing it while nothing is at stake is what makes it available when something is.
The next lesson is about the image rather than the container — multi-stage builds, base image choice and dropping root, which between them turn a large image with a compiler in it into something you would be comfortable publishing. Which is the lesson after that.