skip to content

A container's entrypoint is a bash script whose last line is `myserver --config /etc/app.yml`. Stopping the container always takes the full grace period before the process dies, and the server never runs its shutdown routine. What is wrong with that last line, and what is the fix?

level: seniorimportance: must knowfreq 55%

answer

  1. ask which process the signal reaches
  2. the wrapper is process 1, not the app
  3. the shell is holding the notice
  4. replace the shell instead of forking
  5. exec "$@" as the final line

basics

~20 s

The shell stays as process 1 and the server runs as its child, so the runtime's SIGTERM goes to the shell and never reaches the server. Ending the line with exec replaces the shell, making the server process 1.

solid answer

~50 s

The runtime signals PID 1 on stop. Because the script launched the server as a child, PID 1 is bash, and two things then conspire against you: the kernel does not deliver a default-action signal to PID 1, so a trap-less bash simply ignores SIGTERM, and even with a trap installed bash defers running it until the foreground command finishes. So the server sees nothing, the grace period expires, and everything is killed outright. The fix is `exec myserver --config /etc/app.yml` — exec replaces the shell process with the server, so the server becomes PID 1 and receives SIGTERM directly. The generic form at the end of an entrypoint is `exec "$@"`, which hands off to whatever command the image was given. The cost is that nothing after that line runs, including an EXIT trap, so any cleanup has to happen before the handoff or be moved into the server itself.

code

bash · 8 lines
bash
#!/usr/bin/env bash
set -euo pipefail

: "${DATA_DIR:=/var/lib/app}"
mkdir -p "$DATA_DIR"

# Setup finishes here; nothing below exec will ever run.
exec "$@"

go deeper

for a junior

Know that the entrypoint script itself becomes process 1 and that the stop signal goes there, and that ending the script with exec makes the real program take over that process.

for a middle

Explain both reasons the signal goes nowhere — the kernel withholds default-action signals from PID 1, and bash defers traps until the foreground command returns — and write the exec "$@" idiom correctly.

for a senior

Show the consequences you would check for: buffers unflushed, connections dropped, exit status coming from the shell rather than the application, and cleanup lost because an EXIT trap never runs after exec.

for a principal

Decide when a wrapper has outgrown the handoff pattern and needs a real init in the image, and set the expectation that every service image reports the application's own exit status to its supervisor.

## Who is process 1 A container starts one process, and that process is PID 1 inside the container's PID namespace. When the runtime is asked to stop the container it sends SIGTERM to PID 1, waits a grace period — ten seconds by default for `docker stop` — and then sends SIGKILL to everything remaining. That handshake is the only shutdown notification the application gets. If the entrypoint is a shell script and its final line runs the server as an ordinary command, then bash is PID 1 and the server is a child of it. The signal is delivered to the shell. Whether the child ever hears about it is entirely up to what the shell does next. ## Two reasons the shell relays nothing **PID 1 is special to the kernel.** A signal whose disposition is the default action is not delivered to PID 1 at all. This is what stops a process from accidentally killing the system's init. Applied to a container, it means a bash that has installed no handler for SIGTERM does not merely ignore it as a matter of policy — it never receives it. **Even a handler would not run in time.** Suppose the script does install a trap for SIGTERM. Bash runs a trap handler only after the currently executing foreground command completes. With the server running in the foreground, the handler is queued behind a command that will not return until the server exits, which is precisely the thing you are trying to make happen. So the grace period elapses, SIGKILL arrives, and the server dies without flushing buffers, draining connections, deregistering from a load balancer, or completing an in-flight request. The visible symptom is that every stop takes exactly the grace period, and the application's own shutdown log lines never appear. ## The fix ```bash exec myserver --config /etc/app.yml ``` `exec` without a redirection replaces the shell process with the named program: same process, same PID, new program image. The server becomes PID 1 and receives SIGTERM directly, with its own handler intact. Stops become fast and graceful, and the server's exit status becomes the container's exit status instead of the shell's. The generic form belongs at the end of any entrypoint wrapper that is meant to set up state and then get out of the way: ```bash #!/usr/bin/env bash : "${DATA_DIR:=/var/lib/app}" mkdir -p "$DATA_DIR" /opt/app/wait-for-db exec "$@" ``` The quoted `"$@"` passes the image's own command through intact, one argument per element, so the wrapper stays generic and the command can be overridden at run time. ## What handing off costs you After `exec` succeeds, nothing later in the script runs — there is no script any more. That includes an EXIT trap, so a wrapper that created a temporary directory and expected to remove it on the way out will not. Two workable answers: do that cleanup before the handoff and let the container's ephemeral filesystem take care of the rest, or accept that lifecycle-scoped cleanup belongs in the long-lived process rather than in the wrapper that started it. There is also a case where you genuinely cannot hand off: the wrapper must supervise more than one process, or must do work after the server exits. Then the shell stays as PID 1 and takes on real responsibilities — installing a handler, running the server in the background so the handler can run, forwarding the signal on, and waiting for the child. That is a supervisor, and the moment you are writing one it is worth asking whether a purpose-built minimal init belongs in the image instead. It also inherits PID 1's duty of reaping orphaned children, which is exactly the kind of obligation a shell script should not be quietly taking on. ## The same script under systemd The failure does not reproduce identically outside containers, and knowing why matters. systemd's default kill mode signals every process in the unit's control group, so a child of a wrapper script does get SIGTERM even when the wrapper does not forward it. The wrapper still hurts you in other ways: it, not the server, is the unit's main process, so the exit status systemd records is the shell's, and a wrapper that exits successfully after its child died hides the failure from the service manager entirely. Ending with `exec` is the right habit in both environments, for slightly different reasons.

  • Would adding a SIGTERM trap to the wrapper fix it without exec?
    Not on its own. Bash runs a trap handler only after the current foreground command returns, and the foreground command is the server you are waiting on. To make a trap useful the wrapper has to start the server in the background and wait on it, then forward the signal from the handler — a real supervisor, and more moving parts than simply handing off with exec.
  • What do you lose by ending the entrypoint with exec "$@"?
    Everything after that line, because the script's process no longer exists — including any EXIT trap and its cleanup. Do such cleanup before the handoff, or move it into the long-lived process. In exchange the application's own exit status becomes the container's, which is what any supervisor should be looking at.
  • Why does the same wrapper misbehave less obviously under systemd than in a container?
    systemd's default kill mode signals every process in the unit's control group, so the child receives SIGTERM even though the wrapper never forwards it. The wrapper is still the unit's main process, so its exit status is what systemd records — meaning a wrapper that returns success after its child died reports a healthy service.

A wrapper script that does not exec is a doorman who accepts the eviction notice on the tenant's behalf and never slides it under the door.

saying these in an interview costs you the question

  • Assumes a child automatically receives the signal sent to its parent
  • Adds a SIGTERM trap while the server still runs in the foreground
  • Blames the runtime's grace period instead of the entrypoint
  • Uses exec "$@" but still expects the EXIT trap to run
  • Thinks bash forwards signals to the foreground job by default

context