Properly use CMD and ENTRYPOINT

Properly use CMD and ENTRYPOINT

This post will demonstrate:

  1. How ENTRYPOINT and CMD work together and their differences.
  2. How to redirect the runtime execution flow from ENTRYPOINT to the CMD.

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 in ENTRYPOINT. Note that we said: “default arguments” which we’ll explain what does that mean in a bit.

ℹ️
You can still use 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
ℹ️
This method overrides the 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:

“Dockerfile example”

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 the CMD instruction). It preserves the exact arguments that were passed during container runtime. Also, you still can override the CMD by specifying args on the docker run command. Finally, note that you cannot place any commands in the ‘docker-entrypoint.sh’ file after the exec “$@” line.