The Exit Code Isn't the Bug, It's a Symptom
Every time a container stops, Docker prints an exit code. Most people glance at it, see a 0, assume something is broken because the container "should" still be running, and immediately start googling. But an exit code isn't an error message. It's a signal about why the main process inside that container stopped, and the number itself tells you exactly where to look.
The Docker forum thread that inspired this guide is a perfect example. A beginner pulls the node image, runs it with docker run <image_id>, and the container exits instantly with code 0. No crash, no log output, nothing obviously wrong. Then they run the same image with -it and it stays alive. That difference isn't luck, it's the entire mechanism of how Linux containers decide to live or die.
What Actually Keeps a Container Running
A container is not a virtual machine. It's a process, plus whatever child processes that process spawns, wrapped in namespaces and cgroups. Docker doesn't monitor "the container" as an abstract thing. It monitors PID 1 inside that namespace.
The moment PID 1 exits, the container exits. That's it. There's no separate lifecycle. If you pull a base image like alpine or node and run it with no command, PID 1 is usually a shell that has nothing to do, so it finishes its instructions and exits almost instantly, often with code 0.
Attaching -it doesn't magically "fix" your container. It attaches a pseudo-TTY and keeps stdin open, which gives the shell a reason to sit and wait for input instead of running to completion. Remove the terminal, and the shell has no job left, so it exits. That's exactly what the forum user hit: docker run -it <image_id> worked because the interactive terminal became the foreground process keeping PID 1 alive, not because the image itself was configured to run a service.
Decoding Exit Code 0: Success, Not Failure
Exit code 0 means the main process finished cleanly and returned a success status. In a script or a one-off job, that's completely correct behavior. If you run a container to execute a migration script, run a build step, or process a batch file, code 0 is the goal, not the problem.
The confusion happens when people expect a container to behave like a long-running service but the image's actual entrypoint is a short-lived task. Base images like alpine, debian, or busybox have no application baked in. Without a command, their default shell just starts, has nothing queued, and exits successfully. Zero children, zero foreground work, zero surprises. It's working exactly as designed. It just wasn't designed to be a service.
Check this by inspecting what actually ran:
docker inspect --format='{{.Config.Cmd}} / {{.Config.Entrypoint}}' <image_id>
If both come back empty or point to something trivial like /bin/sh, you've found your answer. The container isn't crashing. It's finishing a task that has no ongoing work attached to it.
Exit Code 1: A Real Application Error
Code 1 is the generic "something in the app threw an unhandled error" code. This is different from code 0 in one critical way: the process tried to run and failed, rather than simply having nothing to do.
This usually shows up with actual application images, not bare base images. A Node.js entrypoint missing an environment variable, a Python script hitting an unhandled exception, or a Go binary panicking on startup will all typically return 1. The fix here is almost always in the logs:
docker logs <container_id>
If logs are empty even on a code 1 exit, check whether your application writes to a log file inside the container instead of stdout/stderr. Docker only captures stdout and stderr by default, so an app logging to /var/log/app.log will look silent from the outside even while failing loudly on the inside.
Exit Code 137: The OOM Kill You Didn't See Coming
This is the one people misdiagnose the most. Exit code 137 is 128 + 9, meaning the process received signal 9, SIGKILL. Unlike 1 or 0, the application never got to choose this outcome. Something outside the process forcibly terminated it, and in the overwhelming majority of cases in containerized environments, that something is the Linux kernel's out-of-memory killer.
If you've set a memory limit on the container and the process inside exceeds it, the kernel doesn't ask nicely. It sends SIGKILL immediately, no cleanup, no final log line, no graceful shutdown handler running. That's why 137 exits often come with suspiciously empty logs, the same symptom people mistake for a startup problem.
Confirm it with:
docker inspect <container_id> --format='{{.State.OOMKilled}}'
If that returns true, you're not dealing with a Docker configuration issue at all. You're dealing with a memory problem in the application itself, or a limit set too aggressively for what the app actually needs. This is exactly the kind of failure worth pairing with proper memory leak detection in production rather than just raising the memory limit and hoping it goes away, because raising the limit on a leaking process just delays the same crash.
Exit Code 143: SIGTERM, Usually On Purpose
Code 143 is 128 + 15, signal 15, SIGTERM. This is the polite version of a kill. Something asked the process to shut down gracefully, and it complied. You'll see this constantly in orchestrated environments: Kubernetes sending a termination signal during a rolling deployment, Docker Compose stopping services, or docker stop timing out and escalating.
The important distinction from 137: SIGTERM gives the application a chance to clean up. If your app has a signal handler that closes database connections and flushes buffers before exiting, 143 is a clean shutdown, not a crash. If it doesn't handle SIGTERM at all and just ignores it, Docker waits out the default grace period, then sends SIGKILL anyway, and you'll see 137 instead even though the intent was graceful.
This matters a lot if you're running under an orchestrator that scales containers up and down. If you're chasing pods or containers that seem to disappear right when load changes, it's worth checking whether it's actually Kubernetes HPA scaling behavior sending termination signals rather than an application bug.
A Practical Checklist for Reading Exit Codes
When a container exits and you don't immediately know why, work through it in this order:
- Run
docker inspect <container_id>and checkState.ExitCode,State.OOMKilled, andState.Errortogether, not the exit code alone. - Check
docker logs <container_id>even if you expect it to be empty, because a genuinely empty log combined with 137 is itself diagnostic information. - Confirm what the entrypoint actually is with
docker inspect --format='{{.Config.Entrypoint}} {{.Config.Cmd}}'before assuming the image is broken. - Check memory limits with
docker inspect --format='{{.HostConfig.Memory}}'if the exit code is 137. - Reproduce interactively with
docker run -it --entrypoint /bin/sh <image>to poke around inside the filesystem without triggering the real entrypoint.
Fixing Code 0 the Right Way
If you actually need a base image to keep running for debugging purposes, don't reach for -it as a permanent solution, it depends on a terminal staying attached. Instead give the container an explicit foreground command:
docker run -d alpine tail -f /dev/null
This keeps PID 1 alive indefinitely without requiring an interactive session, which is the correct pattern for debugging containers or building base layers you want to shell into later with docker exec.
For real application images, the fix isn't to force them to stay alive artificially. It's to check whether the entrypoint script is actually starting a foreground service. A common beginner mistake is overriding CMD with something like docker run myapp /bin/bash, which replaces the intended startup script entirely and gives you a shell with nothing configured to run in the background.
Verifying You've Actually Fixed It
Don't just check that the container status says "Up." Confirm the right process is PID 1:
docker exec <container_id> ps aux
You want to see your actual application, not a leftover shell or a sleep loop, sitting at PID 1. If it's there and the exit code history in docker inspect shows 0 only for intentional one-off runs, 137 only during real memory pressure, and 143 only during deliberate stop or deploy events, your container's lifecycle is behaving exactly the way it should.