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: a68c0a62aa6bc9cd1f937e227229c51dd1a194bacfcbf04ec3b869f3918b53ecEvery 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's wrong with the application. The process that receives the signal isn't the application. Which process it is comes down to two Dockerfile instructions, CMD and ENTRYPOINT, and to how you write each one.
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 you can see the effect of each rule in one line of output. signals/ holds the shutdown experiments.
CMD is a default
CMD sets the command a container runs when you give docker run 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 you put 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. That's the common shape for images that behave like a command-line tool: the program is fixed, and you can swap the default arguments. 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:. Docker stores it as ["/bin/sh", "-c", "echo greeting:"], so a shell parses the string at start-up. That's 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. That shell ignores arguments after its command string unless the string uses them as $0, $1 and so on. So a shell-form ENTRYPOINT ignores both CMD and anything you pass 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 doesn't have:
FROM alpine:3.24ENTRYPOINT ["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. 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 can't be caught. A process killed that way exits with 137 (128 + 9). PID 1 is special in one way that matters here. The kernel doesn't apply the default action of a signal to PID 1. An ordinary process without a SIGTERM handler dies on SIGTERM, but 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. It's a shell script that installs a SIGTERM handler with trap, prints its PID, and waits. Two Dockerfiles start it, and they differ only in the base image and in the spelling of the last line:
FROM alpine:3.24COPY 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. Since dash has no SIGTERM handler, as PID 1 it ignored the signal. The script never received it. After 10 seconds both died from SIGKILL.
Shell form doesn't 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.24COPY serve.sh /serve.shCMD /serve.sh
Whether the shell stays depends on the shell, its version and the command string. That's exactly why you shouldn't depend on it. Exec form makes the answer the same everywhere: your program is PID 1. Need shell features at start-up? Put them in a script that ends with exec your-program "$@". The shell then 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 doesn't handle SIGTERM, it's PID 1 and ignores it. sleep has no handler, so Dockerfile.nohandler (FROM alpine:3.24 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 what you just ran. 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 can't 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. That keeps the command at PID 1. With such images there are two traps. Setting your own ENTRYPOINT replaces their script and its preparation. And the script may interpret some flags you pass. 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
Try this
Pass your own words to the combined image: docker run --rm lab-greet:both hello there, then the same with --entrypoint date before the image name and -u +%Z after it. Predict each line from the ENTRYPOINT and CMD that docker image inspect printed, then check. If the cleanup already removed the image, rebuild it with the same docker build -q -f Dockerfile.both command.
Takeaway
Write ENTRYPOINT and CMD in exec form, and make sure whatever runs as PID 1 handles SIGTERM, or add --init. lab-sig-shell shows the cost: dash held PID 1, docker stop took 10.2 s, and the container ended with 137 without the shutdown message. In a slow deploy, a stop that always takes the full timeout is the clue to look for.
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?