The most surprising thing about RUN, CMD, and ENTRYPOINT is that they all seem to do the same thing – execute a command inside your container – but they are fundamentally different and often misunderstood, leading to confusion about how your container actually starts and behaves.

Let’s see this in action. Imagine we have a simple Dockerfile:

FROM ubuntu:latest

# RUN executes during the image build
RUN echo "Building the image..." > /build_info.txt

# CMD provides default arguments for ENTRYPOINT or the command to run if no ENTRYPOINT is specified
CMD ["echo", "Hello from CMD!"]

# ENTRYPOINT configures a container that will run as an executable
# ENTRYPOINT ["/app/my_script.sh"]

If we build this image:

docker build -t my-cmd-test .

And then run a container from it without any extra arguments:

docker run my-cmd-test

The output will be:

Hello from CMD!

Now, let’s change the Dockerfile slightly, adding an ENTRYPOINT:

FROM ubuntu:latest

RUN echo "Building the image..." > /build_info.txt

ENTRYPOINT ["echo", "Hello from ENTRYPOINT!"]
CMD ["This is a default argument for CMD."]

Build it again:

docker build -t my-entrypoint-test .

And run it:

docker run my-entrypoint-test

The output is:

Hello from ENTRYPOINT! This is a default argument for CMD.

Notice how ENTRYPOINT executed, and CMD was appended as an argument to it. This is key.

The Mental Model

At its core, Docker images are layered filesystems with instructions on how to run an application. RUN, CMD, and ENTRYPOINT are all instructions.

  • RUN: This instruction executes commands during the build time of your Docker image. Think of it as setting up the environment within the image. Anything you RUN creates layers that are baked into the final image. This is typically used for installing packages, creating directories, or downloading files. Each RUN command creates a new layer. For efficiency, it’s common to chain commands using && within a single RUN instruction to minimize the number of layers.

    Example:

    RUN apt-get update && apt-get install -y --no-install-recommends \
        nginx \
        && rm -rf /var/lib/apt/lists/*
    

    This installs nginx, updates package lists, and then cleans up the apt cache, all in one layer.

  • CMD: This instruction provides the default command and/or default arguments for an executing container. There can only be one CMD instruction in a Dockerfile. If you list more than one, only the last one will take effect. CMD can be specified in two forms:

    • Exec form (preferred): CMD ["executable","param1","param2"]. This form is the recommended way as it runs your command directly without a shell.
    • Shell form: CMD command param1 param2. This form runs the command inside a shell (e.g., /bin/sh -c "command param1 param2").

    The primary purpose of CMD is to provide a default executable that can be overridden when you run a container. If an ENTRYPOINT is defined, CMD is treated as its default arguments. If no ENTRYPOINT is defined, CMD is the command that gets executed when the container starts.

    Example:

    CMD ["python", "app.py"]
    

    When you run docker run myimage, python app.py will execute by default. If you run docker run myimage echo "hello", then echo "hello" will override the default CMD.

  • ENTRYPOINT: This instruction configures a container that will run as an executable. It specifies the command that will always be executed when the container starts. ENTRYPOINT also has two forms:

    • Exec form (preferred): ENTRYPOINT ["executable", "param1", "param2"]. This is the more common and recommended form. The parameters specified here become the base command. Any arguments passed to docker run will be appended to this ENTRYPOINT command.
    • Shell form: ENTRYPOINT command param1 param2. This form runs the command through a shell. This is less common and can lead to unexpected behavior with signals and arguments.

    When both ENTRYPOINT and CMD are used in exec form, ENTRYPOINT defines the executable, and CMD defines the default arguments for that executable.

    Example:

    ENTRYPOINT ["java", "-jar", "app.jar"]
    CMD ["--help"]
    

    Running docker run myimage will execute java -jar app.jar --help. Running docker run myimage --verbose will execute java -jar app.jar --verbose.

The Nuance

The critical interaction is between ENTRYPOINT and CMD. When both are present in exec form, ENTRYPOINT specifies the executable, and CMD provides default arguments that are appended to the ENTRYPOINT. If you provide arguments to docker run, they replace the CMD arguments entirely. This is why ENTRYPOINT is often used to create containers that act like executables, where the docker run arguments are simply parameters passed to that executable.

Consider a container designed to run a specific script. You might use ENTRYPOINT ["/app/run.sh"]. If you want to provide a default mode, you add CMD ["--default-mode"]. Now, docker run myimage runs /app/run.sh --default-mode. If you run docker run myimage --production, the container executes /app/run.sh --production, because the arguments provided on the docker run command line entirely override the CMD.

If you want to override the ENTRYPOINT itself, you can use the --entrypoint flag with docker run. For example, docker run --entrypoint /bin/bash myimage -c "ls -l".

The most common pitfall is using the shell form of ENTRYPOINT when you intend for CMD arguments to be appended. The shell form of ENTRYPOINT effectively creates a shell process that then executes your command, and it doesn’t handle the passing of arguments from docker run or CMD in the same way.

The next concept you’ll likely grapple with is how Docker handles signals (like SIGTERM for graceful shutdowns) with different ENTRYPOINT/CMD configurations, especially when using the shell form or wrapping your entrypoint in a script.

Want structured learning?

Take the full Docker course →