✦ For everyone, free.

Practical knowledge for real and everyday life

Home

16.2.1.1 Main Process Exit

A focused guide to Main Process Exit, connecting core concepts with practical Docker and container operations.

Main process exit is the event that defines a container's own lifecycle: a Docker container runs for exactly as long as its PID 1 process runs, and the moment that specific process exits, for any reason, the container itself stops, regardless of whether other processes the main process may have spawned are still running, which is a frequent and important source of confusion for anyone expecting container lifecycle to work more like a traditional virtual machine's.

The container is its main process

Unlike a virtual machine, which continues running as long as its own init system has anything left to manage, a container's lifecycle is tied specifically and only to its designated main process, conventionally referred to as PID 1 within the container's own PID namespace:

CMD ["node", "server.js"]
docker run -d --name my-api my-api:1.4.0
docker exec my-api ps aux
PID   USER     COMMAND
1     node     node server.js

The moment this specific process, PID 1 inside the container, exits, the container transitions to a stopped state immediately, even if that process had spawned other child processes that happen to still be running at that exact moment.

Background processes do not keep a container alive

A common point of confusion is starting a background process and assuming the container will continue running because something is technically still active inside it:

CMD ["sh", "-c", "node server.js & sleep infinity"]

This specific example actually does keep the container running, but only because sleep infinity, not node server.js, has become the effective main process the shell waits on; if the intention was for the Node.js server to be the main process and the container's lifecycle to track its health, this construction has actually decoupled the two, meaning the container would continue reporting as running even after the Node.js process itself crashed in the background, since nothing about that crash affects the actual PID 1 process, the shell waiting on sleep infinity.

CMD ["node", "server.js"]

The simpler, more correct version makes the actual application process the container's PID 1 directly, ensuring the container's lifecycle genuinely tracks that specific process's health rather than an unrelated placeholder.

Daemonizing inside a container is generally an anti-pattern

A process that forks into the background and detaches from its parent, a traditional Unix daemonization pattern, breaks the assumption that the foreground process Docker started is the one whose lifecycle should be tracked, since the original foreground process exits (successfully, from its own perspective) immediately after the fork, causing the container to stop even though the actual daemonized work continues briefly in a now-orphaned child process that has no relationship to the container's own lifecycle anymore:

CMD ["my-daemon", "--daemonize"]

The correct approach for a containerized process is running it in the foreground, the way most modern server software supports through an explicit flag or by being designed for containerized deployment from the outset, rather than relying on the traditional daemonization pattern that predates container-based deployment entirely:

CMD ["my-daemon", "--foreground"]

Multi-process containers and process supervisors

For genuinely legitimate cases requiring more than one process inside a single container, a process supervisor like supervisord or s6 can serve as PID 1, managing multiple child processes and only exiting itself if a sufficiently critical condition occurs:

[program:api]
command=node server.js

[program:worker]
command=node worker.js
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]

This pattern works, but it is worth recognizing the trade-off explicitly: the container's health and lifecycle now depend on the supervisor's own logic for what counts as a fatal condition, which is an additional layer of indirection and configuration compared to the simpler, more directly observable one-process-per-container model that most container orchestration tooling and conventions are designed around.

Restart policies and main process exit

A container's restart policy reacts specifically to the main process exiting, which means understanding exactly what that process is, and what its exit actually represents, determines whether a restart policy behaves as intended:

docker run -d --restart=on-failure my-api

If the main process is a wrapper script that always exits cleanly regardless of whether the actual application work inside it succeeded or failed, a restart policy keyed on the wrapper's own exit code will never trigger a restart for an application-level failure that the wrapper itself swallowed and did not propagate as its own non-zero exit.

#!/bin/sh
node server.js
exit 0  # always exits 0, even if the node process above crashed
#!/bin/sh
node server.js
exit $?  # propagates the actual exit code of the wrapped process

Ensuring a wrapper script propagates the actual exit status of the process it wraps, rather than always reporting success unconditionally, is necessary for a restart policy to correctly react to the underlying application's real success or failure state.

Diagnosing why the main process exited

When a container stops unexpectedly, identifying exactly what its main process actually was, and what caused that specific process to exit, is the central question, since everything else (child processes, background work, supervisors) is secondary to this single, defining process:

docker inspect my-api --format '{{.Config.Cmd}} {{.State.ExitCode}}'
docker logs my-api --tail 50

Confirming the actual command configured as the container's entrypoint or command, alongside its reported exit code and recent log output, anchors the investigation directly on the one process whose behavior actually determines the container's own lifecycle.

Common mistakes

  • Assuming a container stays running because some process inside it is still active, without confirming that process is actually the container's main, PID 1 process.
  • Daemonizing a process inside a container using a traditional Unix forking pattern, causing the container to stop immediately after the foreground process exits despite the daemonized work continuing briefly elsewhere.
  • Writing a wrapper script as the main process that always exits with a success code regardless of the wrapped application's actual outcome, breaking restart policies that depend on an accurate exit code.
  • Adopting a multi-process supervisor pattern without recognizing the added indirection it introduces into how the container's lifecycle and health are actually determined.
  • Investigating an unexpected container stop without first confirming exactly what command was configured as the container's actual main process.

A container's entire lifecycle is defined by its main process, not by whatever else happens to be running inside it at any given moment, and most confusion about containers stopping unexpectedly, or not stopping when expected, traces back to a mismatch between what a developer assumed was the tracked main process and what Docker actually configured as PID 1.