A process receives an ordered argument vector. It does not receive a magical string whose spaces, dollar signs and wildcards always have shell meaning. A shell can parse such syntax before starting a process, but that is a separate interpreter and a separate decision. Keeping those steps distinct prevents broken filenames, lost empty arguments and surprising expansion.
Docker's exec form writes that vector as a JSON array. In our service, ENTRYPOINT ["python", "/app/service.py"] supplies a stable executable prefix and CMD ["--data-dir", "/data"] supplies default arguments. A runtime command replaces the default CMD part in this ordinary exec-form case. The lab accepts those arrays directly; it does not parse every inheritance or command-line override rule.
Compose arrays, never reparse them
Suppose the entrypoint is ["python", "service.py"], the default command is ["--port", "8000"], and the runtime override is ["--port", "8123"]. The final vector has four strings, with 8123 replacing 8000. Appending both CMD and the override would pass two conflicting port choices. Joining and splitting the vector would instead change argument boundaries.
entrypoint = ["python", "service.py"]
default_cmd = ["--greeting", "hello world"]
override = ["--greeting", ""]
argv = entrypoint + (default_cmd if override is None else override)
print(repr(argv))
print(len(argv))The empty greeting is an intentional argument. Likewise, an empty override array intentionally removes default arguments. Use override is None, because if override would incorrectly treat the empty array as absence. Validate the final executable separately: an empty suffix can be legitimate when an entrypoint exists, but an entirely empty vector cannot start a program.
Shell form changes the process story
A shell-form command such as CMD python service.py normally introduces a shell. An exec-form array does not implicitly perform shell expansion: ["echo", "$PORT"] sends literal text unless the program itself interprets it. If a wrapper script is necessary, it should deliberately hand control to the intended long-lived process, commonly with shell exec, and preserve argument boundaries with quoted "$@".
That choice also matters for signal delivery. A wrapper that remains the container's main process may need to forward termination and reap children correctly. We return to lifecycle later; using JSON arrays alone does not make an application handle signals or manage child processes correctly.
Validate at the boundary
This Python API requires lists of strings and rejects NUL bytes, since an operating-system argument cannot contain an embedded NUL. Empty later strings, spaces and wildcard characters remain valid. Return a fresh list to avoid giving the caller an alias to an input array. Validate replaced defaults too: accepting malformed configuration only because a later override masks it makes review inconsistent.
Before building, write the expected vectors for no override, an empty override, an empty entrypoint and a greeting containing spaces. The bug hunts isolate truthiness and shell-like splitting. The resulting function proves composition of supplied arrays. It does not locate an executable, start a process, validate image paths, run Docker or establish a security boundary.