Cheatsheets

Dockerfile

Dockerfile

Comprehensive Dockerfile reference guide covering FROM, RUN, COPY, EXPOSE, CMD, ENTRYPOINT, environment variables, build optimization, best practices, and container image construction.

9 Categories 24 Sections 59 Examples

Getting Started

FROM - Base Image Selection

Understand base image selection and versioning for Dockerfile foundation

FROM Ubuntu LTS Base Image

Select specific Ubuntu version as base image. Using specific versions ensures reproducibility and security patches.

Code
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y curl
Execution
Terminal window
$ docker build -t myapp:latest .
Step 1/2 : FROM ubuntu:20.04
---> 1d622ef08d5e
Step 2/2 : RUN apt-get update && apt-get install -y curl
---> Running in 5f8a9c2b3d4e
Collecting packages...
---> 7c9e4f2b3a5d
Successfully built 7c9e4f2b3a5d
  • Always use specific versions, never just 'ubuntu' or 'latest'
  • ubuntu:20.04 is LTS and receives security updates longer
  • First instruction must be FROM

FROM Alpine Lightweight Base

Alpine-based images are minimal and lightweight, ideal for production microservices where size matters.

Code
FROM alpine:3.18
RUN apk add --no-cache python3 py3-pip
Execution
Terminal window
$ docker build -t lightweight-app:latest .
Step 1/2 : FROM alpine:3.18
---> a24bb4aacf12
Step 2/2 : RUN apk add --no-cache python3 py3-pip
---> Running in 3f7e2c1a9b5d
---> 9c2d4e7f1a3b
Successfully built 9c2d4e7f1a3b
# Final image size: 89MB vs 77MB (Ubuntu) - but lighter for specific apps
  • Alpine uses musl libc instead of glibc
  • Package manager is 'apk' not 'apt'
  • Smallest base images available

FROM Official Python Image

Language-specific official images come with runtime pre-installed, simplifying Dockerfiles.

Code
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
Execution
Terminal window
$ docker build -t python-app:latest .
Step 1/4 : FROM python:3.11-slim
---> b9ef8f396e26
Step 2/4 : WORKDIR /app
---> Running in 3a5f8d2c1e4b
---> 6e2a7f9d4c1b
Step 3/4 : COPY requirements.txt .
---> c9d4e2f7a1b5
Step 4/4 : RUN pip install -r requirements.txt
---> Running in 7f2e4c9a3d1b
Successfully built 8d3f5e2a7c4b
  • python:3.11 has full Python plus build tools (1.1GB)
  • python:3.11-slim has minimal dependencies (181MB)
  • python:3.11-alpine is smallest (51MB)

Image Naming and Tags

Understand Docker image naming conventions and tagging strategies

Tag Image with Domain Registry

Tag images with registry domain and semantic version for private registries and version management.

Code
FROM nginx:1.25
COPY index.html /usr/share/nginx/html/
Execution
Terminal window
$ docker build -t gcr.io/my-project/web-server:1.0.0 .
Successfully built 4d8e2f5a9c1b
$ docker push gcr.io/my-project/web-server:1.0.0
The push refers to repository [gcr.io/my-project/web-server]
8c3f7e2d4a1b: Pushed
1.0.0: digest: sha256:abc123...
  • Format: [registry]/repository:tag
  • gcr.io = Google Container Registry
  • registry.example.com for self-hosted registries

Multiple Tags for Single Image

Tag same image with multiple names for version management and release strategies.

Code
FROM node:18-alpine
COPY app.js .
CMD ["node", "app.js"]
Execution
Terminal window
$ docker build -t myapp:1.2.3 -t myapp:latest -t myapp:stable .
Successfully built 5f9d3a2c8e1b
$ docker images | grep myapp
myapp 1.2.3 5f9d3a2c8e1b
myapp latest 5f9d3a2c8e1b
myapp stable 5f9d3a2c8e1b
  • Use -t flag multiple times to create multiple tags
  • All tags reference same image layers (no duplication)
  • Common tags: latest, stable, v1.2.3, main, rc1

Multi-Stage Dockerfile Overview

Introduction to multi-stage builds for optimized image sizes

Single Stage vs Multi-Stage Comparison

Single stage includes build tools, resulting in large images with unnecessary dependencies.

Code
# Inefficient single stage
FROM golang:1.21
WORKDIR /src
COPY . .
RUN go build -o /usr/local/bin/app .
ENTRYPOINT ["app"]
Execution
Terminal window
$ docker build -t app:single .
Successfully built a2b8f4d7c3e1
$ docker images app:single
REPOSITORY TAG IMAGE ID SIZE
app single a2b8f4d7c3e1 1.3GB
  • golang:1.21 is 1GB+ because it includes compiler and build tools
  • Binary is only a few megabytes
  • Extra bloat in production image

Optimized Multi-Stage Build

Multi-stage builds separate compilation from runtime, drastically reducing final image size.

Code
FROM golang:1.21 AS builder
WORKDIR /src
COPY . .
RUN go build -o /usr/local/bin/app .
FROM alpine:3.18
RUN apk add --no-cache ca-certificates
COPY --from=builder /usr/local/bin/app /usr/local/bin/app
ENTRYPOINT ["app"]
Execution
Terminal window
$ docker build -t app:multi .
Step 1/5 : FROM golang:1.21 AS builder
---> 2d8c4f1a9e3b
Step 3/5 : RUN go build -o /usr/local/bin/app .
---> Running in 4c5f8a2d7e1b
---> 7e3f4c6b1a9d
Step 5/8 : FROM alpine:3.18
---> a24bb4aacf12
Step 6/8 : COPY --from=builder /usr/local/bin/app /usr/local/bin/app
---> c8d2f5a1e7b4
Successfully built 8f2a4e9d3c1b
$ docker images
app multi 8f2a4e9d3c1b 15MB
  • builder stage discarded after build
  • Only Alpine runtime included in final image
  • 1.3GB -> 15MB reduction (1000x smaller)

Copying & Adding Files

COPY - Basic File Operations

Copy files and directories from build context to container

Copy Single File

Copy single file from build context (current directory) into container filesystem.

Code
FROM nginx:1.25-alpine
COPY index.html /usr/share/nginx/html/
Execution
Terminal window
$ docker build -t web:latest .
Step 1/2 : FROM nginx:1.25-alpine
---> a24bb4aacf12
Step 2/2 : COPY index.html /usr/share/nginx/html/
---> 5c2f8e1a3d7b
Successfully built 5c2f8e1a3d7b
  • Source path is relative to build context (docker build . directory)
  • Destination is absolute path in container
  • Creates destination directory if it doesn't exist

Copy Multiple Files with Wildcard

Use wildcards to copy multiple files matching pattern into container.

Code
FROM python:3.11-slim
WORKDIR /app
COPY *.py ./
COPY requirements.txt .
Execution
Terminal window
$ docker build -t python-app:latest .
Step 1/4 : FROM python:3.11-slim
---> b9ef8f396e26
Step 2/4 : WORKDIR /app
---> Running in a1b2c3d4e5f6
---> 7e2f4c8a1d3b
Step 3/4 : COPY *.py ./
---> 3c5f9a2e7d1b
Step 4/4 : COPY requirements.txt .
---> 8f4a2c6e9d1b
Successfully built 8f4a2c6e9d1b
  • Wildcards: * matches any characters, ? matches single character
  • COPY *.py ./ copies all Python files to current directory

Copy Directory Recursively

Copy entire directories with all subdirectories and files.

Code
FROM node:18-alpine
WORKDIR /app
COPY src ./src
COPY public ./public
Execution
Terminal window
$ docker build -t node-app:latest .
Successfully built 9d5e3f2a8c1b
  • Directories copied recursively by default
  • Preserves directory structure in container

COPY with Ownership

Copy files and set ownership to non-root user

Copy with User Ownership

Copy files and assign ownership to non-root user for better security.

Code
FROM node:18-alpine
RUN addgroup -S nodejs && adduser -S nodejs -G nodejs
WORKDIR /app
COPY --chown=nodejs:nodejs . .
USER nodejs
CMD ["node", "index.js"]
Execution
Terminal window
$ docker build -t secure-app:latest .
Successfully built 7c3e5f1a2d8b
$ docker run secure-app:latest
# App runs as nodejs user, not root
  • Format: COPY --chown=user:group source dest
  • Prevents running container as root
  • User/group must exist in image

Copy with Numeric User ID

Use numeric user:group IDs for CHOWN (more portable across systems).

Code
FROM python:3.11-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /home/appuser/app
COPY --chown=1000:1000 . .
USER appuser
ENTRYPOINT ["python", "main.py"]
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 4b2f7d9a1c5e
  • Numeric IDs are more reliable than names
  • 1000:1000 common for non-root user

ADD - Advanced File Operations

Add files, directories, or remote URLs with automatic extraction

ADD from Remote URL

Download files from URLs and automatically extract tar archives.

Code
FROM ubuntu:20.04
ADD https://example.com/app.tar.gz /tmp/
WORKDIR /app
RUN tar -xzf /tmp/app.tar.gz && rm /tmp/app.tar.gz
Execution
Terminal window
$ docker build -t app:latest .
Step 1/4 : FROM ubuntu:20.04
---> 1d622ef08d5e
Step 2/4 : ADD https://example.com/app.tar.gz /tmp/
Downloading [==================================================>] 42MB/42MB
---> 8c5f2a1d4e9b
Step 3/4 : WORKDIR /app
---> 7e3f4c2a1d8b
Successfully built 7e3f4c2a1d8b
  • Automatically extracts .tar.* files
  • Downloads happen during build
  • Should be followed by cleanup (rm) to remove archive

ADD Automatically Extracts TAR

TAR archive automatically extracted to destination directory.

Code
FROM alpine:3.18
ADD https://releases.example.com/v1.2.3/app.tar.gz /opt/
WORKDIR /opt
RUN ls -la
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 3f9e2d5a1c7b
Step 4/4 : RUN ls -la
---> Running in 5d8c3f2a1e9b
total 48
drwxr-xr-x 3 root root 4096 Feb 28 12:00 .
drwxr-xr-x 1 root root 4096 Feb 28 12:00 ..
-rw-r--r-- 1 root root 12345678 Feb 28 12:00 app
-rw-r--r-- 1 root root 54321 Feb 28 12:00 config.yaml
  • Only extracts recognized tar formats (.tar.gz, .tar.bz2, .tar)
  • Regular files copied as-is

Running Commands

RUN - Basic Command Execution

Execute commands during image build

RUN Single Command

Execute shell command during build. Each RUN creates a new layer.

Code
FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y curl
Execution
Terminal window
$ docker build -t app:latest .
Step 1/3 : FROM ubuntu:20.04
---> 1d622ef08d5e
Step 2/3 : RUN apt-get update
---> Running in 5f8a9c2b3d4e
Reading package lists... Done
---> 7c9e4f2b3a5d
Step 3/3 : RUN apt-get install -y curl
---> Running in 3d7f2a1c8e5b
Setting up curl (7.68.0-1ubuntu4)...
---> 8b4f3e2c9a1d
Successfully built 8b4f3e2c9a1d
  • Each RUN instruction creates separate layer
  • Inefficient to use multiple RUN for related commands
  • Command executes in /bin/sh by default

RUN Chain Commands with AND

Chain multiple commands with && to create single layer and improve image efficiency.

Code
FROM ubuntu:20.04
RUN apt-get update && \
apt-get install -y curl git vim && \
apt-get clean
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 9d5e3f2a8c1b
$ docker history app:latest
IMAGE CREATED SIZE
9d5e3f2a8c1b 30 seconds ago 187MB
  • && ensures next command runs only if previous succeeded
  • Backslash continues line in Dockerfile
  • apt-get clean removes package cache

RUN Install Multiple Packages

Multi-line RUN with backslash for readability while maintaining single layer.

Code
FROM debian:12-slim
RUN apt-get update && apt-get install -y \
build-essential \
git \
curl \
wget \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
Execution
Terminal window
$ docker build -t build-tools:latest .
Step 1/2 : FROM debian:12-slim
---> 7b2f8e3a9c1d
Step 2/2 : RUN apt-get update && apt-get install -y ...
---> Running in 4f9d2c5a1e8b
Processing triggers...
---> 8c3f7e2d4a9b
Successfully built 8c3f7e2d4a9b
  • apt-get clean removes cache (reduces layer size)
  • rm -rf /var/lib/apt/lists/* also reduces layer

RUN - Exec Form vs Shell Form

Understand differences between exec form and shell form in RUN

Shell Form (Default)

Shell form processes variables and shell syntax (pipes, redirects, etc).

Code
FROM alpine:3.18
RUN echo "Building application"
RUN echo $PATH
Execution
Terminal window
$ docker build -t app:latest .
Step 1/3 : FROM alpine:3.18
---> a24bb4aacf12
Step 2/3 : RUN echo "Building application"
---> Running in 5f8a9c2b3d4e
Building application
---> 7c9e4f2b3a5d
Step 3/3 : RUN echo $PATH
---> Running in 3d7f2a1c8e5b
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
---> 8b4f3e2c9a1d
  • RUN command will use /bin/sh -c
  • Environment variables expanded
  • Pipes and redirects work

Exec Form (JSON Array)

Exec form calls command directly without shell interpretation.

Code
FROM alpine:3.18
RUN ["apk", "add", "--no-cache", "curl"]
RUN ["echo", "Building"]
Execution
Terminal window
$ docker build -t app:latest .
Step 1/3 : FROM alpine:3.18
---> a24bb4aacf12
Step 2/2 : RUN ["apk", "add", "--no-cache", "curl"]
---> Running in 5f8a9c2b3d4e
Fetching https://dl-cdn.alpinelinux.org/alpine/v3.18/main/x86_64/PACKAGES.gz
(1/2) Installing ca-certificates
---> 7c9e4f2b3a5d
Step 3/3 : RUN ["echo", "Building"]
---> Running in 3d7f2a1c8e5b
Building
---> 8b4f3e2c9a1d
  • No shell expansion or variable interpolation
  • Command executed directly (better for signals)
  • Each array element becomes separate argument

Shell Parameter in RUN

Set default shell with SHELL instruction for all RUN, ENTRYPOINT, CMD instructions.

Code
FROM ubuntu:20.04
SHELL ["/bin/bash", "-c"]
RUN for i in 1 2 3; do echo "Number: $i"; done
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 5c2f8e1a3d7b
Step 2/2 : RUN for i in 1 2 3; do echo "Number: $i"; done
---> Running in 4f9d2c5a1e8b
Number: 1
Number: 2
Number: 3
---> 8f4a2c6e9d1b
  • SHELL instruction affects subsequent instructions
  • Useful for bash-specific syntax (loops, pipes, etc)

Environment & Variables

ENV - Environment Variables

Set environment variables that persist in running containers

Set Single Environment Variable

ENV sets environment variables available during build and in running containers.

Code
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1
ENV APP_ENV=production
RUN echo "Environment: $APP_ENV"
Execution
Terminal window
$ docker build -t app:latest .
Step 1/4 : FROM python:3.11-slim
---> b9ef8f396e26
Step 2/4 : ENV PYTHONUNBUFFERED=1
---> Running in a1b2c3d4e5f6
---> 7e2f4c8a1d3b
Step 3/4 : ENV APP_ENV=production
---> Running in 5f8a9c2b3d4e
---> 8f4a2c6e9d1b
Step 4/4 : RUN echo "Environment: $APP_ENV"
---> Running in 3d7f2a1c8e5b
Environment: production
---> 9c2d4e7f1a3b
Successfully built 9c2d4e7f1a3b
  • Variables persist in running containers
  • Available in all subsequent build steps
  • Can be overridden at runtime with -e flag

Multiple Variables and Defaults

Set multiple environment variables with backslash continuation.

Code
FROM node:18-alpine
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=info \
DATABASE_URL=postgresql://localhost/db
RUN echo "Running in $NODE_ENV mode on port $PORT"
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 5c2f8e1a3d7b
$ docker run app:latest env
NODE_ENV=production
PORT=3000
LOG_LEVEL=info
DATABASE_URL=postgresql://localhost/db
  • Multiple ENV values on single line with backslash
  • All variables available in container at runtime

Variable Substitution in Dockerfile

Use variables in subsequent ENV declarations and RUN instructions.

Code
FROM ubuntu:20.04
ENV APP_VERSION=2.5.1
ENV APP_PATH=/opt/app-${APP_VERSION}
RUN mkdir -p $APP_PATH && echo "App path: $APP_PATH"
LABEL version="${APP_VERSION}"
Execution
Terminal window
$ docker build -t app:2.5.1 .
Step 4/5 : RUN mkdir -p $APP_PATH && echo "App path: $APP_PATH"
---> Running in 5f8a9c2b3d4e
App path: /opt/app-2.5.1
---> 7c9e4f2b3a5d
Successfully built 7c9e4f2b3a5d
  • ${VAR} syntax substitutes variable values
  • Variables only available from that line onward

ARG - Build Arguments

Define build-time arguments that don't persist in final image

ARG for Build-Time Configuration

ARG defines build-time arguments passed via --build-arg flag, not persisted in image.

Code
FROM python:3.11-slim
ARG BUILD_DATE
ARG VCS_REF
ENV BUILD_DATE=${BUILD_DATE}
ENV VCS_REF=${VCS_REF}
RUN echo "Built on: $BUILD_DATE from commit: $VCS_REF"
Execution
Terminal window
$ docker build --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=$(git rev-parse --short HEAD) \
-t app:latest .
Step 1/5 : FROM python:3.11-slim
---> b9ef8f396e26
Step 5/5 : RUN echo "Built on: $BUILD_DATE from commit: $VCS_REF"
---> Running in 5f8a9c2b3d4e
Built on: 2026-02-28T12:00:00Z from commit: abc123xyz
---> 8f4a2c6e9d1b
Successfully built 8f4a2c6e9d1b
  • Build arguments only available during build
  • Not included in final image
  • Use ENV to persist values

ARG with Default Values

ARG with default values that can be overridden at build time.

Code
FROM node:18-alpine
ARG NODE_ENV=development
ARG VERSION=1.0.0
ARG PORT=3000
ENV NODE_ENV=${NODE_ENV} \
VERSION=${VERSION} \
PORT=${PORT}
RUN echo "Building v$VERSION for $NODE_ENV on port $PORT"
Execution
Terminal window
$ docker build -t app:latest .
Step 5/5 : RUN echo "Building v$VERSION for $NODE_ENV on port $PORT"
---> Running in 5f8a9c2b3d4e
Building v1.0.0 for development on port 3000
---> 7c9e4f2b3a5d
$ docker build --build-arg NODE_ENV=production \
--build-arg VERSION=2.0.0 \
-t app:2.0.0 .
Step 5/5 : RUN echo "Building v$VERSION for $NODE_ENV on port $PORT"
---> Running in 3d7f2a1c8e5b
Building v2.0.0 for production on port 3000
---> 8b4f3e2c9a1d
  • Defaults used if no --build-arg provided
  • Can be overridden per build

Variable Substitution and Defaults

Advanced variable substitution patterns in Dockerfile

Variable Expansion with Defaults

Use variables in FROM and subsequent instructions for flexible builds.

Code
ARG UBUNTU_VERSION=20.04
FROM ubuntu:${UBUNTU_VERSION}
ARG APP_PATH=/opt/app
ARG APP_USER=appuser
RUN mkdir -p ${APP_PATH} && \
useradd -r -m -d ${APP_PATH} ${APP_USER}
Execution
Terminal window
$ docker build -t app:focal .
Successfully built 7c9e4f2b3a5d
$ docker build --build-arg UBUNTU_VERSION=22.04 \
-t app:jammy .
Step 1/4 : FROM ubuntu:22.04
---> 6790c6674aaa
Successfully built 4b2f7d9a1c5e
  • ARG before FROM available in FROM instruction
  • Base image variant changes based on ARG

Build-Stage Variables

ARG declared at different stages for multi-stage build flexibility.

Code
ARG GOLANG_VERSION=1.21
FROM golang:${GOLANG_VERSION} AS builder
ARG VERSION=1.0.0
WORKDIR /src
RUN echo "Building with Go $GOLANG_VERSION, version $VERSION"
FROM alpine:3.18
ARG VERSION=1.0.0
COPY --from=builder /src /app
ENV VERSION=${VERSION}
Execution
Terminal window
$ docker build --build-arg GOLANG_VERSION=1.22 \
--build-arg VERSION=2.0.0 \
-t app:2.0.0 .
Step 1/6 : ARG GOLANG_VERSION=1.21
---> Running in 5f8a9c2b3d4e
Step 2/6 : FROM golang:1.22
---> 2d8c4f1a9e3b
Step 3/6 : ARG VERSION=1.0.0
---> Running in 3d7f2a1c8e5b
Successfully built 5c2f8e1a3d7b
  • ARG before FROM is global
  • ARG in stage is local to that stage
  • Each stage can redefine ARG

Working Directory & Volumes

WORKDIR - Working Directory

Set working directory for subsequent instructions

WORKDIR Basic Usage

WORKDIR sets current working directory for COPY, RUN, CMD, and ENTRYPOINT.

Code
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN ls -la
Execution
Terminal window
$ docker build -t app:latest .
Step 1/4 : FROM python:3.11-slim
---> b9ef8f396e26
Step 2/4 : WORKDIR /app
---> Running in a1b2c3d4e5f6
---> 7e2f4c8a1d3b
Step 3/4 : COPY . .
---> 3c5f9a2e7d1b
Step 4/4 : RUN ls -la
---> Running in 5f8a9c2b3d4e
total 48
drwxr-xr-x 8 root root 4096 Feb 28 12:00 .
-rw-r--r-- 1 root root 1234 Feb 28 12:00 main.py
-rw-r--r-- 1 root root 234 Feb 28 12:00 requirements.txt
---> 8f4a2c6e9d1b
Successfully built 8f4a2c6e9d1b
  • Creates directory if it doesn't exist
  • Subsequent COPY/ADD use this as destination
  • RUN commands execute in this directory

Multiple WORKDIR Instructions

Multiple WORKDIR instructions change directory progressively.

Code
FROM node:18-alpine
WORKDIR /app
COPY package.json .
RUN npm install
WORKDIR /app/src
COPY src .
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 5c2f8e1a3d7b
  • Each WORKDIR is cumulative (relative paths work)
  • /app and /app/src are different working directories

WORKDIR with Variables

Use ARG/ENV variables in WORKDIR for flexible directory paths.

Code
FROM ubuntu:20.04
ARG APP_HOME=/usr/local/app
ENV APP_HOME=${APP_HOME}
WORKDIR ${APP_HOME}
RUN echo "Application home: $(pwd)"
Execution
Terminal window
$ docker build --build-arg APP_HOME=/opt/application \
-t app:latest .
Step 4/5 : WORKDIR /opt/application
---> Running in 5f8a9c2b3d4e
---> 7c9e4f2b3a5d
Step 5/5 : RUN echo "Application home: $(pwd)"
---> Running in 3d7f2a1c8e5b
Application home: /opt/application
---> 8b4f3e2c9a1d
  • Variables interpolated at build time

VOLUME - Data Persistence Points

Define mount points for persistent data

VOLUME for Database Storage

VOLUME declares mount points for persistent data that survives container removal.

Code
FROM postgres:15-alpine
VOLUME ["/var/lib/postgresql/data"]
EXPOSE 5432
CMD ["postgres"]
Execution
Terminal window
$ docker build -t postgres-custom:latest .
Successfully built 7c9e4f2b3a5d
$ docker run -d postgres-custom:latest
4f8e2c7a3b1d
$ docker inspect 4f8e2c7a3b1d --format='{{json .Mounts}}'
[{"Type":"volume","Name":"abc123def456","Source":"...","Destination":"/var/lib/postgresql/data"...}]
  • Creates anonymous volume if not specified
  • Data persists even if container is deleted
  • Can be mounted by other containers

Multiple Volumes

Multiple VOLUME instructions create separate mount points for different data.

Code
FROM mysql:8.0
VOLUME ["/var/lib/mysql", "/var/log/mysql"]
EXPOSE 3306
ENV MYSQL_ROOT_PASSWORD=secret
CMD ["mysqld"]
Execution
Terminal window
$ docker run -d mysql-custom:latest
5f9d3a2c8e1b
$ docker volume ls | grep mysql
volumes abc123def456 (database files)
volumes def789ghi012 (log files)
  • Each VOLUME is independent
  • Can mount to named volumes with -v flag

Exposing Ports

EXPOSE - Port Declarations

Document which ports the application listens on

EXPOSE Single Port

EXPOSE documents which ports container listens on (does not actually publish).

Code
FROM nginx:1.25-alpine
EXPOSE 80
EXPOSE 443
COPY index.html /usr/share/nginx/html/
Execution
Terminal window
$ docker build -t web-server:latest .
Successfully built 5c2f8e1a3d7b
$ docker inspect web-server:latest --format='{{json .ExposedPorts}}'
{"80/tcp":{},"443/tcp":{}}
  • EXPOSE is declarative and informational
  • Does NOT actually publish ports
  • Use -p flag at runtime to publish

EXPOSE Multiple Ports

Multiple EXPOSE declarations for application and debug ports.

Code
FROM node:18-alpine
EXPOSE 3000 3001 9229
COPY . .
RUN npm install
CMD ["npm", "start"]
Execution
Terminal window
$ docker build -t node-app:latest .
Successfully built 7c9e4f2b3a5d
  • 3000: application port
  • 3001: alternative service
  • 9229: Node.js debugger port

EXPOSE with Port Range

EXPOSE range of ports for applications using dynamic port allocation.

Code
FROM ubuntu:20.04
EXPOSE 8000-8100
RUN apt-get update && apt-get install -y netcat
CMD ["nc", "-l", "-p", "8000"]
Execution
Terminal window
$ docker build -t range-app:latest .
Successfully built 8b4f3e2c9a1d
$ docker run -p 8000-8100:8000-8100 range-app:latest
  • Useful for services with multiple instances
  • Port range notation: START-END

Entry Points & Commands

CMD - Default Command

Specify default command to run when container starts

CMD Shell Form

CMD shell form executes command through shell, allowing variable expansion.

Code
FROM python:3.11-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD python app.py
Execution
Terminal window
$ docker build -t python-app:latest .
Successfully built 5c2f8e1a3d7b
$ docker run python-app:latest
Starting application...
App is running
  • Runs via /bin/sh -c
  • Can use environment variables
  • Same as typing command in shell

CMD Exec Form (JSON)

CMD exec form calls command directly without shell, better for signals.

Code
FROM node:18-alpine
COPY . /app
WORKDIR /app
RUN npm install
CMD ["node", "server.js"]
Execution
Terminal window
$ docker build -t node-app:latest .
Successfully built 7c9e4f2b3a5d
$ docker run node-app:latest
Server listening on port 3000
  • No shell interpretation
  • Each element becomes separate argument
  • Signals (SIGTERM) properly delivered

Override CMD at Runtime

CMD can be overridden by providing command at docker run time.

Code
FROM python:3.11-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
Execution
Terminal window
$ docker run python-app:latest
Running app.py...
$ docker run python-app:latest python -m pytest tests/
Running tests...
test_app.py::test_main PASSED
  • Container image has default CMD
  • Passing command at runtime overrides it

ENTRYPOINT - Entry Point

Configure container as executable with ENTRYPOINT

ENTRYPOINT with Exec Form

ENTRYPOINT with exec form makes container behave as executable.

Code
FROM python:3.11-slim
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["python", "app.py"]
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 5c2f8e1a3d7b
$ docker run app:latest
Setup: Creating database...
Starting application...
$ docker run app:latest python -c "import sys; print(sys.version)"
Setup: Creating database...
3.11.2
  • ENTRYPOINT is the wrapper script
  • CMD provides default arguments
  • Useful for setup before main app

ENTRYPOINT with Shell Script

ENTRYPOINT executes shell script as main process.

Code
FROM alpine:3.18
RUN apk add --no-cache bash curl
COPY health-check.sh /usr/local/bin/health-check
RUN chmod +x /usr/local/bin/health-check
ENTRYPOINT ["bash", "/usr/local/bin/health-check"]
Execution
Terminal window
$ docker build -t health-checker:latest .
Successfully built 7c9e4f2b3a5d
$ docker run health-checker:latest api.example.com
Checking health of api.example.com
Status: OK
  • Script runs as PID 1 in container
  • Must handle signals properly
  • Can accept arguments from docker run

ENTRYPOINT with CMD

ENTRYPOINT + CMD combination where tini manages signals properly.

Code
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y curl tini
COPY . /app
WORKDIR /app
ENTRYPOINT ["tini", "--"]
CMD ["./app", "start"]
Execution
Terminal window
$ docker build -t tini-app:latest .
Successfully built 8b4f3e2c9a1d
$ docker run tini-app:latest
Starting app with tini init system...
  • tini is lightweight init system for containers
  • Ensures proper signal handling
  • CMD provides default arguments to ENTRYPOINT

CMD and ENTRYPOINT Interaction

Understand how CMD and ENTRYPOINT work together

ENTRYPOINT as Wrapper with CMD Arguments

ENTRYPOINT and CMD together allow flexible command execution with wrapper.

Code
FROM python:3.11-slim
COPY wrapper.sh /usr/local/bin/
COPY app.py .
RUN chmod +x /usr/local/bin/wrapper.sh
ENTRYPOINT ["/usr/local/bin/wrapper.sh"]
CMD ["python", "app.py"]
Execution
Terminal window
$ docker build -t wrapped-app:latest .
Successfully built 5c2f8e1a3d7b
$ docker run wrapped-app:latest
[WRAPPER] Starting application...
App is running
$ docker run wrapped-app:latest python -c "print('test')"
[WRAPPER] Starting...
test
  • CMD becomes arguments to ENTRYPOINT
  • Can override CMD at runtime
  • Wrapper executes first, then CMD

Pure ENTRYPOINT (No CMD)

ENTRYPOINT alone without CMD for single-purpose containers.

Code
FROM golang:1.21-alpine
WORKDIR /src
COPY . .
RUN go build -o /app .
ENTRYPOINT ["/app"]
Execution
Terminal window
$ docker build -t go-app:latest .
Successfully built 7c9e4f2b3a5d
$ docker run go-app:latest --help
Usage: app [OPTIONS]
  • Container is tool-like (always runs binary)
  • Can still pass arguments
  • No default command

Build Optimization

Layer Caching Strategy

Understand Docker layer caching to speed up builds

Inefficient Layer Caching

Copying all files early invalidates cache when any file changes, forcing reinstall.

Code
FROM python:3.11-slim
COPY . .
RUN pip install -r requirements.txt
RUN python app.py
Execution
Terminal window
$ docker build -t app:latest .
Step 1/4 : FROM python:3.11-slim
---> b9ef8f396e26
Step 2/4 : COPY . .
---> 7e2f4c8a1d3b
Step 3/4 : RUN pip install -r requirements.txt
---> Running in 5f8a9c2b3d4e
Collecting requests...
---> 8f4a2c6e9d1b (3 min 45 sec)
Step 4/4 : RUN python app.py
---> Running in 3d7f2a1c8e5b
---> 9c2d4e7f1a3b
# After changing app.py
$ docker build -t app:latest .
Step 1/4 : FROM python:3.11-slim
---> b9ef8f396e26
Step 2/4 : COPY . .
---> (invalidated by changed app.py)
Step 3/4 : RUN pip install -r requirements.txt
---> Running in 5f8a9c2b3d4e (runs again!)
Collecting requests...
---> 8f4a2c6e9d1b (3 min 45 sec - WASTED TIME)
  • COPY . . invalidates all subsequent layers on any change
  • Python packages reinstalled unnecessarily

Optimized Layer Caching

Copy stable files (dependencies) first, changing files last to preserve cache.

Code
FROM python:3.11-slim
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Execution
Terminal window
$ docker build -t app:latest .
Step 1/5 : FROM python:3.11-slim
---> b9ef8f396e26
Step 2/5 : COPY requirements.txt .
---> 7e2f4c8a1d3b
Step 3/5 : RUN pip install -r requirements.txt
---> Running in 5f8a9c2b3d4e
Collecting requests...
---> 8f4a2c6e9d1b (3 min 45 sec)
Step 4/5 : COPY . .
---> 9d5e3f2a8c1b
Step 5/5 : CMD ["python", "app.py"]
---> 5c2f8e1a3d7b
# After changing app.py
$ docker build -t app:latest .
Step 1/5 : FROM python:3.11-slim
---> b9ef8f396e26 (cached)
Step 2/5 : COPY requirements.txt .
---> 7e2f4c8a1d3b (cached)
Step 3/5 : RUN pip install -r requirements.txt
---> 8f4a2c6e9d1b (cached) - REUSED!
Step 4/5 : COPY . .
---> (invalidated by changed app.py)
Step 5/5 : CMD ["python", "app.py"]
---> 5c2f8e1a3d7b
Total time: 5 seconds vs 3 min 50 sec
  • Stable: requirements.txt, package.json rarely change
  • Volatile: app.py, src files change frequently
  • Saves significant build time

Cache with External Mounts

Cache mounts preserve package manager caches across builds.

Code
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json .
RUN --mount=type=cache,target=/root/.npm \
npm ci --prefer-offline --no-audit
COPY . .
RUN npm run build
Execution
Terminal window
$ docker build --build-context=. \
--progress=plain \
-t app:latest .
Step 1/6 : FROM node:18-alpine
---> 52f389ea4d15
Step 4/6 : RUN --mount=type=cache,target=/root/.npm ...
---> Running in 5f8a9c2b3d4e
added 1200 packages in 45s
# Second build
$ docker build -t app:latest .
Step 4/6 : RUN --mount=type=cache,target=/root/.npm ...
---> Running in 3d7f2a1c8e5b
up to date (cached packages reused!)
added 1200 packages in 2s
  • npm/yarn/pip cache reused
  • Requires BuildKit (DOCKER_BUILDKIT=1)
  • Dramatically speeds up dependency installation

.dockerignore File

Exclude files from build context to speed up builds

Basic .dockerignore

.dockerignore filters out unnecessary files before sending to Docker daemon.

Code
# .dockerignore content
.git
.gitignore
.github
node_modules
.env
.env.local
dist
build
__pycache__
*.log
.DS_Store
.vscode
.idea
npm-debug.log
.npm
coverage
  • Dramatically reduces build context size
  • Speeds up build (less data to transfer)
  • Similar to .gitignore syntax

Comprehensive .dockerignore

Comprehensive .dockerignore excludes all unnecessary files from context.

Code
# .dockerignore with multiple patterns
# Version control
.git
.gitignore
.github
.gitlab-ci.yml
# Dependencies (often excluded from repo)
node_modules
venv
vendor
.bundle
# Development files
.env
.env.*.local
.vscode
.idea
.DS_Store
*.swp
*.swo
*~
# Build artifacts
dist
build
*.tmp
coverage
# Logs
*.log
logs
# Docker
Dockerfile
compose.yml
.dockerignore
# CI/CD
.circleci
.travis.yml
Jenkinsfile
# Documentation
docs
README.md
# Testing
tests
__pycache__
.pytest_cache
.coverage
Execution
Terminal window
$ docker build -t app:latest .
# Context drastically reduced
  • Reduces build context from 500MB to 11MB
  • Speeds up docker build command significantly

Multi-Stage Build Optimization

Advanced multi-stage patterns for minimal images

Multi-Stage Node.js Build

Multi-stage Node.js avoids dev dependencies in final image.

Code
FROM node:18-alpine AS builder
WORKDIR /src
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /src/node_modules ./node_modules
COPY . .
USER nobody
CMD ["node", "server.js"]
Execution
Terminal window
$ docker build -t node-app:latest .
Step 1/8 : FROM node:18-alpine AS builder
---> 52f389ea4d15
Step 2/8 : WORKDIR /src
---> Running in 5f8a9c2b3d4e
Step 4/8 : RUN npm ci --only=production
---> Running in 3d7f2a1c8e5b
added 120 packages in 30s
---> 7c9e4f2b3a5d
Step 5/8 : FROM node:18-alpine
---> 52f389ea4d15
Step 8/8 : CMD ["node", "server.js"]
---> Running in 2f5c8e1a3d7b
Successfully built 5f9d3a2c8e1b
$ docker images node-app:latest
REPOSITORY TAG IMAGE ID SIZE
node-app latest 5f9d3a2c8e1b 156MB
  • Builder stage includes all dependencies
  • Final stage has only production dependencies
  • Size reduction: devDependencies excluded

Multi-Stage Rust Build

Rust compiler (2GB) not in final image, only binary (2-5MB).

Code
FROM rust:1.75 AS builder
WORKDIR /usr/src/app
COPY . .
RUN cargo build --release
FROM debian:12-slim
COPY --from=builder /usr/src/app/target/release/app /usr/local/bin/
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
USER nobody
ENTRYPOINT ["app"]
Execution
Terminal window
$ docker build -t rust-app:latest .
Step 1/6 : FROM rust:1.75
---> f7d8c1e2a9b3
Step 3/6 : RUN cargo build --release
---> Running in 5f8a9c2b3d4e
Compiling app v1.0.0
---> 7c9e4f2b3a5d (5 min 30 sec)
Step 4/6 : FROM debian:12-slim
---> 5f8a9c2b3d4e
Step 5/6 : COPY --from=builder /usr/src/app/target/release/app /usr/local/bin/
---> Running in 3d7f2a1c8e5b
---> 4b2f7d9a1c5e
Successfully built 4b2f7d9a1c5e
$ docker images rust-app:latest
REPOSITORY TAG IMAGE ID SIZE
rust-app latest 4b2f7d9a1c5e 45MB
  • Builder: rust:1.75 (2GB with compiler)
  • Final: debian:12-slim + binary (45MB total)
  • 2GB -> 45MB reduction!

Metadata & Advanced

LABEL - Image Metadata

Add metadata labels to Docker images

Common Docker Labels

LABEL adds metadata to image for documentation and organization.

Code
FROM python:3.11-slim
LABEL maintainer="devops@example.com"
LABEL version="1.2.3"
LABEL description="Python API application for order processing"
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.created="2026-02-28T00:00:00Z"
Execution
Terminal window
$ docker build -t order-api:1.2.3 .
Successfully built 7c9e4f2b3a5d
$ docker inspect order-api:1.2.3 --format='{{json .Config.Labels}}'
{
"maintainer":"devops@example.com",
"version":"1.2.3",
"description":"Python API...",
"org.opencontainers.image.source":"https://github.com/org/repo",
"org.opencontainers.image.created":"2026-02-28T00:00:00Z"
}
  • Labels appear in docker inspect output
  • Useful for filtering and organizing images
  • No size impact

Multi-line Labels

Multiple labels using backslash continuation for readability.

Code
FROM node:18-alpine
LABEL maintainer="platform-team@company.com" \
org.opencontainers.image.title="Web Server" \
org.opencontainers.image.description="Production Node.js web server" \
org.opencontainers.image.version="2.1.0" \
org.opencontainers.image.authors="John Doe, Jane Smith"
Execution
Terminal window
$ docker build -t web-server:2.1.0 .
Successfully built 5c2f8e1a3d7b
  • Backslash extends single instruction across lines
  • Each label can be queried independently

USER - Container User

Specify user to run container as instead of root

Run as Created User

Create dedicated user and switch to it before running application.

Code
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y nodejs
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /home/appuser/app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["node", "server.js"]
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 5c2f8e1a3d7b
$ docker run app:latest
(running as appuser, not root)
$ docker run app:latest id
uid=100(appuser) gid=101(appuser) groups=101(appuser)
  • Root user is dangerous in containers
  • appuser has no shell (better security)
  • Works with CMD/ENTRYPOINT

Run as Numeric UID

Use numeric UID instead of username for better portability.

Code
FROM python:3.11-slim
RUN groupadd -r app && useradd -r -u 1001 -g app app
WORKDIR /app
COPY --chown=1001:1000 . .
USER 1001
CMD ["python", "app.py"]
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 7c9e4f2b3a5d
$ docker run app:latest whoami
# Returns: 1001 (numeric ID)
  • 1001 common for first non-system user
  • More portable across systems

HEALTHCHECK - Container Health

Define health check to determine if container is running properly

HTTP Endpoint Health Check

Check container health by making HTTP request to application.

Code
FROM nginx:1.25-alpine
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1
COPY index.html /usr/share/nginx/html/
Execution
Terminal window
$ docker build -t web-server:latest .
Successfully built 5c2f8e1a3d7b
$ docker run -d web-server:latest
4f8e2c7a3b1d
$ sleep 10 && docker inspect 4f8e2c7a3b1d --format='{{.State.Health.Status}}'
healthy
  • interval=30s: check every 30 seconds
  • timeout=3s: health check must complete in 3 seconds
  • start-period=5s: grace period before checks start
  • retries=3: 3 failed checks = unhealthy

Application-Specific Health Check

Use application-specific tools for health checks.

Code
FROM postgres:15-alpine
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=5 \
CMD ["pg_isready", "-U", "postgres"]
Execution
Terminal window
$ docker build -t postgres-custom:latest .
Successfully built 7c9e4f2b3a5d
$ docker run -d postgres-custom:latest
5f9d3a2c8e1b
$ docker container ls
CONTAINER ID IMAGE STATUS
5f9d3a2c8e1b postgres-custom:latest Up 15s (healthy)
  • pg_isready for PostgreSQL
  • redis-cli PING for Redis
  • mysql -e "SELECT 1" for MySQL

Script-Based Health Check

Custom script for complex health checks.

Code
FROM node:18-alpine
COPY health-check.js /usr/local/bin/
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD node /usr/local/bin/health-check.js
COPY . .
CMD ["node", "server.js"]
Execution
Terminal window
$ docker build -t app:latest .
Successfully built 8b4f3e2c9a1d
  • Exit code 0 = healthy
  • Exit code 1 = unhealthy
  • Script can hit any endpoint

Dockerfile Shell Form

Control shell behavior with

Use Bash Instead of /bin/sh

Change default shell from /bin/sh to /bin/bash for bash-specific features.

Code
FROM ubuntu:20.04
SHELL ["/bin/bash", "-c"]
RUN apt-get update && apt-get install -y curl
RUN <<EOF
for i in {1..5}; do
echo "Iteration: $i"
done
EOF
Execution
Terminal window
$ docker build -t app:latest .
Step 1/3 : FROM ubuntu:20.04
---> 1d622ef08d5e
Step 2/3 : SHELL ["/bin/bash", "-c"]
---> Running in 5f8a9c2b3d4e
---> 7c9e4f2b3a5d
Step 3/3 : RUN <<EOF
---> Running in 3d7f2a1c8e5b
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
Successfully built 8b4f3e2c9a1d
  • SHELL must appear early in Dockerfile
  • Affects all RUN, CMD, ENTRYPOINT, COPY --chown
  • Alpine Linux doesn't have bash by default

Multi-Line RUN with Heredoc

Heredoc syntax for multi-line RUN commands without backslash continuation.

Code
FROM python:3.11-slim
RUN <<EOF
apt-get update
apt-get install -y curl git vim
apt-get clean
rm -rf /var/lib/apt/lists/*
EOF
Execution
Terminal window
$ docker build -t app:latest .
Step 1/2 : FROM python:3.11-slim
---> b9ef8f396e26
Step 2/2 : RUN <<EOF
---> Running in 5f8a9c2b3d4e
Reading package lists...
Processing triggers...
---> 8f4a2c6e9d1b
Successfully built 8f4a2c6e9d1b
  • Available in Docker 23.09+
  • Cleaner than backslash continuation
  • Each line individual command (not chained)