1. Overview

When creating a Dockerfile, we need to remember the initial command that will be executed when the container starts. While we can override this command when starting the container, it’s essential to define the default behavior.

This is where the ENTRYPOINT directive comes in handy. It defines the command that starts the container’s main runtime process. ENTRYPOINT comes in two forms: shell and exec, and it is also used in the run directives in Docker.

In this article, we’ll discuss the differences between the two forms and see how we can make the command passed to ENTRYPOINT dynamic by incorporating environment variables to modify its behavior at runtime.

2. ENTRYPOINT Shell vs Exec Form

Let’s note that the ENTRYPOINT directive supports two forms, shell and exec.

2.1. Exec Form

In exec form, the ENTRYPOINT directive specifies the executable to be run along with its arguments. This approach executes the command directly using the command itself and any of its subsequent arguments, written in array form within square brackets. A simple example, printing the string “Hello world” would look like this:

FROM alpine

ENTRYPOINT ["echo", "Hello, world!"]

This method ensures that the command is executed as a standalone process within the container.

2.2. Shell Form

In shell form, the ENTRYPOINT executes the given command by starting a new shell process. It takes a list of arguments, where the first one is the shell path to be executed, and any subsequent ones are passed as arguments to the shell. This allows for further processing within the shell before the final command is executed.

The shell form of ENTRYPOINT is convenient in cases where we need shell features like piping, redirection, or, most importantly, variable substitution. However, it can be less efficient due to the overhead of starting a shell process. The same example in shell form would look like this:

FROM alpine

ENTRYPOINT sh -c "echo Hello, world!"

To run the Dockerfile examples, first save the code in a file named “Dockerfile” and then build and run the Docker image:

$ docker build -t custom-echo .
$ docker run --rm custom-echo

The shell form provides more flexibility but comes with the overhead of initiating a shell process.

3. Variable Substitution in ENTRYPOINT

The need for dynamic configuration becomes apparent in more complex Docker setups, such as running a Python application with various parameters. We can choose between different approaches depending on whether we want to configure our application at runtime or build time.

Consider the following example, where we run a Python application with specific parameters in exec form:

FROM python:3.12-slim 

# ... other steps 

ENTRYPOINT ["python", "-m", "myapp", "--log-level", "INFO", "--max-workers", "4"]

It would be very handy if we could set some of the parameters, like the database URL, the log level, and the number of workers, as variables to make the entrypoint command more customizable. However, the exec form of ENTRYPOINT doesn’t provide this feature, since the command runs directly on the container, there’s no related shell process that could handle variable substitution. This approach is straightforward but lacks flexibility, as any changes to parameters would require rebuilding the Docker image.

Hence, the shell form of ENTRYPOINT. 

3.1. Using ENV for Runtime Configuration

In the shell form of ENTRYPOINT, the same example would look like this:

FROM python:3.12-slim

ENV LOG_LEVEL="INFO"
ENV MAX_WORKERS="4"

# ... other steps

ENTRYPOINT sh -c 'exec python -m myapp --log-level "$LOG_LEVEL" --max-workers "$MAX_WORKERS"'

In contrast with the exec form, we can define environment variables with the ENV directive and incorporate them in the ENTRYPOINT command. This approach provides the needed flexibility, but it comes with the overhead of starting a shell process.

3.2. Using ARG for Build-Time Configuration

Alternatively, we can provide the parameters as build-time arguments using the ARG directive:

FROM python:3.12-slim

ARG LOG_LEVEL="INFO"
ARG MAX_WORKERS="4"

# ... other steps

ENTRYPOINT sh -c 'exec python -m myapp --log-level "$LOG_LEVEL" --max-workers "$MAX_WORKERS"'

The arguments are passed when building the Docker image with the –build-arg option:

$ docker build -t python-app --build-arg LOG_LEVEL="DEBUG" MAX_WORKERS="8" . 
$ docker run --rm python-app

Unlike ENV, the ARG solution is dynamic at build time only, not at runtime.

4. Security Considerations

When using the shell form of ENTRYPOINT, there’s a potential security risk due to the involvement of the shell process. Improperly sanitizing the input to the shell could make it vulnerable to injection attacks. For example, consider the following Dockerfile:

FROM alpine
ENTRYPOINT echo "Hello, $GREETING"

If an attacker sets the GREETING environment variable to a malicious value, such as:

The command would expand to:

/bin/sh -c "echo Hello, world; rm -rf / --"

It’s essential to handle shell commands carefully to avoid these risks.

5. Conclusion

In this article, we explored the ENTRYPOINT directive in Docker, which defines the default command to run when a container starts. We examined the two forms of ENTRYPOINT, exec and shell.

We saw that the exec form is efficient for straightforward command execution and offers better performance and security. It’s the recommended way of writing run commands in Docker, however it lacks the flexibility of the underlying shell process.

On the other hand, the shell form executes the command within a shell process, allowing for the use of shell features and processing. Nevertheless, starting the shell adds overhead and could introduce a potential security vulnerability if we don’t handle it carefully.

Ultimately, choosing between the exec and shell forms depends on your specific use case. If you need a straightforward, efficient command execution, the exec form is the better choice. However, the shell form is more suitable if you require flexibility and runtime configurability.