Overview

The Dockerfile defines how Docker builds an image. This cheat sheet covers all 17 primary instructions, advanced patterns like multi-stage builds and cache mounts, best practices for security and performance, and optimization techniques. All syntax is current for Docker 27.

Core instructions

FROM

Instruction Purpose Example
FROM <image>:<tag> Set the base image (must be the first line, except comments/ARG) FROM ubuntu:24.04
FROM <image>:<tag> AS <stage> Set a named stage in a multi-stage build FROM golang:1.22 AS builder
FROM scratch Start from an empty base (for static binaries only) FROM scratch

Notes:
– You can use multiple FROM statements to create multiple build stages.
– Each FROM resets the filesystem; previous layers are discarded unless explicitly copied.
– scratch contains only what you explicitly add; smallest possible image.

RUN

Instruction Purpose Example
RUN <command> Execute a command during the build (shell form) RUN apt-get update && apt-get install -y curl
RUN ["executable", "param1", "param2"] Execute a command in exec form (no shell) RUN ["python3", "-m", "pip", "install", "django"]

Best practices:
– Use exec form when possible to avoid shell interpretation issues.
– Chain commands with && to keep layers small: RUN apt-get update && apt-get install && apt-get clean
– Clean package manager caches: rm -rf /var/lib/apt/lists/* (apt), apk del .build-deps (apk)
– Use --no-cache flags in package managers to reduce layer size.

COPY

Instruction Purpose Example
COPY <source> <dest> Copy files/directories from build context to image COPY app.py /app/app.py
COPY --chown=<user>:<group> <source> <dest> Copy and set ownership COPY --chown=appuser:appuser . /app/
COPY --chmod=<mode> <source> <dest> Copy and set file permissions COPY --chmod=755 script.sh /app/script.sh

Best practices:
– COPY is preferred over ADD for simple file copying.
– Use .dockerignore to exclude build artifacts, tests, .git.
– Copy only what you need; split into multiple COPY lines for cache efficiency.

ADD

Instruction Purpose Example
ADD <source> <dest> Copy files/directories, with extra features (tar extraction, URL fetching) ADD archive.tar.gz /app/
ADD --chown=<user>:<group> <source> <dest> Add with ownership ADD --chown=appuser:appuser file.txt /app/
ADD --chmod=<mode> <source> <dest> Add with file permissions ADD --chmod=644 config.txt /etc/config.txt

Notes:
– If source is a tar/zip/gzip archive, it is extracted automatically.
– If source is a URL, it is downloaded (no extraction).
– COPY is usually clearer; use ADD only for tar extraction or URL fetching.

WORKDIR

Instruction Purpose Example
WORKDIR <path> Set the working directory for all subsequent commands (RUN, CMD, ENTRYPOINT, COPY, ADD) WORKDIR /app

Notes:
– If the directory doesn’t exist, it is created.
– Use absolute paths to avoid confusion.
– Multiple WORKDIR instructions are cumulative: WORKDIR /app then WORKDIR src results in /app/src.

ENV

Instruction Purpose Example
ENV <key>=<value> Set an environment variable (available in the running container) ENV NODE_ENV=production
ENV <key> <value> Alternative syntax (deprecated, avoid) ENV DEBUG true

Notes:
– Persists into containers; set once during build, available at runtime.
– Use for application-level configuration (database URLs, API keys for testing).
– Do not use ENV for secrets; use Docker secrets or pass at runtime.

ARG

Instruction Purpose Example
ARG <name>=<default> Define a build-time variable (not available in running container) ARG PYTHON_VERSION=3.12
ARG <name> Define a variable with no default (must be provided with --build-arg) ARG BUILD_NUMBER

Usage in build:

docker build --build-arg PYTHON_VERSION=3.11 -t myapp:1.0 .

Notes:
– ARG variables are used during build only; they do not appear in the final image.
– To pass an ARG value to the running container, re-assign it with ENV: ENV PYTHON_VERSION=$PYTHON_VERSION

EXPOSE

Instruction Purpose Example
EXPOSE <port> Document that a container listens on a port (does not publish it) EXPOSE 8080
EXPOSE <port>/udp Document a UDP port EXPOSE 53/udp
EXPOSE <port1> <port2> Document multiple ports EXPOSE 8080 8443

Notes:
– EXPOSE is metadata only; it does not actually publish the port.
– To publish, use docker run -p <host>:<container> or Docker Compose ports:.
– Useful for documentation and for docker run -P (random high-numbered ports).

USER

Instruction Purpose Example
USER <user> Set the user (or UID) for subsequent RUN, CMD, ENTRYPOINT, COPY, ADD USER appuser
USER <uid>:<gid> Use UID and GID directly USER 1000:1000

Best practice for security:

RUN groupadd -r appgroup && useradd -r -g appgroup appuser
USER appuser

Notes:
– Running as root in a container is a security risk.
– Use a dedicated non-root user to limit blast radius on container escape.
– Must be created before the USER instruction (use RUN).

HEALTHCHECK

Instruction Purpose Example
HEALTHCHECK --interval=<seconds> --timeout=<seconds> --start-period=<seconds> --retries=<count> CMD <command> Define a health check command HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD curl -f http://localhost:8000/health || exit 1
HEALTHCHECK NONE Disable a health check inherited from the base image HEALTHCHECK NONE

Options:
– --interval: How often to run the check (default: 30s)
– --timeout: How long to wait for the check (default: 30s)
– --start-period: Grace period before first check (default: 0s)
– --retries: Consecutive failures before marking unhealthy (default: 3)

Notes:
– Exit code 0 = healthy, 1 = unhealthy, 2 = reserved.
– Docker Compose and orchestrators use health status for restarting/replacing containers.

ENTRYPOINT

Instruction Purpose Example
ENTRYPOINT ["executable", "param1"] Define the container’s main process (exec form, preferred) ENTRYPOINT ["python3", "-u", "app.py"]
ENTRYPOINT command param1 Define the main process (shell form, avoid) ENTRYPOINT python3 -u app.py

Exec form vs. shell form:
– Exec form: Runs the executable directly; signals (SIGTERM) are delivered correctly; no shell interpretation.
– Shell form: Wraps command in /bin/sh -c; shell processes signals, not your app; can cause zombie processes.

With CMD:

ENTRYPOINT ["python3"]
CMD ["app.py"]
# docker run image arg → python3 app.py arg

Notes:
– ENTRYPOINT defines what cannot be easily overridden.
– Always use exec form for ENTRYPOINT.

CMD

Instruction Purpose Example
CMD ["executable", "param1", "param2"] Provide default arguments or command (exec form, preferred) CMD ["python3", "app.py"]
CMD command param1 Provide default arguments (shell form, avoid) CMD python3 app.py
CMD ["param1", "param2"] Provide default args to ENTRYPOINT CMD ["production"]

Notes:
– If no ENTRYPOINT is set, CMD is the command.
– If ENTRYPOINT is set, CMD provides arguments to it.
– Always use exec form for CMD.

VOLUME

Instruction Purpose Example
VOLUME <path> Mark a directory as a volume mount point VOLUME /data
VOLUME ["<path1>", "<path2>"] Mark multiple mount points VOLUME ["/var/log", "/data"]

Notes:
– VOLUME is declarative; it documents that a directory should be persisted or mounted.
– Does not actually create a volume; the volume is created at runtime.
– At runtime: docker run -v myvolume:/data image or docker run -v /host/path:/data image

LABEL

Instruction Purpose Example
LABEL <key>=<value> Add metadata to an image LABEL maintainer="team@example.com"
LABEL <key1>=<value1> <key2>=<value2> Add multiple labels LABEL version="1.0" description="My app"

Notes:
– Labels are searchable: docker images --filter "label=version=1.0"
– Common labels: maintainer, version, description, org.opencontainers.image.*

STOPSIGNAL

Instruction Purpose Example
STOPSIGNAL SIGTERM Signal to send when stopping the container (default: SIGTERM) STOPSIGNAL SIGTERM
STOPSIGNAL SIGKILL Force kill signal (rarely needed) STOPSIGNAL SIGKILL

Notes:
– Most apps handle SIGTERM (15) for graceful shutdown.
– Use SIGKILL (9) only if your app doesn’t handle SIGTERM.

SHELL

Instruction Purpose Example
SHELL ["<executable>", "<param>"] Override the default shell (default: ["/bin/sh", "-c"] on Linux) SHELL ["/bin/bash", "-c"]

Use case:

FROM mcr.microsoft.com/windows/servercore:ltsc2022
SHELL ["powershell", "-Command", "-ErrorActionPreference", "Stop"]
RUN Get-ChildItem

ONBUILD

Instruction Purpose Example
ONBUILD <instruction> Trigger an instruction in child images (rarely used) ONBUILD RUN npm install

Notes:
– When a child image uses FROM <parent>, the ONBUILD instructions from the parent are executed.
– Useful for base images that orchestrate builds; avoid in application Dockerfiles.

Multi-stage builds

Multi-stage builds reduce final image size by using intermediate stages for building and a final stage with only runtime dependencies.

Pattern: Builder + Runtime

# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o app .

# Stage 2: Runtime (minimal)
FROM alpine:3.20
COPY --from=builder /src/app /app
ENTRYPOINT ["/app"]

Key benefits:
– Build dependencies (compilers, dev tools) are not in the final image.
– Final image contains only the compiled binary.
– Can reduce image size from 500MB to 10MB or less.

Advanced: Named stages, conditional building

FROM golang:1.22 AS builder
ARG BUILD_TARGET=linux
WORKDIR /src
COPY . .
RUN GOOS=linux GOARCH=amd64 go build -o app .

FROM alpine:3.20 AS runtime
COPY --from=builder /src/app /app
USER nobody
ENTRYPOINT ["/app"]

# Use: docker build --target runtime -t myapp:latest .

Cache mounts (Docker 19.03+)

Cache mounts preserve files between builds, enabling faster package installations and dependency caching.

FROM python:3.12-alpine
WORKDIR /app

# Mount cache for pip packages
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# Use: docker build --build-context . -t myapp:1.0 .

Use cases:
– Package manager caches (pip, npm, cargo, etc.)
– Build artifact caches (Go cache, Rust target, Maven .m2)

Build command:

docker build -t myapp:1.0 --build-context .

.dockerignore

Use .dockerignore to exclude files from the build context, reducing layer size and build time.

# .dockerignore
.git
.gitignore
.env
.env.local
*.pyc
__pycache__
.pytest_cache
node_modules
dist
build
*.log
README.md
.vscode
.idea
.DS_Store

Notes:
– Patterns work like .gitignore.
– Reduces context size sent to Docker daemon.
– Can significantly speed up builds on large projects.

Best practices for security

Run as non-root

FROM alpine:3.20
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup . /app
USER appuser
ENTRYPOINT ["./app"]

Drop unnecessary capabilities

RUN setcap -r /bin/app  # Remove all capabilities from executable
# Or at runtime: docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE image

Use read-only filesystem

# At runtime: docker run --read-only --tmpfs /tmp image

Minimal base images

  • alpine:3.20 (~7 MB) — Recommended for most use cases
  • distroless/python3-nonroot (~50 MB) — Pre-hardened, no package manager
  • scratch (~0 MB) — For static binaries only

Best practices for size

Clean package manager caches

# Alpine
RUN apk add --no-cache python3 py3-pip

# Debian/Ubuntu
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

# pip
RUN pip install --no-cache-dir django

Combine RUN commands

# Bad (3 layers)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# Good (1 layer)
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

Use COPY strategically

# Copy only what you need, in order of change frequency
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY src ./src
# Changing src doesn't invalidate the pip layer

Patterns you’ll reuse

# Base image with ARG for version
FROM python:${PYTHON_VERSION:-3.12}-alpine AS base

# Multi-stage with build args
FROM node:20-alpine AS builder
ARG NODE_ENV=production
RUN npm ci --only=production

# Health check with curl
HEALTHCHECK --interval=30s --timeout=5s \
  CMD curl -f http://localhost:3000/health || exit 1

# Non-root user with static UID for reproducibility
RUN adduser -D -u 1000 appuser

# Copy with ownership for non-root
COPY --chown=appuser:appuser . /app

# Use build cache for package managers
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt

# Exec form ENTRYPOINT + CMD
ENTRYPOINT ["python3", "-u"]
CMD ["app.py"]

FAQ

What’s the difference between COPY and ADD?

COPY is simpler and preferred for local files. ADD can extract tar archives and fetch URLs, but these features can cause unexpected behavior. Use COPY unless you specifically need tar extraction or URL fetching.

Should I use ENV or ARG?

ARG is for build-time variables (available only during build, not in the running container). ENV is for runtime variables (available in the running container). Use ARG for build configuration and ENV for application configuration.

What’s the difference between ENTRYPOINT and CMD?

ENTRYPOINT defines the container’s main process and usually cannot be overridden. CMD provides default arguments to ENTRYPOINT, or is the command if no ENTRYPOINT is set. Use exec form for both.

How do I write a Dockerfile that builds a small image?

Use a base image like alpine:latest or scratch. Use multi-stage builds to copy only compiled artifacts. Remove build dependencies. Clean package manager caches (rm -rf /var/lib/apt/lists/*). Avoid including unnecessary files (use .dockerignore).

Why should I run as a non-root user?

Running as root in a container means a breach or escape could give an attacker root access to the host. Create a dedicated user and drop capabilities to reduce risk. Add USER <uid> before the final command.

How this guide was verified

Written by hand from the official documentation, every command re-checked, and reviewed a second time before publishing. Last reviewed on .

Official sources read for this article

Share.

Haythem Rehouma builds and teaches DevOps and AI tooling: Kubernetes, Docker, Terraform, Claude Code, ChatGPT and n8n. Every KubPoint guide is written from the official docs and re-tested before it goes live.

Comments are closed.