A container's entrypoint is a bash script whose last line is "$@". Stopping the container always takes the full ten-second timeout and the application never logs its shutdown message. Explain what the shell is doing, and what change to that last line fixes it.
answer
- count the processes in the container
- the runtime signals only the first one
- bash waits, it does not relay
- replace the shell instead of forking
- keep the quotes around the arguments
basics
~20 sThe bash script is the container's main process, and the application is only its child. The stop signal goes to bash, which neither forwards it nor acts on it, so the application is eventually killed outright. Changing the last line to exec "$@" makes the application the main process itself.
solid answer
~50 sRunning `"$@"` starts the application as a *child* of the entrypoint script, so bash stays alive as the container's main process while the app runs one level down. When the runtime stops the container it signals that main process — bash — and bash does two unhelpful things: it does not relay signals to the child it is waiting on, and as the main process it ignores signals it has no trap for. Nothing reaches the application, the grace period expires, and the runtime kills everything the hard way — which is why you see the full timeout and no shutdown log. Changing the line to `exec "$@"` makes bash replace itself with the application: same process, so the app *is* the main process and the stop signal is delivered straight to its own handler. Keep the quotes, or the arguments get re-split.
code
bash · 11 lines#!/usr/bin/env bash
set -euo pipefail
: "${DATABASE_URL:?DATABASE_URL is required}"
./migrate
# Wrong: bash stays as the main process and swallows the stop signal.
# "$@"
# Right: bash is replaced, so the app is the main process and is signalled directly.
exec "$@"go deeper
Know the idiom: an entrypoint script should end with exec "$@" so the real program takes over the script's process rather than running underneath it as a child.
Explain the two-process tree, that the runtime signals only the main process, and that exec keeps the process number so the application receives the stop signal itself. Know that exec ends the script.
Diagnose from the symptom: full grace period plus no shutdown log means the signal is not reaching the application. Walk the process tree, distinguish a shell swallowing the signal from an application with no handler, and know that a trap over a foreground child cannot fire.
Own graceful shutdown as a platform property. Decide where the responsibility sits — a correct entrypoint convention, a runtime-provided init, or an in-app handler — and set the grace period against real drain times so rollouts do not silently drop in-flight work.
## What the process tree actually looks like With `"$@"` as the last line, the container runs two processes: ``` bash /entrypoint.sh <-- the container's main process \_ myapp <-- your application, a child ``` The container runtime knows only about the first one. That distinction is the whole bug. ## Why the signal goes nowhere A graceful stop sends a termination signal to the container's *main* process and then waits out a grace period — ten seconds by default in the common runtimes — before killing what is left. Two separate bash behaviours combine to swallow it: 1. **Bash does not forward signals to the child it is waiting on.** While a foreground child runs, bash is blocked waiting for it. It does not relay anything to that child on its own, and even a `trap` you install will not run until the child exits — bash defers the handler until the foreground command completes. Installing a trap and expecting instant forwarding is a common half-fix that does not work for that reason. 2. **The main process of a container gets special treatment.** For a process that is number one in its namespace, signals with no handler installed are simply not acted on. Bash has no handler for the stop signal, so it neither dies nor forwards. The container just sits there. The grace period elapses, the runtime resorts to the unblockable kill, and every process in the container disappears at once. The application never got the chance to finish in-flight requests, flush buffers, deregister from a load balancer or write its shutdown line — which is exactly the symptom described. ## What exec changes ```bash exec "$@" ``` `exec` replaces the shell's process image with the application instead of forking a child. The process number does not change, so the application *becomes* the container's main process: ``` myapp <-- the container's main process, same PID as bash had ``` Now the stop signal is delivered to the application itself. If the application installs a handler — as any well-behaved server does — it runs its shutdown path immediately and exits, usually in well under the grace period. As a bonus, the extra bash process disappears from the image's runtime footprint and from every process listing. Note that the special "unhandled signals are ignored" rule applies to the application too, so an application that installs *no* handler still will not die from the polite signal and will still hit the timeout. `exec` removes the shell from the equation; it cannot make an unprepared program graceful. ## Keep the quotes `exec "$@"` and `exec $@` are not the same. Unquoted, the arguments are re-split on whitespace, so a command like `myapp --message "hello world"` arrives as four arguments instead of three. The quoted form expands to exactly the argument list the entrypoint received, one word per original word. ## What you give up `exec` ends the script. Any line after it is dead, and any `EXIT` trap you registered — a temp-directory cleanup, for instance — will never run, because the shell that would run it no longer exists. Do the setup and cleanup that must happen *before* the handoff: ```bash #!/usr/bin/env bash set -euo pipefail : "${DATABASE_URL:?DATABASE_URL is required}" /usr/local/bin/wait-for-db ./migrate exec "$@" # hand off; nothing below this line runs ``` ## When you genuinely cannot hand off Sometimes the script must stay alive — it supervises two processes, or it has to do work after the app exits. Then you own the signal plumbing yourself: run the app in the background, record its PID, install a trap that relays the signal to that PID, and `wait` on it. Because bash defers handlers until a foreground command finishes, backgrounding plus `wait` is what makes the trap able to run at all. Understand that you are now writing a process supervisor in bash, and that supervisors are harder to get right than they look — reaping orphaned children, propagating the right exit status, and handling a second signal during shutdown are all now your problem. The lighter alternative is to let the runtime insert a tiny purpose-built init process as process one (Docker's `--init` flag does this), which reaps zombies and forwards signals correctly. That solves the reaping problem; it does not remove the need for `exec` in your own entrypoint, since a shell sitting between init and your application still swallows the signal. ## The review heuristic Any entrypoint script that ends by *running* a command rather than `exec`ing it is a graceful-shutdown bug waiting to be reported as "deploys are slow" or "we drop connections on every rollout". It is one of the highest-value one-word fixes in container packaging.
- Someone adds trap 'kill $!' TERM to the entrypoint but keeps running the app in the foreground. Does that fix it?No. While a foreground child is running, bash defers trap handlers until that command finishes, so the handler cannot fire during shutdown. To make a trap-based relay work you must background the app, capture its PID with `$!`, and `wait` on it — at which point you are writing a supervisor and should ask whether `exec` would not be simpler.
- After switching to exec "$@", stops still take the full grace period. Where do you look next?At the application. With `exec` the signal is now delivered to it directly, so a continued timeout means the app installs no handler for the stop signal, or its shutdown path blocks — draining a connection pool, waiting on a long request, or flushing to a slow dependency. Reproduce it outside the container by signalling the process and watching what it logs.
- Why is exec $@ without quotes a bug?Unquoted, the expansion is subject to word splitting, so any argument containing whitespace is broken into several. `myapp --msg "hello world"` becomes four arguments and the program either misparses its options or fails. `"$@"` expands to exactly one word per original argument and is the only correct form when forwarding an argument list.
saying these in an interview costs you the question
- Thinks the stop signal reaches every process in the container
- Believes bash automatically forwards signals to its children
- Adds a trap while keeping the app in the foreground and calls it fixed
- Blames the application before checking the entrypoint's process tree
- Writes exec $@ unquoted and breaks arguments containing spaces