You run the container, check docker ps, and it's already gone. Nine times out of ten it's PID 1 doing its job. Here's how to read it.
You run docker run my-image, the prompt comes back, and everything looks fine. Then you check:
docker ps -a
CONTAINER ID IMAGE STATUS NAMES
a1b2c3d4e5f6 my-image Exited (0) 2 seconds ago sleepy_hopper
Gone. It ran, it exited, and nothing you did got a chance to happen. This is the most common "my container won't stay up" question, and the confusing part is that Exited (0) means success. Nothing crashed. The container did exactly what you told it to. The problem is that what you told it to do took two seconds.
A container lives exactly as long as its PID 1. That's it. When you start a container, Docker runs your CMD (or ENTRYPOINT) as process ID 1 inside the namespace. When that process exits, the container stops. There is no background. There is no "keep it warm." PID 1 returns, the container is dead, exit code and all.
So the question is never really "why did my container stop?" It's "why did PID 1 finish?" Frame it that way and every case below is obvious.
This is Exited (0) land. Your CMD ran a command that completes and returns.
CMD ["python", "setup_db.py"]
That script sets up a schema and exits. Container's done its job, so it stops. Same thing if your CMD is echo hello or ls or a one-shot migration. People hit this constantly with base images: docker run ubuntu exits instantly because the default command is bash, and with no TTY attached bash has no input, reaches EOF, and returns.
The fix depends on intent. If it was supposed to be a one-shot job, congratulations, it worked. If you wanted a service, you need a process that stays in the foreground and doesn't return until you kill it:
CMD ["python", "app.py"] # app.py runs a server loop, blocks forever
This one is sneaky because the process is a "server," but it forks a background copy and the foreground one exits. PID 1 returned, so the container dies even though the daemon technically started.
nginx is the classic. By default it backgrounds itself. You have to tell it not to:
CMD ["nginx", "-g", "daemon off;"]
Apache wants httpd -D FOREGROUND. Anything you'd normally run with a -d flag or a start subcommand is suspect. The rule: never let a process inside a container background itself. Docker is your process manager now, so run everything in the foreground and let it own the lifecycle. If you're reaching for systemd inside a container to work around this, stop and run the process in the foreground instead.
Exited (1), Exited (127), Exited (137). Now it's not doing its job, it's failing. And the answer is almost never in your head, it's in the logs:
docker logs sleepy_hopper
Read what it printed before it died. A few codes carry meaning on their own:
127 is "command not found." Usually a typo in CMD, or a binary that isn't in the image, or a wrong PATH.126 is "found it, couldn't execute it." Permission bit missing on your entrypoint script, or it's not actually executable.1 and 2 are generic app errors. Read the logs.137 is 128 + 9, a SIGKILL. Often the OOM killer. Check docker inspect <id> for "OOMKilled": true.I've watched people rebuild an image five times chasing a 127 that a single docker logs would have named in one line. Read the logs first.
Sometimes the image is fine and the invocation is wrong. If you want an interactive shell, you have to actually attach one:
docker run -it ubuntu bash # -i keeps stdin open, -t gives a tty
Without -it, bash sees no input and exits immediately. That Exited (0) from docker run ubuntu earlier was exactly this.
When you're not sure what the image even tries to run, override the entrypoint and poke around from the inside:
docker run -it --entrypoint sh my-image
# now you're in the container's filesystem, run the CMD by hand and watch it fail
This is the highest-value move on the list. You run the real command by hand and see the actual error, instead of guessing at a container that's already gone.
This one doesn't kill the container on startup, it bites you later, but it belongs here because it's the same PID 1 story. Compare:
CMD python app.py # shell form: runs `/bin/sh -c "python app.py"`
CMD ["python", "app.py"] # exec form: python is PID 1 directly
In shell form, PID 1 is sh, and your app is a child. When Docker sends SIGTERM on docker stop, it goes to sh, which doesn't forward it. Your app never hears the shutdown, gets a hard SIGKILL ten seconds later, and you get ungraceful exits and Exited (137) on shutdown. Use exec form (the JSON array) so your process is PID 1 and receives signals directly. If your app genuinely needs to reap child processes, add a tiny init with docker run --init rather than reaching for a shell wrapper.
When a container won't stay up, this is the order:
docker ps -a # exit code: 0 = finished, nonzero = crashed
docker logs <container> # what it printed before dying
docker run --rm my-image # run in the foreground, no -d, watch it live
docker run -it --entrypoint sh my-image # go inside, run CMD by hand
Ninety percent get solved at step two. The exit code tells you which story you're in, the logs tell you the line, and the foreground run lets you watch it happen. For the wider map of Docker failures, our Docker troubleshooting guide covers the rest of the usual suspects.
Before touching the Dockerfile, run docker ps -a and docker logs. Read the exit code as a sentence: 0 means your process finished, nonzero means it broke, 137 means something killed it. If it's a service that dies on 0, you have a backgrounding or one-shot-command problem, so pin the process to the foreground. Use exec-form CMD so signals land where they should. And keep docker run -it --entrypoint sh in your muscle memory, because a container you can walk around inside is worth ten you can only stare at in ps -a.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
We've shipped production pipelines on both. Here's where GitHub Actions wins, where GitLab CI wins, and how to pick without regretting it in six months.
How pods can talk to AWS, GCP, and Azure with no static keys — using audience-bound projected ServiceAccount tokens and the cluster OIDC issuer.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Evergreen posts worth revisiting.