Start here: what did it say and how did it end?
# Read the logs first, even after the process exits.
docker logs <container>
# Show every container and its exit code
docker ps -a
# Show only the exit details
docker inspect <container> --format '{{.State.ExitCode}} {{.State.Error}}'
# Follow a restart loop live
docker logs -f --tail 50 <container>
docker logs often gives you the cause directly: a stack trace, a missing environment variable or a refused database connection. Read the most recent output before changing the image or command.
What the exit code tells you
| Code | Meaning | Usual cause |
|---|---|---|
0 | Completed successfully | The process finished; it may not have been a long-running command |
1 | Generic application error | An unhandled exception. Check the logs. |
125 | The Docker daemon itself failed | A malformed docker run command or invalid flag |
126 | Command found but not executable | Missing +x, or a script with CRLF line endings |
127 | Command not found | Typo, missing binary, or no shell in the image |
137 | SIGKILL (128 + 9) | Out of memory, or a docker stop that timed out |
139 | SIGSEGV (128 + 11) | Segfault, sometimes caused by an architecture mismatch |
143 | SIGTERM (128 + 15) | A clean stop request; normal for docker stop |
For these exit statuses, subtract 128 to identify the signal number. Codes 137 and 143 are the ones you are most likely to encounter in routine container work.
Cause 1: exit code 0 means the process finished
Exit code 0 can look suspicious when you expected a service, but it means the command completed normally. docker run ubuntu starts Bash without a terminal or command to process, so Bash exits and the container follows.
docker run ubuntu # Bash exits when it has nothing to do
docker run -it ubuntu bash # An interactive terminal keeps Bash open
-i keeps standard input open and -t allocates a pseudo-terminal. Together they give an interactive shell something to wait for.
Cause 2: the process daemonised itself
Some services still daemonize by default. If PID 1 starts the service and then exits while the service moves into the background, Docker considers the container finished.
# nginx moves into the background and PID 1 exits
CMD ["nginx"]
# Keep nginx in the foreground
CMD ["nginx", "-g", "daemon off;"]
| Service | Foreground flag |
|---|---|
| nginx | nginx -g "daemon off;" |
| Apache | httpd-foreground or apachectl -DFOREGROUND |
| PostgreSQL | postgres (not pg_ctl start) |
| Redis | redis-server --daemonize no |
| systemd services | Run the service binary directly instead of starting systemd |
Cause 3: exit 127 means the command was not found
Check the command path first, then consider the image’s shell, the script’s line endings and the image architecture.
There is no shell in the image
Distroless and scratch images have no /bin/sh; some other minimal images may omit the shell you expect. Shell-form CMD runs through /bin/sh -c "…", so it cannot work in an image without that binary:
# Shell form needs /bin/sh, which distroless omits
CMD npm start
# Exec form runs the binary directly without a shell
CMD ["node", "server.js"]
Windows line endings
When an entrypoint script has CRLF line endings, the kernel can interpret its shebang as a request for /bin/sh\r. That path does not exist, so the resulting error can misleadingly name the shell.
# Normalize line endings while building
RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh
To prevent the mismatch, add *.sh text eol=lf to the repository’s .gitattributes file.
Architecture mismatch
Running an amd64 image on Apple Silicon, or an ARM image on an amd64 host, can produce exec format error, exit 127 or a segfault. Build and run for the intended platform:
docker build --platform linux/amd64 -t myapp .
docker run --platform linux/amd64 myapp
Cause 4: exit 137 often points to memory pressure
Code 137 means the process received SIGKILL. The kernel’s out-of-memory killer is a common source, but a timed-out docker stop can produce the same code. Check the recorded OOM state:
docker inspect <container> --format '{{.State.OOMKilled}}'
# true means the memory limit was exceeded
docker stats --no-stream
- Raise the container limit with
docker run -m 2gormem_limitin Compose when the workload legitimately needs more memory. - On Docker Desktop, also check the VM’s memory setting. A per-container limit cannot exceed the memory available to that VM.
- For the JVM, consider
-XX:MaxRAMPercentage=75. Older JVMs may read host memory instead of the cgroup limit and allocate more than the container can use. - For Node, use
--max-old-space-sizewhen the JavaScript heap needs an explicit cap below the container limit.
Cause 5: it crashed on a missing dependency
Exit code 1 is application-specific, so let the stack trace lead the investigation. Missing environment variables and dependencies that are still starting are common causes.
In Compose, depends_on waits for a container to start, not for the service inside it to become ready. Gate the dependent service on a health check when startup order matters:
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
api:
build: .
depends_on:
db:
condition: service_healthy # Wait for the readiness check
Getting a shell inside a container that will not run
If the normal entrypoint exits before you can inspect the filesystem, replace it temporarily with a shell:
# Replace the image startup command with a shell
docker run -it --entrypoint sh <image>
# Inspect a stopped container filesystem
docker commit <dead-container> debug-image
docker run -it --entrypoint sh debug-image
# Copy a file from a stopped container
docker cp <container>:/app/config.json ./
# Display the configured startup command
docker inspect <image> --format '{{.Config.Entrypoint}} {{.Config.Cmd}}'
Why tail -f /dev/null is not an application fix
tail -f /dev/null keeps PID 1 running, but it does not repair the service that exited. For an application container, this hides a visible failure behind a container that appears healthy at a glance.
It can be useful in a temporary debugging container or a sidecar designed without its own long-running process. An application container should instead run the application as PID 1 and expose its failure.
Signals, and why PID 1 is special
PID 1 has special responsibilities on Linux, including reaping orphaned child processes, and default signal handling differs from that of other processes. A shell wrapper that does not forward SIGTERM makes docker stop wait for its timeout and then send SIGKILL, producing exit code 137 instead of a clean shutdown.
# Shell form adds a wrapper that may not forward signals
CMD npm start
# Exec form lets PID 1 receive SIGTERM
CMD ["node", "server.js"]
# Add an init process when the application spawns children
# Docker can provide the init process at runtime
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]
Frequently asked questions
Why does my container exit with code 0 when nothing went wrong?
Code 0 means the main process finished successfully. The command may have been short-lived, such as a shell without a TTY, or a service may have forked into the background. A long-running container needs its service to remain in the foreground as PID 1.
What does exit code 137 mean?
128 + 9, meaning SIGKILL. Usually the out-of-memory killer: check docker inspect --format "{{.State.OOMKilled}}". It also appears when docker stop times out after SIGTERM and escalates to SIGKILL, which points at a process that is not handling signals.
How do I see logs from a container that already exited?
docker logs works for stopped containers because their logs remain after the process exits. Use docker ps -a to find the container ID. Avoid --rm while debugging, since removing the container also removes access to those logs.
Why does docker run -it ubuntu bash work but docker run ubuntu does not?
The -i flag keeps standard input open, and -t allocates a pseudo-terminal. Without them, Bash has no terminal or input to wait for, so it exits and the container stops.
Is tail -f /dev/null an acceptable fix?
It can be appropriate for a temporary debugging container or a sidecar designed without its own long-running process. In an application container, it keeps the container running while the service remains unavailable, which can mislead checks that look only at container state.
Why does my container work locally but not in CI?
Compare the host architectures first. An image built on Apple Silicon may not run on an amd64 CI worker, and the reverse is also possible. Set --platform explicitly or publish a multi-architecture image with docker buildx.