Docker 13 🐳 Writing your First Dockerfile: FROM, RUN, CMD, and ENTRYPOINT Mechanics
A Dockerfile is a text file with instructions that the image builder reads in order. Each instruction creates a layer, and the final layer stack becomes the image. The three instructions that most define an image’s behavior are FROM, which sets the base, RUN, which executes commands during the build, and CMD and ENTRYPOINT, which determine what runs when a container starts. These four form the core of every Dockerfile, and understanding how they interact is the difference between an image that works and one that shuts down uncleanly, ignores configuration, or cannot be overridden.
The build-time and runtime split is the key distinction. FROM and RUN execute while the image is being built. CMD and ENTRYPOINT do not execute during the build; they record the default command and arguments that the container runtime will use. The RUN instruction creates layers that contain the result of the command. The CMD and ENTRYPOINT instructions create layers too, but those layers are metadata with zero bytes of filesystem change. They store a configuration that the container’s process supervisor reads at startup.
This chapter covers the FROM base image selection, the RUN instruction in its shell and exec forms, the CMD instruction and how it can be overridden, the ENTRYPOINT instruction and how it receives arguments, the relationship between CMD and ENTRYPOINT, and the patterns that make the container start correctly and handle signals properly.
Key point: FROM sets the base image. RUN executes build-time commands and creates layers. CMD provides the default command or arguments for the container, and it is overridden by any command passed to docker run. ENTRYPOINT defines the executable that always runs, and its exec form ensures the process becomes PID 1 and receives signals. Use ENTRYPOINT for the main executable and CMD for default arguments that users can override.
Why these four instructions matter
The base-image problem. Every image starts from another image. FROM is the instruction that declares the parent. The choice of base image determines the available tools, the package manager, the libc implementation, and the image size. A base image that is too large bloats every image built on it; one that is too minimal may lack tools the application needs.
The build-time problem. Dependencies must be installed, source code must be compiled, and configuration must be written before the image is ready. RUN is the instruction that executes these commands. Each RUN creates a layer, and the order of layers determines build cache behavior and final image size.
The startup-command problem. A container needs a command to run when it starts. CMD provides a default, and it is the instruction that most Dockerfiles use for the application entry point. But CMD is overridable: if the user passes a command to docker run, the CMD is ignored.
The fixed-executable problem. Some images should always run a specific executable. A database image should always run the database server, regardless of what arguments the user passes. ENTRYPOINT is the instruction for this: it defines the executable that always runs, and the user’s arguments become parameters to it.
The signal-handling problem. The process that runs as PID 1 in a container is responsible for handling signals like SIGTERM. If the container’s main process is a shell that spawned the application as a child, the shell receives the signal and may not forward it, so the application is killed abruptly. The exec form of ENTRYPOINT and CMD runs the executable directly as PID 1, which allows it to handle signals correctly.
a. FROM: choosing the base image
The FROM instruction sets the base image for subsequent instructions. It must be the first instruction in a Dockerfile (with the exception of parser directives and comments).
FROM node:20-alpine
The image reference follows the same format as docker pull: registry, namespace, repository, and tag. Docker Hub is the default registry, and the library namespace is the default for official images.
The base image determines what is available inside the container. alpine is a minimal Linux distribution under 6 MB, which makes images small but uses musl libc instead of glibc. debian and ubuntu are larger but more compatible. scratch is an empty image, used for statically compiled binaries that need no runtime.
Pinning the base image is a security practice. A tag like node:20-alpine can point to a different image if the publisher updates the tag. Pinning to a digest guarantees the same image every time:
FROM node:20-alpine@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
The FROM instruction also supports build arguments, which let the caller choose the base image version at build time:
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
b. RUN: executing build-time commands
The RUN instruction executes a command during the build and commits the result as a new layer. It has two forms: shell and exec.
The shell form passes the command to a shell:
RUN apt-get update && apt-get install -y curl
The shell form is the common case for RUN. It supports shell features: piping, redirection, variable expansion, and command chaining with &&. The && operator is important because it ensures that if the first command fails, the second does not run. Chaining commands with && in a single RUN also reduces the number of layers.
The exec form runs the command directly without a shell:
RUN ["apt-get", "update"]
The exec form does not perform shell expansion, so $HOME and $PATH are not substituted. It is rarely used for RUN because most build commands need shell features.
A critical pattern is installing packages and cleaning up in the same layer. If installation and cleanup are separate RUN instructions, the package cache is in a lower layer and remains in the image even after deletion:
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
The RUN instruction also supports mount options that temporarily provide files or secrets without persisting them in a layer:
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
pip install -r /tmp/requirements.txt
The bind mount makes the file available for the duration of the RUN instruction and does not add it to the image or the cache.
c. CMD: the default command
The CMD instruction sets the default command that runs when a container starts from the image. It does not execute during the build; it records a configuration.
There are three forms of CMD:
| Form | Syntax | Behavior |
|---|---|---|
| Exec form | CMD ["executable", "arg1"] | Runs directly as PID 1 |
| Shell form | CMD executable arg1 | Runs as /bin/sh -c "executable arg1" |
| Default arguments | CMD ["arg1", "arg2"] | Used as arguments to ENTRYPOINT |
The exec form is recommended because it runs the executable as PID 1 and allows it to receive signals:
CMD ["node", "server.js"]
The shell form runs the command through a shell, which means the shell is PID 1 and the executable is a child. Signals like SIGTERM go to the shell, not the application, which can cause unclean shutdowns:
CMD node server.js
The third form, arguments without an executable, is used when ENTRYPOINT is present. The CMD provides default arguments to the ENTRYPOINT:
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
A CMD is overridden by any command passed to docker run:
docker run my-image --help
This ignores the CMD and runs --help instead.
Only the last CMD in a Dockerfile takes effect. Multiple CMD instructions are a mistake; the earlier ones are ignored.
d. ENTRYPOINT: the fixed executable
The ENTRYPOINT instruction defines the executable that always runs when the container starts. Unlike CMD, it is not overridden by command-line arguments; instead, the arguments are appended to it.
ENTRYPOINT ["node", "server.js"]
Running docker run my-image --port 3000 executes node server.js --port 3000. The --port 3000 is appended to the entry point.
ENTRYPOINT has two forms:
| Form | Syntax | Signal handling |
|---|---|---|
| Exec form | ENTRYPOINT ["executable", "arg1"] | Runs as PID 1, receives signals |
| Shell form | ENTRYPOINT executable arg1 | Runs under /bin/sh -c, does not receive signals |
The exec form is strongly preferred. The shell form prevents CMD and command-line arguments from being appended and runs the executable as a child of the shell, which breaks signal handling.
The docker run --entrypoint flag overrides the ENTRYPOINT for a single run:
docker run --entrypoint /bin/bash my-image
This is useful for debugging an image that has a restrictive entry point.
e. CMD and ENTRYPOINT together
The CMD and ENTRYPOINT instructions are designed to work together. ENTRYPOINT defines the executable, and CMD provides default arguments that the user can override.
ENTRYPOINT ["python", "/app/my_script.py"]
CMD ["--default-arg"]
docker run command | Executed command |
|---|---|
docker run my-image | python /app/my_script.py --default-arg |
docker run my-image --user-arg | python /app/my_script.py --user-arg |
docker run my-image --help | python /app/my_script.py --help |
The CMD is only used when no arguments are passed. When arguments are passed, they replace the CMD and are appended to the ENTRYPOINT.
This pattern is common for CLI tools packaged as containers. The ENTRYPOINT is the tool, and the CMD is the default subcommand or arguments.
If ENTRYPOINT is present but CMD is not, the arguments passed to docker run are appended to the ENTRYPOINT. If neither is present, the container exits immediately because there is no command to run.
f. Signal handling and PID 1
The process that runs as PID 1 in a container has special responsibilities. It receives signals from the Docker daemon, and it is responsible for reaping orphaned child processes. When docker stop is called, the daemon sends SIGTERM to PID 1, waits for a grace period, then sends SIGKILL.
The exec form of ENTRYPOINT and CMD runs the executable directly as PID 1. The shell form runs a shell as PID 1, and the executable as a child. Most shells do not forward signals to their children, so the executable does not receive SIGTERM and is killed by SIGKILL after the timeout.
# Good: node is PID 1 and receives SIGTERM
ENTRYPOINT ["node", "server.js"]
# Bad: /bin/sh is PID 1 and node is a child
ENTRYPOINT node server.js
The bad example may produce a clean shutdown if the shell is BusyBox ash (Alpine’s shell), because ash replaces itself with the command when it is the last command in a script. But this is shell-specific and not portable. The exec form is always correct.
For cases where a shell is unavoidable, the exec shell built-in replaces the shell with the command, so the command becomes PID 1:
CMD exec node server.js
This is the shell form with exec, which runs node server.js as PID 1 through the shell’s exec mechanism. It is a workaround when the command needs shell features but the process must be PID 1.
For images that run multiple processes or need signal forwarding, an init process like tini is used as PID 1. The --init flag on docker run inserts tini automatically, and it forwards signals and reaps orphaned processes.
Complete Example Session
# ============================================
# PART 1: MINIMAL DOCKERFILE
# ============================================
FROM alpine:3.19
CMD ["echo", "hello"]
# ============================================
# PART 2: WITH RUN
# ============================================
FROM alpine:3.19
RUN apk add --no-cache curl
CMD ["curl", "--version"]
# ============================================
# PART 3: COMBINED RUN FOR FEWER LAYERS
# ============================================
FROM debian:12-slim
RUN apt-get update && \
apt-get install -y curl jq && \
rm -rf /var/lib/apt/lists/*
CMD ["bash"]
# ============================================
# PART 4: ENTRYPOINT AND CMD TOGETHER
# ============================================
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
# ============================================
# PART 5: SHELL FORM VS EXEC FORM
# ============================================
FROM node:20-alpine
# Exec form: node is PID 1
CMD ["node", "server.js"]
# Shell form: /bin/sh is PID 1
# CMD node server.js
# ============================================
# PART 6: ARG FOR BASE IMAGE
# ============================================
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
CMD ["node", "--version"]
# ============================================
# PART 7: MOUNT FOR BUILD SECRETS
# ============================================
FROM python:3.12-slim
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
pip install -r /tmp/requirements.txt
COPY . .
CMD ["python", "app.py"]
# ============================================
# PART 8: SIGNAL HANDLING WITH EXEC
# ============================================
FROM alpine:3.19
RUN apk add --no-cache nginx
CMD exec nginx -g 'daemon off;'
# ============================================
# PART 9: WORKDIR AND COPY
# ============================================
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
# ============================================
# PART 10: COMPLETE MULTI-STAGE
# ============================================
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app
FROM alpine:3.19
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
These ten parts cover a minimal Dockerfile, adding RUN, combining commands to reduce layers, ENTRYPOINT and CMD together, shell versus exec form, ARG for the base image, mount for build secrets, signal handling with exec, WORKDIR and COPY, and a complete multi-stage build.
Quick Reference
The Four Instructions
| Instruction | When It Runs | Creates Layer | Overridable |
|---|---|---|---|
FROM | Build | Yes | No |
RUN | Build | Yes | No |
CMD | Container start | Metadata only | Yes |
ENTRYPOINT | Container start | Metadata only | Only with --entrypoint |
CMD Forms
| Form | Syntax | PID 1 |
|---|---|---|
| Exec | CMD ["node", "server.js"] | node |
| Shell | CMD node server.js | /bin/sh |
| Default args | CMD ["--port", "8080"] | ENTRYPOINT |
ENTRYPOINT Forms
| Form | Syntax | PID 1 |
|---|---|---|
| Exec | ENTRYPOINT ["python", "app.py"] | python |
| Shell | ENTRYPOINT python app.py | /bin/sh |
CMD and ENTRYPOINT Interaction
| Configuration | docker run img | docker run img arg |
|---|---|---|
CMD only | CMD | arg |
ENTRYPOINT only | ENTRYPOINT | ENTRYPOINT arg |
| Both | ENTRYPOINT + CMD | ENTRYPOINT + arg |
Signal Handling
| Form | Receives SIGTERM |
|---|---|
| Exec form | Yes |
| Shell form | No (shell receives it) |
Shell with exec | Yes |
Best Practices
✅ Do This:
# Pin the base image
FROM node:20-alpine@sha256:...
# Combine commands to reduce layers
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Use exec form for CMD and ENTRYPOINT
CMD ["node", "server.js"]
# Use ENTRYPOINT for the executable
ENTRYPOINT ["/app"]
CMD ["--port", "8080"]
# Use exec in shell scripts
CMD exec nginx -g 'daemon off;'
❌ Don’t Do This:
# Unpinned base image
FROM node:latest # ❌
# Separate install and cleanup
RUN apt-get update # ❌
RUN apt-get install -y curl # ❌
RUN rm -rf /var/lib/apt/lists/* # ❌ cache already in lower layer
# Shell form for CMD
CMD node server.js # ❌ /bin/sh is PID 1
# Multiple CMD
CMD ["echo", "first"] # ❌ ignored
CMD ["echo", "second"] # ❌ only this one takes effect
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Container exits immediately | No CMD or ENTRYPOINT | Add a command |
| Signals not received | Shell form used | Use exec form |
| Image too large | Separate install and cleanup | Combine in one RUN |
CMD ignored | ENTRYPOINT present and arguments passed | Understand the interaction |
--help not working | ENTRYPOINT captures it | Use --entrypoint flag |
| Build cache stale | Copying all files before installing deps | Copy lockfile first |
| Secrets in image | Copied with COPY | Use --mount=type=secret |
Real-World Examples
1. Node.js Application
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
2. Python Application
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
3. CLI Tool
FROM alpine:3.19
COPY mytool /usr/local/bin/mytool
ENTRYPOINT ["mytool"]
CMD ["--help"]
4. Nginx with Exec
FROM nginx:1.25-alpine
COPY nginx.conf /etc/nginx/nginx.conf
CMD exec nginx -g 'daemon off;'
5. Multi-Stage Go Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app
FROM scratch
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
6. Base Image with ARG
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
7. Build Secret
RUN --mount=type=secret,id=token \
TOKEN=$(cat /run/secrets/token) && \
./configure --token=$TOKEN
8. Debug with Entrypoint Override
docker run --entrypoint /bin/sh my-image
9. Signals with tini
docker run --init my-image
10. Healthcheck
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost/ || exit 1
Visual
Build-Time vs Runtime
┌──────────────────────────────────────────────────────────────┐
│ BUILD TIME │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ FROM node:20-alpine → base layer │ │
│ │ RUN npm install → dependencies layer │ │
│ │ COPY . . → source layer │ │
│ │ CMD ["node", "app.js"] → metadata layer (0 bytes) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ RUNTIME │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ docker run my-image │ │
│ │ └── reads CMD/ENTRYPOINT metadata │ │
│ │ └── starts the process as PID 1 │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
CMD and ENTRYPOINT Interaction
┌──────────────────────────────────────────────────────────────┐
│ ENTRYPOINT ["python", "app.py"] │
│ CMD ["--port", "8080"] │
│ │
│ docker run my-image │
│ └── python app.py --port 8080 │
│ │
│ docker run my-image --debug │
│ └── python app.py --debug │
│ │
│ docker run --entrypoint /bin/bash my-image │
│ └── /bin/bash │
└──────────────────────────────────────────────────────────────┘
Signal Handling
┌──────────────────────────────────────────────────────────────┐
│ EXEC FORM: │
│ ENTRYPOINT ["node", "server.js"] │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ PID 1: node │ │
│ │ ← SIGTERM received and handled │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ SHELL FORM: │
│ ENTRYPOINT node server.js │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ PID 1: /bin/sh │ │
│ │ ← SIGTERM received (not forwarded) │ │
│ │ └── child: node (does not receive SIGTERM) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Layer Creation
┌──────────────────────────────────────────────────────────────┐
│ Layer 4: CMD ["node", "app.js"] (metadata, 0 bytes) │
│ Layer 3: COPY . . (source files) │
│ Layer 2: RUN npm install (node_modules) │
│ Layer 1: FROM node:20-alpine (base image) │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
FROM | Sets the base image |
RUN | Executes build-time commands, creates layers |
CMD | Default command or arguments, overridable |
ENTRYPOINT | Fixed executable, arguments appended |
| Exec form | ["executable", "arg"], runs as PID 1 |
| Shell form | executable arg, runs under /bin/sh -c |
| CMD + ENTRYPOINT | CMD provides default args to ENTRYPOINT |
docker run img arg | arg replaces CMD, appended to ENTRYPOINT |
| Signal handling | Exec form required for PID 1 to receive signals |
| Multiple CMD | Only the last takes effect |
Key takeaways:
FROMsets the base image and must be first. The base determines the available tools, the package manager, and the image size. Pin the tag or digest for reproducibility.RUNexecutes during the build and creates a layer. Combine commands with&&and clean up in the same layer to avoid leaving caches in the image.CMDsets the default command and is overridable. Any command passed todocker runreplaces theCMD. Only the lastCMDin a Dockerfile takes effect.ENTRYPOINTsets the fixed executable. Arguments passed todocker runare appended to theENTRYPOINTrather than replacing it. Use--entrypointto override for debugging.CMDandENTRYPOINTwork together.ENTRYPOINTdefines the executable, andCMDprovides default arguments that the user can override.- The exec form is required for signal handling.
ENTRYPOINT ["node", "server.js"]runs node as PID 1, which receives SIGTERM. The shell form runs/bin/shas PID 1, which does not forward signals. - Use
execin shell scripts when PID 1 must be the application.CMD exec nginx -g 'daemon off;'replaces the shell with nginx, so nginx becomes PID 1. - Multi-stage builds reduce the final image size. The builder stage compiles the application, and the final stage copies only the binary, leaving the build tools behind.
Remember: A Dockerfile is a sequence of instructions that build an image layer by layer. FROM establishes the base, RUN executes build-time commands, and CMD and ENTRYPOINT define what runs at startup. The distinction between build time and runtime is the key: FROM and RUN produce the image, while CMD and ENTRYPOINT produce the container’s process. The most common mistakes are using the shell form when the exec form is needed, leaving package caches in the image, and misunderstanding how CMD and ENTRYPOINT interact. Understanding these four instructions is the foundation for writing Dockerfiles that build efficiently, start correctly, and shut down cleanly.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!