Properly use CMD and ENTRYPOINT
This post will demonstrate:
- How
ENTRYPOINT
andCMD
work together and their differences. - How to redirect the runtime execution flow from
ENTRYPOINT
to theCMD
.
The way ENTRYPOINT
and CMD
work together
In most cases, the CMD
and ENTRYPOINT
instructions can be used interchangeably if only one of them is being used. However, each instruction offers additional features that can help in control application execution. Before moving forward, let’s quickly review what each instruction does:
-
ENTRYPOINT
sets the “main command” or the starting point for the container. It’s the default action the container takes when you run it. -
CMD
can be used to provide default arguments to the command specified inENTRYPOINT
. Note that we said: “default arguments” which we’ll explain what does that mean in a bit.
CMD
alone without ENTRYPOINT
to act as the entry point to your application.
This Dockerfile explains how echo
, the main command, accepts string argument Hello world
:
FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["Hello world"]
Output:
Hello world
Overriding CMD
As mentioned above, CMD
in Dockerfile provides default arguments for the container. To override these default arguments, pass the argument(s) after the image name at run time:
docker run test 'another hello world'
Output:
another hello world
CMD
, whether it’s used in combination with the ENTRYPOINT
instruction or alone.
Overriding ENTRYPOINT
Given this Dockerfile:
FROM ubuntu
ENTRYPOINT ["echo", "Hello world"]
When you have a Dockerfile with only an ENTRYPOINT
(no CMD
), you need to use the --entrypoint
flag to override the entry-point command as the following:
# docker run --entrypoint <command> <image>
docker run --entrypoint 'printenv' test
Output:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=7ffd59696373
HOME=/root
If you try to supply a command at runtime without specifying the --entrypoint
flag, Docker will treat the that command as additional arguments to the command specified in the ENTRYPOINT
:
docker run test 'printenv'
Output:
Hello world printenv
This is similar to an entry-point in Dockerfile like this: ENTRYPOINT ["echo", "Hello world", "printenv"]
Handing over execution flow from ENTRYPOINT
to CMD
Consider the following Python Flask Docker file:
FROM python:latest
# ...
# RUN >>> install Python packages & configs ...
# COPY >>> add files and executables
# ...
ENTRYPOINT ["uvicorn"]
CMD ["main:app", "--host", "0.0.0.0"]
The uvicorn command will be executed when on container start, and the CMD
will provide the necessary arguments for the uvicorn server.
In some cases, we need a way to include runtime configurations that our Flask app expects to be available in the run environment prior to executing the main application specified in the entry-point (e.g. uvicorn
). These configurations could be starting a service, exporting environment variables, running a database migration script, or simply editing some files.
This type of commands (runtime commands) cannot be included in RUN
stages, and it is an anti-pattern and honestly it’s quite ugly to cram a lot of shell commands into the ENTRYPOINT
and/or CMD
sections.
Container runtime configuration
For runtime specific configurations, you can put various commands in a shell script (conventionally named ‘docker-entrypoint.sh’ or ’entrypoint.sh’) and execute it using the ENTRYPOINT
instruction then run the main app using CMD
.
But since ENTRYPOINT
provides runtime execution, the trick is how to return the execution flow back from ENTRYPOINT
to CMD
to run the main application command.
To do so, simply add an exec "@$"
statement at the very end of the shell script that is being executed by the ENTRYPOINT
(i.e. ‘docker-entrypoint.sh’) file.
After adding all configuration scripts to ‘docker-entrypoint.sh,’ we will modify the Dockerfile as follows:
FROM python:latest
# ...
# RUN >>> install Python packages & configs ...
# COPY >> add our app
# ....
# Copy the init script file to a directory in the PATH
# You might need to `chmod +x` it too
COPY docker-entrypoint.sh /usr/local/bin
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
To visualize the process:
When we run the container, Docker will execute the ENTRYPOINT
, which contains the “docker-entrypoint.sh” script. Then, the exec "$@"
command in the “docker-entrypoint.sh” script will, in a sense, return control to the CMD
.
To clarify, the exec part won’t transfer execution flow; it just expands the arguments specified in the CMD
instruction in a new process.
Let’s break down what the exec "@$"
statement does:
exec
is a Linux command used to replace the current process with a new process. In this case, it ensures that"$@"
becomes the main process running in the container."$@"
expands to all the command-line arguments passed to the container when it starts (e.g. expanding the content of theCMD
instruction). It preserves the exact arguments that were passed during container runtime. Also, you still can override theCMD
by specifying args on thedocker run
command. Finally, note that you cannot place any commands in the ‘docker-entrypoint.sh’ file after the exec “$@” line.