A backend service is running normally inside a container. During a deployment, the platform asks the container to stop and waits for the service to finish its in-flight requests. No shutdown message appears. After the grace period expires, the platform forcibly kills the container.
The service may have perfectly good shutdown code and still never receive the request. A common cause is this process tree:
The shell holds PID 1, and the actual server is an ordinary child. Signals sent to the container arrive at the shell, not at the server.
The container runtime targets PID 1 with the stop signal. The shell is the target, and the actual server may never see the request. Signals sent to one process do not automatically propagate to its children.
Container PID 1 also has responsibilities that an ordinary application process may not expect. It can adopt orphaned descendants, must ensure that terminated children are reaped, and has special signal behavior on Linux.
A container's main process is not merely the first application to start. It is the init process for that PID namespace and the process that defines the container's lifetime.
On a normal Linux host, the first user-space process is PID 1. It starts or supervises services and acts as the final adopter for processes that lose their parents.
A container can have its own PID namespace. The first process created in that namespace receives PID 1 in the namespace's view, even though the same task has a different PID on the host.
The task has not been duplicated. The kernel exposes a different process identifier depending on which PID namespace observes it.
Inside the container, the process can confirm its namespace-visible PID:
From the host, a container tool can show the host-visible PID:
The two numbers identify the same underlying kernel task.
A container runtime tracks the process it started as the container's main process. That process is normally PID 1 in the container's PID namespace.
When namespace PID 1 exits, the PID namespace can no longer function normally. Linux terminates the remaining processes in that namespace. From the runtime's perspective, the container has stopped.
This has an important practical consequence: a server should usually run in the foreground. If a traditional server forks a daemon into the background and lets its original PID 1 process exit, the background server does not keep the container alive. Container images commonly disable a server's daemon mode for this reason.
The exit status of PID 1 also becomes the container's exit status. A wrapper that loses or replaces the application's status can therefore make successful exits look like failures, or hide real failures behind status 0.
Stopping a container is usually a two-stage protocol rather than an immediate kill.
First, the runtime sends a configurable graceful-stop signal to the container's PID 1. SIGTERM is a common default. The runtime then waits for a configured grace period. If PID 1 is still running when the deadline expires, the runtime sends SIGKILL.
The exact signal and timeout are platform settings, not universal constants. An image can also declare a preferred stop signal, and a runtime or orchestrator may override it.
SIGKILL cannot be caught, blocked, or ignored. Once escalation reaches that point, the application cannot run shutdown hooks, finish requests, flush user-space buffers, or release external leases through normal cleanup code.
The grace period is therefore a real time budget. A service's shutdown path must complete within it.
An ordinary Linux process with the default disposition for SIGTERM terminates when that signal is delivered.
Linux protects an init process, including the init process of a nested PID namespace, from signals that could accidentally destroy its namespace. Processes in the same PID namespace can send its init process only signals for which that init process has installed a handler. Subject to the normal permission checks, the same restriction applies when a process in an ancestor PID namespace signals it.
There are two forced-delivery exceptions for a sender in an ancestor PID namespace: SIGKILL and SIGSTOP. Neither can be caught. A container runtime operates outside the container's PID namespace, so it can ultimately use SIGKILL to end an unresponsive namespace init process.
Consequently, a program with no SIGTERM handler can terminate under SIGTERM when run as an ordinary process yet remain alive under the same request when it is namespace PID 1. If it installs a handler, the signal can be delivered and the handler can run.
The safe rule is not “PID 1 ignores every signal.” The safe rule is:
A program used as container PID 1 must explicitly implement the lifecycle signals it expects to receive.
Some language runtimes and frameworks install signal handling on behalf of the application. Others do not. Verify the behavior of the actual executable rather than assuming that direct execution alone guarantees graceful shutdown.
A signal has a target. Sending SIGTERM to one PID requests an action from that process alone.
The kernel does not walk the target's descendants and repeat the signal:
If PID 1 is a wrapper, it must either:
Forwarding only to one direct child may still be insufficient when that child launches workers or helpers. A supervisor may need to target an entire process group, while also accounting for descendants that create their own groups.
This is one reason a seemingly small shell wrapper can become surprisingly difficult to implement correctly.
Container image formats let a command be expressed in shell form or executable form.
A Dockerfile shell-form command looks like this:
On Linux, the runtime normally interprets it through a shell, conceptually:
The shell can therefore become PID 1 while the server runs as its child. Shell behavior varies: some shells can replace themselves for simple commands, while scripts, pipelines, background jobs, and other shell constructs often leave the shell in place. Correct shutdown should not depend on an optimization that may or may not occur.
Executable form states the program and its arguments directly:
The runtime starts ./server without an intermediate shell, so the server becomes PID 1.
Executable form does not perform shell expansion. Constructs such as $PORT, >, &&, pipes, or wildcard expansion are not interpreted automatically. If startup genuinely requires shell logic, use a deliberate entrypoint script and finish it with exec.
Loading simulation...
exec in an Entrypoint ScriptAn entrypoint often performs brief setup before launching the service:
The corresponding image configuration can pass the server as arguments:
exec "$@" replaces the shell process with the requested program. Replacement preserves the PID, so the server takes over PID 1 and there is no wrapper shell left to forward signals.
The quotes around "$@" matter. They preserve the original argument boundaries, including arguments containing spaces.
Without exec, a script might instead start the server as a child:
The shell remains PID 1, must forward lifecycle signals, must wait for the child, and must return the child's exit status correctly. If none of those supervisory behaviors are required, replacing the shell is simpler and more reliable.
Making the server PID 1 ensures that the runtime's stop signal reaches the server. It does not guarantee that the server responds correctly.
The application must still register the appropriate handler or use a runtime facility that does so. A typical graceful shutdown marks the service as shutting down, stops accepting new work, finishes or cancels in-flight operations, closes resources, flushes required state, and exits before the grace period expires.
Shutdown should be bounded. Waiting forever for a client, worker, or external dependency merely converts graceful shutdown into delayed SIGKILL.
The signal handler itself also deserves care. Low-level POSIX handlers interrupt normal execution and can safely call only a restricted set of functions. Common designs have the handler set a flag or notify the application's normal event loop, which then performs the full shutdown sequence. Managed runtimes often expose a safer callback or cancellation mechanism built around the same idea.
The service should be tested under the same signal, timeout, and startup command used in production. Handling Ctrl+C during local development proves behavior for SIGINT; it does not automatically prove correct SIGTERM handling.
Signal delivery is only half of PID 1's job.
When a child terminates, most of its resources are released immediately, but the kernel retains a small record containing information such as its PID and exit status. The parent removes that record by calling wait() or waitpid(). Until then, the child is a zombie.
SIGCHLD can notify a parent that a child changed state, but the signal itself does not collect the child. The parent must wait:
The loop is essential. Standard signals can coalesce, so several children may terminate before the parent observes one SIGCHLD. One call to waitpid() collects only one child.
A production implementation must also distinguish “no completed child,” “no remaining child,” interruption, and real errors. The important lifecycle invariant is:
Every terminated child must eventually have its exit status collected by its parent or adopter.
An application that never creates child processes may have nothing to reap during normal operation. In practice, services sometimes launch compression tools, certificate helpers, database clients, shell commands, or worker processes indirectly through libraries, so the real process tree is worth inspecting.
If a parent exits while its child is still alive, the child becomes an orphan and is assigned a new parent. Linux can reparent it to the nearest living process designated as a child subreaper. If no closer subreaper applies, the relevant init process becomes the adopter.
Inside a PID namespace, that adopter can be container PID 1. For example, suppose the application launches a helper, the helper launches a worker, and the helper exits first. The still-running worker can be reparented to PID 1. When the worker eventually exits, PID 1 is responsible for reaping it. This can surprise an application that waits only for children it launched directly.
Zombie accumulation is not mainly a memory-consumption problem. Zombies no longer execute and have released most of their former resources. The danger is retained kernel bookkeeping and occupied process slots. A long-running service that repeatedly inherits and fails to reap descendants can eventually lose the ability to create more processes.
An application can be a correct container PID 1 if it handles shutdown signals, manages every child it creates or adopts, and exits with an accurate status.
When the application is not designed for those responsibilities, a minimal init process can sit at PID 1:
Tools such as tini and dumb-init are designed for this narrow role. A runtime can also provide an option that injects a minimal init; Docker, for example, supports --init.
A minimal init commonly:
It is not automatically a full service manager. It does not make an application gracefully drain requests, repair an incorrect process model, or decide how several independent services should be restarted.
For one well-behaved server, direct execution is often sufficient. A minimal init is valuable when the workload launches complex subprocess trees, daemonizes internally, cannot be changed, or has uncertain reaping behavior.
Sometimes a workload intentionally contains a parent and several workers. Sending the stop signal only to the parent works if that parent reliably tells every worker to stop and waits for them.
If it does not, a supervisor can forward a signal to a process group rather than one PID. The application leader and workers in that group can then receive the lifecycle event together.
Process groups are useful but not magical. A child can create another group or session, and each process still applies its own signal disposition. A supervisor must understand the workload's real topology.
Avoid combining unrelated long-running services in one container merely because a wrapper can start them. If several processes are required for one workload, define which process owns shutdown, how signals reach every participant, how exits are collected, and which status should end the container.
Do not infer the process tree from the image's intended command. Inspect the running container.
This command deliberately keeps a shell as PID 1 and starts sleep as a child:
View its processes from the host:
The output should show a shell and a sleep process. Depending on the host's ps implementation, a more detailed view may be available:
Now compare a directly executed command:
The direct container has no wrapper shell. This demonstrates correct signal routing, but sleep is not a graceful server and should not be used to demonstrate application cleanup.
Clean up the examples:
If a container takes the full timeout to stop, inspect before concluding that the application ignored the signal. PID 1 may be an unexpected wrapper, the configured stop signal may differ, the signal may not reach all workers, or shutdown may simply exceed the deadline.
Useful inspection begins with three questions:
Linux can create a PID namespace directly with unshare:
Inside the new shell:
The shell reports PID 1 because it is the first process in the new PID namespace. From another host shell, the same task has a normal host PID.
Exit the namespace shell when finished:
This experiment exposes the central container model without requiring an image: container PID 1 is an ordinary Linux task placed at the root of a new PID namespace, with init-like lifecycle responsibilities inside that namespace.
Begin with process identity rather than the application's shutdown code:
If PID 1 is a shell or launcher, check whether it uses exec or intentionally forwards signals. If the application is PID 1, confirm that it installs a handler for the configured stop signal.
Next, reproduce the production stop request and measure the result:
A duration close to the full timeout suggests escalation, but timing alone does not prove why. Correlate it with application logs and the process tree.
Finally, inspect for children left in zombie state while the container is still running:
Look for Z in the state column and identify the corresponding parent. One short-lived zombie can be transient; a stable or growing population indicates that a parent is not collecting child status.
SIGKILL after a configurable timeout.exec "$@" avoid an unnecessary wrapper shell.5 quizzes