Ordered by how often each one turns out to be the answer.
The main process was never going to stay running
CommonThe command completed and returned, so the container had nothing left to do. Classic versions: a base image whose default command is a shell that immediately reaches end-of-input, a script that ends, or a server launched with a trailing `&` so the foreground shell exits while the server is still starting.
Confirm
docker inspect <container> --format '{{.State.ExitCode}} {{json .Config.Cmd}} {{json .Config.Entrypoint}}'Exit code 0 alongside a command that is not a long-running server. If Entrypoint is null and Cmd is something like ["bash"], that is the whole answer.
Fix
- Run the actual server in the foreground as the container's main process. A container does not need a process manager; the application *is* the process.
- If you genuinely need the container to stay up for debugging, run it interactively with `-it` so the shell has a terminal to read from.
- Remove any trailing `&` from the start command. Backgrounding the server means the foreground command finishes, which ends the container.
# Foreground, in exec form
CMD ["node", "server.js"]
# Or, to keep a shell alive for poking around:
docker run -it --entrypoint sh myimage
Exec form also matters for signal handling — see ENTRYPOINT vs CMD.
Required configuration was absent, so the process exited on start-up
CommonThe application read its configuration, found something missing — a database URL, an API key, a required environment variable — and exited rather than starting in an unusable state. This is correct behaviour and it looks like a crash.
Confirm
docker logs <container> 2>&1 | tail -30
An explicit message near the end naming what was missing. Most frameworks say so plainly; the difficulty is that people check `docker ps` rather than the logs.
Fix
- Pass the missing values with `-e` or `--env-file`, or mount the config the application expects.
- Check for a name mismatch rather than an absence — a variable spelled `DATABASE_URL` in compose and read as `DB_URL` in code produces exactly this.
- Do not bake secrets into the image to make this go away. Inject them at run time.
The image was built for a different CPU architecture
OccasionalAn image built on an arm64 machine and run on amd64, or the reverse. The container is created and the binary cannot execute, so it fails almost instantly with an exec-format error.
Confirm
docker logs <container> 2>&1 | head -5; docker image inspect <image> --format '{{.Architecture}}'`exec format error` in the logs, or an image Architecture that differs from your host's. This became common as arm laptops became normal.
Fix
- Build for the target platform explicitly with `docker build --platform linux/amd64`.
- For images used across both, build a multi-platform image rather than one per developer.
ENTRYPOINT and CMD combined into something that is not a command
OccasionalDocker concatenates ENTRYPOINT and CMD. When one is set as a full command and the other adds unexpected arguments, the result can be a valid string that is not a valid invocation — so the process fails immediately with a usage error or a not-found.
Confirm
docker inspect <image> --format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}'Read the two as one line concatenated in that order. If the combined result is not something you would type, that is the fault.
Fix
- Put the executable in ENTRYPOINT and only its default arguments in CMD.
- Use exec form — the bracket syntax — for both. Shell form introduces `/bin/sh -c`, which changes both parsing and signal handling.
The container was killed for exceeding its memory limit during start-up
RareA memory limit lower than the application's start-up footprint. The kernel kills the process before it finishes initialising, so it looks like an immediate exit rather than a resource problem.
Confirm
docker inspect <container> --format '{{.State.ExitCode}} oom={{.State.OOMKilled}}'`oom=true`, or exit code 137. Note that a 137 with `oom=false` means something sent SIGKILL for another reason.
Fix
- Raise the limit above the observed start-up peak, which is often higher than steady-state usage.
- Check whether the runtime is sizing its heap against the host rather than the container — a JVM or Node process that has not been told the limit will happily exceed it.