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 youRUNcreates layers that are baked into the final image. This is typically used for installing packages, creating directories, or downloading files. EachRUNcommand creates a new layer. For efficiency, it’s common to chain commands using&&within a singleRUNinstruction 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 oneCMDinstruction in a Dockerfile. If you list more than one, only the last one will take effect.CMDcan 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
CMDis to provide a default executable that can be overridden when you run a container. If anENTRYPOINTis defined,CMDis treated as its default arguments. If noENTRYPOINTis defined,CMDis the command that gets executed when the container starts.Example:
CMD ["python", "app.py"]When you run
docker run myimage,python app.pywill execute by default. If you rundocker run myimage echo "hello", thenecho "hello"will override the defaultCMD. - Exec form (preferred):
-
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.ENTRYPOINTalso 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 todocker runwill be appended to thisENTRYPOINTcommand. - 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
ENTRYPOINTandCMDare used in exec form,ENTRYPOINTdefines the executable, andCMDdefines the default arguments for that executable.Example:
ENTRYPOINT ["java", "-jar", "app.jar"] CMD ["--help"]Running
docker run myimagewill executejava -jar app.jar --help. Runningdocker run myimage --verbosewill executejava -jar app.jar --verbose. - Exec form (preferred):
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.