CMD, ENTRYPOINT and PID 1
What runs when a container starts, which process gets the stop signal, and when to use --init.
cd ~/lab && curl -fsSLO https://secopslog.com/lab-files/docker-fund/cmdentrypoint.tar.gz && tar -xzf cmdentrypoint.tar.gz, which creates ~/lab/cmdentrypoint/. SHA-256: 43d6648f58b7572d13b99b73ffacdc81d7ec1faeb02ad85613f73eab070fd25cEvery deploy of one service takes ten seconds longer than it should. docker stop hangs for exactly ten seconds, the container ends with exit status 137, and the application never logs the shutdown message its code prints on SIGTERM. Nothing is wrong with the application. The process that receives the signal is not the application. Which process that is comes down to two Dockerfile instructions, CMD and ENTRYPOINT, and to how each one is written.
Use the main lab VM and unpack the lesson files into ~/lab/cmdentrypoint. The greet/ folder holds small Alpine images that only echo text, so the effect of each rule is visible in one line of output; signals/ holds the shutdown experiments.
CMD is a default
CMD sets the command a container runs when docker run gets no command of its own. Anything after the image name replaces it completely.
ENTRYPOINT is the program
ENTRYPOINT names the program that always runs. Words after the image name are appended to it as arguments instead of replacing it. Only --entrypoint on the command line swaps the program, and it takes just the program name; its arguments still go after the image name.
Both together
With both set, the container runs ENTRYPOINT followed by CMD, and arguments on docker run replace only the CMD part. This is the common shape for images that behave like a command-line tool: the program is fixed, the default arguments can be swapped. docker image inspect shows both values the way Docker stores them.
The last two runs show a rule that surprises people: --entrypoint also discards the image's CMD. The first echo printed an empty line, not hello world. Only arguments you pass explicitly reach the new entrypoint. In images without an ENTRYPOINT, CMD itself is the program, which is why CMD ["node", "server.js"] worked on its own in "Writing a Dockerfile". Add an ENTRYPOINT to such an image later and that CMD silently turns into arguments for the new program.
Exec form and shell form
Both instructions have two spellings. Exec form is a JSON array, ["echo", "greeting:"], and Docker runs that program directly with those arguments. Shell form is a plain string, echo greeting:, and Docker stores it as ["/bin/sh", "-c", "echo greeting:"], so a shell parses the string at start-up (which is what makes $VARIABLE expansion and && work). For ENTRYPOINT, shell form has a side effect worth seeing once.
goodbye never arrived and neither did the CMD default. Docker did append them, but as extra arguments to /bin/sh -c, which ignores arguments after its command string unless the string uses them as $0, $1 and so on. A shell-form ENTRYPOINT therefore ignores both CMD and anything passed on docker run, with no warning.
Docker does not check the program at build time
A build only records ENTRYPOINT and CMD in the image configuration. Whether the program exists is checked when a container starts. Dockerfile.missing names a program that Alpine does not have:
FROM alpine:3.22ENTRYPOINT ["greet"]CMD ["World"]
The build succeeded; the run failed in runc, the low-level runtime that starts the container process, with exec: "greet": executable file not found in $PATH, and the CLI exited with 127. When you see 127 from a fresh image, check the ENTRYPOINT or CMD spelling and whether the binary is installed in the image.
PID 1 and docker stop
docker stop sends the image's stop signal (SIGTERM unless the image sets STOPSIGNAL) to the container's main process, the one with PID 1 inside the container. It waits 10 seconds by default, then sends SIGKILL, which cannot be caught; a process killed that way exits with 137 (128 + 9). PID 1 is special in one way that matters here: the kernel does not apply the default action of a signal to PID 1. An ordinary process without a SIGTERM handler dies on SIGTERM; PID 1 without a handler ignores it. So the question for every image is which process is PID 1, and whether it handles SIGTERM.
The lesson files contain a stand-in service, a shell script that installs a SIGTERM handler with trap, prints its PID, and waits. Two Dockerfiles start it; they differ only in the base image and in the spelling of the last line:
FROM alpine:3.22COPY serve.sh /serve.shCMD ["/serve.sh"]
FROM debian:trixie-slimCOPY serve.sh /serve.shCMD /serve.sh
The build step below also builds Dockerfile.nohandler, used in the next section.
lab-sig:exec starts the script in exec form on Alpine. lab-sig:shell starts it in shell form, CMD /serve.sh, on Debian, whose /bin/sh is dash; Docker stored it as /bin/sh -c /serve.sh. Start both and read /proc/1/cmdline in each, the command line of PID 1.
In the exec-form container the script is PID 1 (/bin/sh /serve.sh is the interpreter running the script), it receives SIGTERM, logs its message and exits in about a tenth of a second. In the shell-form container dash stayed at PID 1 and started the script as PID 7. dash has no SIGTERM handler, so as PID 1 it ignored the signal; the script never received it; after 10 seconds both died from SIGKILL.
Shell form does not always end this way. Some shells replace themselves with the command when the -c string is a single simple command, and BusyBox sh on Alpine does. The same Dockerfile line on Alpine leaves the script as PID 1 and stops quickly:
FROM alpine:3.22COPY serve.sh /serve.shCMD /serve.sh
Whether the shell stays depends on the shell, its version and the command string, which is exactly why you should not depend on it. Exec form makes the answer the same everywhere: your program is PID 1. If you need shell features at start-up, put them in a script that ends with exec your-program "$@", so the shell hands PID 1 to the program after it has done its work.
Exec form is not enough without a handler
Exec form only decides who receives the signal. If that process does not handle SIGTERM, it is PID 1 and ignores it. sleep has no handler, so Dockerfile.nohandler (FROM alpine:3.22 and CMD ["sleep", "600"], exec form) still waits the full 10 seconds. docker run --init puts a tiny init process, docker-init (Docker's bundled build of tini), at PID 1. It starts your command as a child, forwards signals to it, and reaps exited child processes. Your process is no longer PID 1, so its default signal actions apply again.
The STATUS column sums up the experiment. Exit 0: the script handled SIGTERM and exited cleanly. Exit 143 (128 + 15): sleep under docker-init was terminated by SIGTERM, quickly. Exit 137: the container sat out the timeout and was killed. For a real service the best result is the first one, an application that handles SIGTERM by finishing in-flight work and closing connections, like the handler in "Writing a Dockerfile". Use --init (or init: true in a Compose file) when you cannot change the application, or when it starts child processes that would otherwise linger as zombies. If a clean shutdown legitimately needs longer than 10 seconds, raise the timeout with docker stop -t or docker run --stop-timeout rather than letting SIGKILL cut it short.
Official images and their entrypoint scripts
Many official images, nginx and postgres among them, set ENTRYPOINT to a script that prepares configuration before the server starts. nginx also sets its own stop signal.
Passing a different command still works: the script ends with exec "$@", so after its preparation it replaces itself with whatever command it was given, which keeps that command at PID 1. The traps with such images are setting your own ENTRYPOINT, which replaces their script and its preparation, and passing flags the script interprets itself. Read the image's documentation before you override either. STOPSIGNAL SIGQUIT is why docker stop on nginx triggers a graceful shutdown that lets open connections finish.
Clean up
ENTRYPOINT ["echo", "log:"] and CMD ["idle"]. What does docker run --rm myimg started print?CMD ["python", "app.py"] (exec form) takes 10 seconds to stop and exits with 137. Its code has no SIGTERM handler. What fixes the stop?ENTRYPOINT /app/start.sh and CMD ["--port", "8080"]. The service always starts on its default port, even with docker run myimg --port 9090. Why?Try this
Work through “Clean up” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
If you keep one thing from cmd, entrypoint and pid 1, keep “Clean up”. Decide now which check you will run when this shows up on a live system, and write it somewhere your team will find it.