| |

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:

FormSyntaxBehavior
Exec formCMD ["executable", "arg1"]Runs directly as PID 1
Shell formCMD executable arg1Runs as /bin/sh -c "executable arg1"
Default argumentsCMD ["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:

FormSyntaxSignal handling
Exec formENTRYPOINT ["executable", "arg1"]Runs as PID 1, receives signals
Shell formENTRYPOINT executable arg1Runs 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 commandExecuted command
docker run my-imagepython /app/my_script.py --default-arg
docker run my-image --user-argpython /app/my_script.py --user-arg
docker run my-image --helppython /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

InstructionWhen It RunsCreates LayerOverridable
FROMBuildYesNo
RUNBuildYesNo
CMDContainer startMetadata onlyYes
ENTRYPOINTContainer startMetadata onlyOnly with --entrypoint

CMD Forms

FormSyntaxPID 1
ExecCMD ["node", "server.js"]node
ShellCMD node server.js/bin/sh
Default argsCMD ["--port", "8080"]ENTRYPOINT

ENTRYPOINT Forms

FormSyntaxPID 1
ExecENTRYPOINT ["python", "app.py"]python
ShellENTRYPOINT python app.py/bin/sh

CMD and ENTRYPOINT Interaction

Configurationdocker run imgdocker run img arg
CMD onlyCMDarg
ENTRYPOINT onlyENTRYPOINTENTRYPOINT arg
BothENTRYPOINT + CMDENTRYPOINT + arg

Signal Handling

FormReceives SIGTERM
Exec formYes
Shell formNo (shell receives it)
Shell with execYes

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

PitfallWhy It HappensFix
Container exits immediatelyNo CMD or ENTRYPOINTAdd a command
Signals not receivedShell form usedUse exec form
Image too largeSeparate install and cleanupCombine in one RUN
CMD ignoredENTRYPOINT present and arguments passedUnderstand the interaction
--help not workingENTRYPOINT captures itUse --entrypoint flag
Build cache staleCopying all files before installing depsCopy lockfile first
Secrets in imageCopied with COPYUse --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

ItemValue
FROMSets the base image
RUNExecutes build-time commands, creates layers
CMDDefault command or arguments, overridable
ENTRYPOINTFixed executable, arguments appended
Exec form["executable", "arg"], runs as PID 1
Shell formexecutable arg, runs under /bin/sh -c
CMD + ENTRYPOINTCMD provides default args to ENTRYPOINT
docker run img argarg replaces CMD, appended to ENTRYPOINT
Signal handlingExec form required for PID 1 to receive signals
Multiple CMDOnly the last takes effect

Key takeaways:

  • FROM sets 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.
  • RUN executes during the build and creates a layer. Combine commands with && and clean up in the same layer to avoid leaving caches in the image.
  • CMD sets the default command and is overridable. Any command passed to docker run replaces the CMD. Only the last CMD in a Dockerfile takes effect.
  • ENTRYPOINT sets the fixed executable. Arguments passed to docker run are appended to the ENTRYPOINT rather than replacing it. Use --entrypoint to override for debugging.
  • CMD and ENTRYPOINT work together. ENTRYPOINT defines the executable, and CMD provides 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/sh as PID 1, which does not forward signals.
  • Use exec in 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!