Dash0 Raises $110M Series B at $1B Valuation

  • 11 min read

What Is the Difference between RUN and CMD in a Dockerfile?

RUN and CMD look alike because both take a command, but they fire at completely different moments in a container's life. RUN executes while you build the image and commits its result into a new layer. CMD runs nothing at build time; it only records the default command that executes when someone starts a container from the finished image.

That single distinction (build time versus runtime) explains almost every confusing behavior people hit, including why a CMD can vanish without a trace when you pass arguments to docker run, and why RUN instructions keep bloating your image even after you think you've cleaned up. Once that split clicks, the rest of it (layers, the two syntax forms, and how CMD bends around ENTRYPOINT) follows from there.

Build time vs runtime

A Dockerfile describes two separate phases, and RUN and CMD live in different ones.

The build phase turns a Dockerfile into an image. Every RUN instruction executes during this phase, inside a temporary container, and whatever it changes on disk gets saved into the image. By the time the build finishes, all RUN commands have already happened. They will never run again.

The runtime phase starts when you call docker run. This is when CMD matters. The command in CMD is not executed during the build at all; Docker just stores it as metadata in the image. When a container launches, Docker reads that metadata and runs it as the container's main process.

A minimal Dockerfile shows both phases:

dockerfile
123456
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]

When you run docker build, the RUN pip install line executes and the installed packages are written into the image. The CMD ["python", "main.py"] line does nothing yet. Then when you run docker run my-image, Docker skips straight to executing python main.py, because the build work is already baked in.

A useful rule of thumb: if a command needs to leave something behind in the image (an installed package, a compiled binary, a created directory), it belongs in RUN. If a command defines what the running container should do, it belongs in CMD.

How RUN affects layering

Each RUN instruction creates a new read-only layer in the image, capturing the filesystem changes that command produced. This is why you can have many RUN instructions stacked on top of each other, and why build caching works: if nothing above a layer has changed, Docker reuses the cached layer instead of re-executing it.

Layers are additive, and that has a consequence people get burned by. If you install something in one RUN and delete it in a later RUN, the deleted files still occupy space in the earlier layer:

dockerfile
123
# This does NOT shrink the image
RUN apt-get update && apt-get install -y build-essential
RUN apt-get purge -y build-essential

The build-essential packages are gone from the final filesystem view, but they're permanently stored in the layer the first RUN created. To actually keep them out of the image, do the install and cleanup in a single RUN so the deletion happens before the layer is committed:

dockerfile
12345
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& make -C /app \
&& apt-get purge -y build-essential \
&& rm -rf /var/lib/apt/lists/*

CMD does not create a filesystem layer in this sense. It writes a small piece of image configuration. You can have multiple CMD lines in a Dockerfile, but only the last one survives; the earlier ones are silently discarded.

Shell form vs exec form

Both RUN and CMD accept two syntaxes, and the choice changes how the command is executed.

Shell form is a plain string. Docker wraps it in /bin/sh -c, so you get shell features like variable expansion, pipes, and &&:

dockerfile
12
RUN apt-get update && apt-get install -y curl
CMD python main.py

Exec form is a JSON array. Docker runs the executable directly, without a shell:

dockerfile
12
RUN ["/bin/bash", "-c", "apt-get update && apt-get install -y curl"]
CMD ["python", "main.py"]

For RUN, shell form is usually what you want, because build steps lean heavily on shell operators to chain commands together. Note the exec form bypasses the shell entirely, so RUN ["echo", "$HOME"] prints the literal string $HOME rather than expanding the variable.

For CMD, exec form is the better default, and the reason is signal handling. In exec form, your process runs as process ID 1 (PID 1) and receives signals like SIGTERM directly, so docker stop shuts it down cleanly. In shell form, /bin/sh becomes PID 1, and many shells don't forward signals to their child process. The result is a container that ignores docker stop and gets force-killed after the timeout. One subtle catch: the exec form is parsed as JSON, so it requires double quotes. Single quotes will break the parse.

How CMD interacts with ENTRYPOINT

CMD changes behavior the moment you add an ENTRYPOINT. On its own, CMD is the full command Docker runs. But when an ENTRYPOINT is present, CMD stops being the command and becomes the default arguments appended to the ENTRYPOINT.

dockerfile
123
FROM alpine
ENTRYPOINT ["ping"]
CMD ["localhost"]

Running docker run my-image executes ping localhost. The ENTRYPOINT supplies the fixed executable, and CMD supplies the default argument. Override the argument and the ENTRYPOINT stays put:

bash
12
docker run my-image example.com
# runs: ping example.com

Watch the form here, though. When you combine an exec form ENTRYPOINT with CMD, use exec form for the CMD too. If you write CMD in shell form, Docker expands it to /bin/sh -c "your command", and the ENTRYPOINT receives /bin/sh, -c, and your command as three separate trailing arguments instead of the clean parameters you intended. This is one of the most common reasons an ENTRYPOINT-plus-CMD combination behaves strangely, and it's spelled out in the official Dockerfile reference.

Overriding CMD with docker run

Anything you pass after the image name when you start a container with docker run replaces CMD entirely. This is the override behavior that surprises people when their default command seems to disappear.

Given the earlier Python image with CMD ["python", "main.py"]:

bash
1234567891011
# Uses the CMD from the Dockerfile
docker run my-image
# runs: python main.py
# Replaces the CMD completely
docker run my-image python debug.py
# runs: python debug.py
# Also replaces it: you get a shell, not your app
docker run -it my-image bash
# runs: bash

That last example is the gotcha. Running your image with bash tacked on doesn't run your app and then drop you into a shell. It throws away the CMD and runs only bash. This is exactly why dropping into a container to debug it works, and also why people occasionally wonder why their service never started.

RUN has no runtime equivalent to override. Once the image is built, RUN instructions are frozen into layers and there's nothing at runtime to replace.

Common pitfalls

The most common mistake is expecting RUN to do something at startup. People write RUN python main.py, watch it execute during the build, and then can't figure out why the app never starts in the container. RUN only ever fires at build time, so anything that should happen when the container launches belongs in CMD or ENTRYPOINT.

Stacking multiple CMD instructions causes a quieter version of the same confusion. Only the last CMD in the file takes effect, and Docker won't warn you that the earlier ones were thrown away. If your container keeps running the wrong command, check whether a later CMD is shadowing the one you meant to use.

The signal-handling pitfall is the one that bites in production. A CMD in shell form makes /bin/sh your PID 1, and the shell often won't pass SIGTERM down to your application, so graceful shutdown breaks. The usual symptom is docker stop hanging for ten seconds and then force-killing the container. Switch the CMD to exec form and the signal reaches your process directly. For more on the stop and start lifecycle, see how to restart a Docker container.

Final thoughts

The short version: RUN builds the image and CMD runs the container, with CMD quietly turning into default arguments once an ENTRYPOINT enters the picture. Get those two phases straight and most Dockerfile confusion resolves itself.

Once your containers are running, the next question is what's actually happening inside them. A misbehaving container that ignores shutdown signals or restarts in a loop is hard to spot without visibility into its resource usage and logs.

Dash0's infrastructure monitoring tracks container resource usage alongside real-time logs and distributed traces, built on OpenTelemetry so you own your data and avoid lock-in. Start a free trial to see your containers, pods, and clusters in one view. No credit card required.