---
title: "Dockerfile"
description: "Dockerfile reference guide covering FROM, RUN, COPY, EXPOSE, CMD, ENTRYPOINT, environment variables, build optimization, best practices, and container image construction."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/cheatsheets/dockerfile
---

# Dockerfile

This cheat sheet covers the core Dockerfile instructions, best practices, and common pitfalls for building small, secure Docker images. Each section pairs a Dockerfile snippet with the build output it produces, plus notes on how the instruction behaves.

## Getting Started

### FROM - Base Image Selection

Base image selection and version pinning for a Dockerfile

**Keywords:** FROM, base, image, version, tag, registry

#### FROM Ubuntu LTS Base Image

```dockerfile
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y curl
```

_exec_
```bash
$ 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
```

Select a specific Ubuntu version as the base image. Pinning the version keeps builds reproducible and makes the patch level explicit.

- 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

```dockerfile
FROM alpine:3.18
RUN apk add --no-cache python3 py3-pip
```

_exec_
```bash
$ 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-based images are minimal, which suits production microservices where image size matters.

- Alpine uses musl libc instead of glibc
- Package manager is 'apk' not 'apt'
- Smallest base images available

#### FROM Official Python Image

```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
```

_exec_
```bash
$ 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
```

Language-specific official images ship with the runtime already installed, so the Dockerfile stays shorter.

- 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)

**Best practices:**

- Always specify explicit version tags (never use latest)
- Choose slim or alpine variants for production to reduce image size
- Use official images from Docker Hub when possible
- Review image documentation for security and size considerations

**Common errors:**

- **FROM instruction not first**: FROM must be first instruction in Dockerfile
- **Using latest tag**: Loses reproducibility and introduces unpredictable changes

### Image Naming and Tags

Docker image naming conventions and tagging strategies

**Keywords:** tag, naming, registry, version, semantic

#### Tag Image with Domain Registry

```dockerfile
FROM nginx:1.25
COPY index.html /usr/share/nginx/html/
```

_exec_
```bash
$ 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...
```

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

- Format: [registry]/repository:tag
- gcr.io = Google Container Registry
- registry.example.com for self-hosted registries

#### Multiple Tags for Single Image

```dockerfile
FROM node:18-alpine
COPY app.js .
CMD ["node", "app.js"]
```

_exec_
```bash
$ 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
```

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

- 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

**Best practices:**

- Use semantic versioning (major.minor.patch)
- Always tag with version number, not just latest
- Maintain stable/latest tags for quick releases
- Use registry domain for private images

**Common errors:**

- **Using only latest tag**: Makes it impossible to rollback to previous versions

### Multi-Stage Dockerfile Overview

Introduction to multi-stage builds for optimized image sizes

**Keywords:** multi-stage, builder, production, size, optimization

#### Single Stage vs Multi-Stage Comparison

```dockerfile
# Inefficient single stage
FROM golang:1.21
WORKDIR /src
COPY . .
RUN go build -o /usr/local/bin/app .
ENTRYPOINT ["app"]
```

_exec_
```bash
$ docker build -t app:single .
Successfully built a2b8f4d7c3e1

$ docker images app:single
REPOSITORY   TAG       IMAGE ID       SIZE
app          single    a2b8f4d7c3e1   1.3GB
```

A single stage keeps the build tools in the image, so the result is large and carries dependencies the application never uses.

- 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

```dockerfile
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"]
```

_exec_
```bash
$ 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
```

Multi-stage builds separate compilation from runtime, so the final image holds 15MB instead of 1.3GB.

- builder stage discarded after build
- Only Alpine runtime included in final image
- 1.3GB -> 15MB reduction (1000x smaller)

**Best practices:**

- Use multi-stage builds for compiled languages (Go, Rust, Java)
- Name stages meaningfully (builder, dependencies, runtime)
- Copy only artifacts needed in final stage from builder
- Reduces security attack surface with minimal dependencies

**Common errors:**

- **Forgetting FROM in builder stage**: Each stage needs its own FROM

## Copying & Adding Files

### COPY - Basic File Operations

Copy files and directories from build context to container

**Keywords:** COPY, files, directories, context, source, destination

#### Copy Single File

```dockerfile
FROM nginx:1.25-alpine
COPY index.html /usr/share/nginx/html/
```

_exec_
```bash
$ 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
```

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

- 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

```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY *.py ./
COPY requirements.txt .
```

_exec_
```bash
$ 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
```

Use wildcards to copy multiple files matching pattern into container.

- Wildcards: * matches any characters, ? matches single character
- COPY *.py ./ copies all Python files to current directory

#### Copy Directory Recursively

```dockerfile
FROM node:18-alpine
WORKDIR /app
COPY src ./src
COPY public ./public
```

_exec_
```bash
$ docker build -t node-app:latest .
Successfully built 9d5e3f2a8c1b
```

Copy entire directories with all subdirectories and files.

- Directories copied recursively by default
- Preserves directory structure in container

**Best practices:**

- COPY files after FROM but before RUN to use the cache
- Use specific paths instead of COPY . . to reduce cache invalidation
- Place stable files (dependencies) before frequently changing files

**Common errors:**

- **Path not found**: Verify file exists relative to build context
- **Permission denied**: Original file permissions preserved

### COPY with Ownership

Copy files and set ownership to non-root user

**Keywords:** COPY, CHOWN, user, ownership, permissions

#### Copy with User Ownership

```dockerfile
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"]
```

_exec_
```bash
$ docker build -t secure-app:latest .
Successfully built 7c3e5f1a2d8b

$ docker run secure-app:latest
# App runs as nodejs user, not root
```

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

- Format: COPY --chown=user:group source dest
- Prevents running container as root
- User/group must exist in image

#### Copy with Numeric User ID

```dockerfile
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"]
```

_exec_
```bash
$ docker build -t app:latest .
Successfully built 4b2f7d9a1c5e
```

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

- Numeric IDs are more reliable than names
- 1000:1000 common for non-root user

**Best practices:**

- Always use --chown to run as non-root in production
- Combine with USER directive for consistent security
- Create dedicated user with no shell for security

**Common errors:**

- **chown: user not found: User must be created before COPY --chown**: undefined

### ADD - Advanced File Operations

Add files, directories, or remote URLs with automatic extraction

**Keywords:** ADD, files, URL, tar, extraction, automatic

#### ADD from Remote URL

```dockerfile
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
```

_exec_
```bash
$ 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
```

Download files from URLs and automatically extract tar archives.

- Automatically extracts .tar.* files
- Downloads happen during build
- Should be followed by cleanup (rm) to remove archive

#### ADD Automatically Extracts TAR

```dockerfile
FROM alpine:3.18
ADD https://releases.example.com/v1.2.3/app.tar.gz /opt/
WORKDIR /opt
RUN ls -la
```

_exec_
```bash
$ 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
```

TAR archive automatically extracted to destination directory.

- Only extracts recognized tar formats (.tar.gz, .tar.bz2, .tar)
- Regular files copied as-is

**Best practices:**

- Prefer COPY over ADD for local files (more explicit)
- Use ADD only for remote URLs or tar extraction
- Always clean up extracted archives to reduce layer size

**Common errors:**

- **File not extracted**: Only tar archives are auto-extracted
- **Large layer size**: Remember to rm archive after extraction

## Running Commands

### RUN - Basic Command Execution

Execute commands during image build

**Keywords:** RUN, command, shell, build, dependencies

#### RUN Single Command

```dockerfile
FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y curl
```

_exec_
```bash
$ 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
```

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

- 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

```dockerfile
FROM ubuntu:20.04
RUN apt-get update && \
    apt-get install -y curl git vim && \
    apt-get clean
```

_exec_
```bash
$ docker build -t app:latest .
Successfully built 9d5e3f2a8c1b

$ docker history app:latest
IMAGE              CREATED        SIZE
9d5e3f2a8c1b       30 seconds ago 187MB
```

Chain multiple commands with && to create a single layer and keep the image smaller.

- && runs the next command only if the previous one succeeded
- Backslash continues line in Dockerfile
- apt-get clean removes package cache

#### RUN Install Multiple Packages

```dockerfile
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/*
```

_exec_
```bash
$ 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
```

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

- apt-get clean removes cache (reduces layer size)
- rm -rf /var/lib/apt/lists/* also reduces layer

**Best practices:**

- Chain commands with && to minimize layers
- Clean package managers (apt-get clean, apk cache)
- Use specific package versions for reproducibility
- Remove build dependencies in same RUN to save space

**Common errors:**

- **Command not found errors**: apt-get update must run before install
- **Large layers**: Each RUN instruction creates new layer

### RUN - Exec Form vs Shell Form

Differences between the exec form and the shell form of RUN

**Keywords:** RUN, exec, shell, form, JSON, signal

#### Shell Form (Default)

```dockerfile
FROM alpine:3.18
RUN echo "Building application"
RUN echo $PATH
```

_exec_
```bash
$ 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
```

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

- RUN command will use /bin/sh -c
- Environment variables expanded
- Pipes and redirects work

#### Exec Form (JSON Array)

```dockerfile
FROM alpine:3.18
RUN ["apk", "add", "--no-cache", "curl"]
RUN ["echo", "Building"]
```

_exec_
```bash
$ 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
```

Exec form calls command directly without shell interpretation.

- No shell expansion or variable interpolation
- Command executed directly (better for signals)
- Each array element becomes separate argument

#### Shell Parameter in RUN

```dockerfile
FROM ubuntu:20.04
SHELL ["/bin/bash", "-c"]
RUN for i in 1 2 3; do echo "Number: $i"; done
```

_exec_
```bash
$ 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
```

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

- SHELL instruction affects subsequent instructions
- Useful for bash-specific syntax (loops, pipes, etc)

**Best practices:**

- Use shell form for complex commands with pipes and variables
- Use exec form for simple commands (more explicit)
- Specify SHELL if using bash features
- Remember exec form needs array syntax

**Common errors:**

- **Unknown escape sequence**: Shell form escapes differ from exec form
- **Variables not expanded**: Exec form doesn't expand $VAR

## Environment & Variables

### ENV - Environment Variables

Set environment variables that persist in running containers

**Keywords:** ENV, environment, variables, persistence, containers

#### Set Single Environment Variable

```dockerfile
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1
ENV APP_ENV=production
RUN echo "Environment: $APP_ENV"
```

_exec_
```bash
$ 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
```

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

- Variables persist in running containers
- Available in all subsequent build steps
- Can be overridden at runtime with -e flag

#### Multiple Variables and Defaults

```dockerfile
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"
```

_exec_
```bash
$ 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
```

Set multiple environment variables with backslash continuation.

- Multiple ENV values on single line with backslash
- All variables available in container at runtime

#### Variable Substitution in Dockerfile

```dockerfile
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}"
```

_exec_
```bash
$ 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
```

Use variables in subsequent ENV declarations and RUN instructions.

- ${VAR} syntax substitutes variable values
- Variables only available from that line onward

**Best practices:**

- Set PYTHONUNBUFFERED=1 for Python apps
- Set NODE_ENV for Node.js apps
- Use ENV for default values, ARG for build-time arguments
- Document purpose of environment variables

**Common errors:**

- **Variable not set in container**: Used ARG instead of ENV
- **Variable undefined**: Referenced before being set

### ARG - Build Arguments

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

**Keywords:** ARG, arguments, build-time, variable, substitution

#### ARG for Build-Time Configuration

```dockerfile
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"
```

_exec_
```bash
$ 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
```

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

- Build arguments only available during build
- Not included in final image
- Use ENV to persist values

#### ARG with Default Values

```dockerfile
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"
```

_exec_
```bash
$ 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
```

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

- Defaults used if no --build-arg provided
- Can be overridden per build

**Best practices:**

- Use ARG for build-time configuration
- Use ENV for runtime configuration
- Document all ARG values
- Provide sensible defaults

**Common errors:**

- **ARG not available at runtime**: Use ENV instead to persist values
- **Build argument not working**: Use --build-arg flag (not -e)

### Variable Substitution and Defaults

Advanced variable substitution patterns in Dockerfile

**Keywords:** variables, substitution, defaults, expansion, patterns

#### Variable Expansion with Defaults

```dockerfile
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}
```

_exec_
```bash
$ 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
```

Use variables in FROM and subsequent instructions for flexible builds.

- ARG before FROM available in FROM instruction
- Base image variant changes based on ARG

#### Build-Stage Variables

```dockerfile
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}
```

_exec_
```bash
$ 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 declared at different stages for multi-stage build flexibility.

- ARG before FROM is global
- ARG in stage is local to that stage
- Each stage can redefine ARG

**Best practices:**

- Use ARG for all configurable build parameters
- Provide clear defaults
- Document all variable names and purposes
- Use semantic versioning for VERSION

**Common errors:**

- **Variable undefined in FROM**: Declare ARG before FROM
- **Unexpected substitution: ${} syntax required**: undefined

## Working Directory & Volumes

### WORKDIR - Working Directory

Set working directory for subsequent instructions

**Keywords:** WORKDIR, directory, path, cd, working

#### WORKDIR Basic Usage

```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN ls -la
```

_exec_
```bash
$ 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
```

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

- Creates directory if it doesn't exist
- Subsequent COPY/ADD use this as destination
- RUN commands execute in this directory

#### Multiple WORKDIR Instructions

```dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package.json .
RUN npm install
WORKDIR /app/src
COPY src .
```

_exec_
```bash
$ docker build -t app:latest .
Successfully built 5c2f8e1a3d7b
```

Multiple WORKDIR instructions change directory progressively.

- Each WORKDIR is cumulative (relative paths work)
- /app and /app/src are different working directories

#### WORKDIR with Variables

```dockerfile
FROM ubuntu:20.04
ARG APP_HOME=/usr/local/app
ENV APP_HOME=${APP_HOME}
WORKDIR ${APP_HOME}
RUN echo "Application home: $(pwd)"
```

_exec_
```bash
$ 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
```

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

- Variables interpolated at build time

**Best practices:**

- Always set WORKDIR explicitly (default is /)
- Use absolute paths for clarity
- Keep paths consistent across team
- Use WORKDIR with CHOWN for proper ownership

**Common errors:**

- **No such file or directory**: WORKDIR doesn't exist for COPY in some images

### VOLUME - Data Persistence Points

Define mount points for persistent data

**Keywords:** VOLUME, mount, persistent, data, storage

#### VOLUME for Database Storage

```dockerfile
FROM postgres:15-alpine
VOLUME ["/var/lib/postgresql/data"]
EXPOSE 5432
CMD ["postgres"]
```

_exec_
```bash
$ 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"...}]
```

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

- Creates anonymous volume if not specified
- Data persists even if container is deleted
- Can be mounted by other containers

#### Multiple Volumes

```dockerfile
FROM mysql:8.0
VOLUME ["/var/lib/mysql", "/var/log/mysql"]
EXPOSE 3306
ENV MYSQL_ROOT_PASSWORD=secret
CMD ["mysqld"]
```

_exec_
```bash
$ docker run -d mysql-custom:latest
5f9d3a2c8e1b

$ docker volume ls | grep mysql
volumes     abc123def456        (database files)
volumes     def789ghi012        (log files)
```

Multiple VOLUME instructions create separate mount points for different data.

- Each VOLUME is independent
- Can mount to named volumes with -v flag

**Best practices:**

- Document what each volume is for
- Use named volumes in production (not anonymous)
- Consider backup strategy for volumes
- Mount volumes explicitly at runtime

**Common errors:**

- **Volume not persisting**: Use -v flag to mount volume at runtime

## Exposing Ports

### EXPOSE - Port Declarations

Document which ports the application listens on

**Keywords:** EXPOSE, port, network, listening, documentation

#### EXPOSE Single Port

```dockerfile
FROM nginx:1.25-alpine
EXPOSE 80
EXPOSE 443
COPY index.html /usr/share/nginx/html/
```

_exec_
```bash
$ docker build -t web-server:latest .
Successfully built 5c2f8e1a3d7b

$ docker inspect web-server:latest --format='{{json .ExposedPorts}}'
{"80/tcp":{},"443/tcp":{}}
```

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

- EXPOSE is declarative and informational
- Does NOT actually publish ports
- Use -p flag at runtime to publish

#### EXPOSE Multiple Ports

```dockerfile
FROM node:18-alpine
EXPOSE 3000 3001 9229
COPY . .
RUN npm install
CMD ["npm", "start"]
```

_exec_
```bash
$ docker build -t node-app:latest .
Successfully built 7c9e4f2b3a5d
```

Multiple EXPOSE declarations for application and debug ports.

- 3000: application port
- 3001: alternative service
- 9229: Node.js debugger port

#### EXPOSE with Port Range

```dockerfile
FROM ubuntu:20.04
EXPOSE 8000-8100
RUN apt-get update && apt-get install -y netcat
CMD ["nc", "-l", "-p", "8000"]
```

_exec_
```bash
$ docker build -t range-app:latest .
Successfully built 8b4f3e2c9a1d

$ docker run -p 8000-8100:8000-8100 range-app:latest
```

EXPOSE range of ports for applications using dynamic port allocation.

- Useful for services with multiple instances
- Port range notation: START-END

**Best practices:**

- Document all ports application uses
- Include both HTTP and debugging ports
- Remember EXPOSE is informational only
- Publish ports at runtime with -p flag

**Common errors:**

- **Port not accessible**: Need -p flag at runtime to publish
- **Wrong port mapping**: EXPOSE doesn't map, use -p

## Entry Points & Commands

### CMD - Default Command

Specify default command to run when container starts

**Keywords:** CMD, command, default, startup, execution

#### CMD Shell Form

```dockerfile
FROM python:3.11-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD python app.py
```

_exec_
```bash
$ docker build -t python-app:latest .
Successfully built 5c2f8e1a3d7b

$ docker run python-app:latest
Starting application...
App is running
```

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

- Runs via /bin/sh -c
- Can use environment variables
- Same as typing command in shell

#### CMD Exec Form (JSON)

```dockerfile
FROM node:18-alpine
COPY . /app
WORKDIR /app
RUN npm install
CMD ["node", "server.js"]
```

_exec_
```bash
$ docker build -t node-app:latest .
Successfully built 7c9e4f2b3a5d

$ docker run node-app:latest
Server listening on port 3000
```

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

- No shell interpretation
- Each element becomes separate argument
- Signals (SIGTERM) properly delivered

#### Override CMD at Runtime

```dockerfile
FROM python:3.11-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
```

_exec_
```bash
$ 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
```

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

- Container image has default CMD
- Passing command at runtime overrides it

**Best practices:**

- Use exec form for main application command
- Only one CMD in Dockerfile (last one wins)
- Make commands interruptible (handle SIGTERM)

**Common errors:**

- **Command not executing**: Need exec form for proper signal handling
- **Multiple CMDs**: Only last CMD is used

### ENTRYPOINT - Entry Point

Configure container as executable with ENTRYPOINT

**Keywords:** ENTRYPOINT, entry, point, wrapper, executable

#### ENTRYPOINT with Exec Form

```dockerfile
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"]
```

_exec_
```bash
$ 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 with exec form makes container behave as executable.

- ENTRYPOINT is the wrapper script
- CMD provides default arguments
- Useful for setup before main app

#### ENTRYPOINT with Shell Script

```dockerfile
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"]
```

_exec_
```bash
$ 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
```

ENTRYPOINT executes shell script as main process.

- Script runs as PID 1 in container
- Must handle signals properly
- Can accept arguments from docker run

#### ENTRYPOINT with CMD

```dockerfile
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y curl tini
COPY . /app
WORKDIR /app
ENTRYPOINT ["tini", "--"]
CMD ["./app", "start"]
```

_exec_
```bash
$ docker build -t tini-app:latest .
Successfully built 8b4f3e2c9a1d

$ docker run tini-app:latest
Starting app with tini init system...
```

ENTRYPOINT + CMD combination where tini manages signals properly.

- tini is lightweight init system for containers
- Forwards signals to the application and reaps zombie processes
- CMD provides default arguments to ENTRYPOINT

**Best practices:**

- Use ENTRYPOINT for wrapper scripts
- Combine with CMD for default arguments
- Make wrapper scripts handle SIGTERM
- Consider using tini for signal management

**Common errors:**

- **Script not executable**: Remember chmod +x in Dockerfile
- **Signals not working**: Exec form required, not shell form

### CMD and ENTRYPOINT Interaction

How CMD and ENTRYPOINT work together

**Keywords:** CMD, ENTRYPOINT, interaction, override, arguments

#### ENTRYPOINT as Wrapper with CMD Arguments

```dockerfile
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"]
```

_exec_
```bash
$ 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
```

ENTRYPOINT and CMD together allow flexible command execution with wrapper.

- CMD becomes arguments to ENTRYPOINT
- Can override CMD at runtime
- Wrapper executes first, then CMD

#### Pure ENTRYPOINT (No CMD)

```dockerfile
FROM golang:1.21-alpine
WORKDIR /src
COPY . .
RUN go build -o /app .
ENTRYPOINT ["/app"]
```

_exec_
```bash
$ docker build -t go-app:latest .
Successfully built 7c9e4f2b3a5d

$ docker run go-app:latest --help
Usage: app [OPTIONS]
```

ENTRYPOINT alone without CMD for single-purpose containers.

- Container is tool-like (always runs binary)
- Can still pass arguments
- No default command

**Best practices:**

- Use ENTRYPOINT primarily for wrapper/setup
- Use CMD for default arguments
- Make both overridable for flexibility
- Document expected arguments

**Common errors:**

- **Cannot override**: Use exec form for both
- **Arguments not passed**: Arrays must use exec form

## Build Optimization

### Layer Caching Strategy

Docker layer caching and how it shortens rebuilds

**Keywords:** layers, caching, efficiency, build, speed

#### Inefficient Layer Caching

```dockerfile
FROM python:3.11-slim
COPY . .
RUN pip install -r requirements.txt
RUN python app.py
```

_exec_
```bash
$ 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)
```

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

- COPY . . invalidates all subsequent layers on any change
- Python packages reinstalled unnecessarily

#### Optimized Layer Caching

```dockerfile
FROM python:3.11-slim
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
```

_exec_
```bash
$ 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
```

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

- Stable: requirements.txt, package.json rarely change
- Volatile: app.py, src files change frequently
- Skips dependency installation when only source files change

#### Cache with External Mounts

```dockerfile
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
```

_exec_
```bash
$ 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
```

Cache mounts preserve package manager caches across builds.

- npm/yarn/pip cache reused
- Requires BuildKit (DOCKER_BUILDKIT=1)
- Reuses downloaded packages instead of fetching them again

**Best practices:**

- Order instructions by change frequency
- Keep stable layers early in Dockerfile
- Use cache mounts for package managers
- Don't invalidate cache unnecessarily

**Common errors:**

- **Slow builds**: Dependencies reinstalled every time
- **Cache not used**: Checking intermediate layers

### .dockerignore File

Exclude files from build context to speed up builds

**Keywords:** dockerignore, context, exclude, efficiency

#### Basic .dockerignore

```dockerfile
# .dockerignore content
.git
.gitignore
.github
node_modules
.env
.env.local
dist
build
__pycache__
*.log
.DS_Store
.vscode
.idea
npm-debug.log
.npm
coverage
```

_exec_
```bash
# Build context without excluded files
$ docker build --progress=plain -t app:latest .

# File sizes comparison:
# Without .dockerignore: 248MB context sent to daemon
# With .dockerignore: 42MB context sent to daemon (82% reduction)
```

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

- Reduces the build context size
- Speeds up build (less data to transfer)
- Similar to .gitignore syntax

#### Comprehensive .dockerignore

```dockerfile
# .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
```

_exec_
```bash
$ docker build -t app:latest .
# Context drastically reduced
```

A thorough .dockerignore excludes all unnecessary files from context.

- Reduces build context from 500MB to 11MB
- Sends less data to the daemon, so docker build starts faster

**Best practices:**

- Always use .dockerignore for production
- Exclude large directories (node_modules, venv, .git)
- Exclude temporary and log files
- Review before each major change

**Common errors:**

- **Large build context**: Missing .dockerignore file

### Multi-Stage Build Optimization

Advanced multi-stage patterns for minimal images

**Keywords:** multi-stage, builder, optimization, size, artifacts

#### Multi-Stage Node.js Build

```dockerfile
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"]
```

_exec_
```bash
$ 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
```

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

- Builder stage includes all dependencies
- Final stage has only production dependencies
- Size reduction: devDependencies excluded

#### Multi-Stage Rust Build

```dockerfile
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"]
```

_exec_
```bash
$ 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
```

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

- Builder: rust:1.75 (2GB with compiler)
- Final: debian:12-slim + binary (45MB total)
- 2GB -> 45MB reduction!

**Best practices:**

- Use multi-stage for any compiled language
- Name stages meaningfully (builder, dependencies, etc)
- Clean up artifacts in builder
- Copy only necessary files to final stage

**Common errors:**

- **Builder stage not discarded**: Forgotten FROM in later stage
- **Large final image**: Copying unnecessary files from builder

## Metadata & Advanced

### LABEL - Image Metadata

Add metadata labels to Docker images

**Keywords:** LABEL, metadata, version, maintainer, description

#### Common Docker Labels

```dockerfile
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"
```

_exec_
```bash
$ 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"
}
```

LABEL adds metadata to image for documentation and organization.

- Labels appear in docker inspect output
- Useful for filtering and organizing images
- No size impact

#### Multi-line Labels

```dockerfile
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"
```

_exec_
```bash
$ docker build -t web-server:2.1.0 .
Successfully built 5c2f8e1a3d7b
```

Multiple labels using backslash continuation for readability.

- Backslash extends single instruction across lines
- Each label can be queried independently

**Best practices:**

- Use OpenContainers standard labels
- Include maintainer contact information
- Document version and source repo
- Use consistent naming conventions

**Common errors:**

- **Labels not visible**: Remember labels are in metadata, not filesystem

### USER - Container User

Specify user to run container as instead of root

**Keywords:** USER, security, non-root, permissions, uid

#### Run as Created User

```dockerfile
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"]
```

_exec_
```bash
$ 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)
```

Create dedicated user and switch to it before running application.

- Root user is dangerous in containers
- appuser has no shell (better security)
- Works with CMD/ENTRYPOINT

#### Run as Numeric UID

```dockerfile
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"]
```

_exec_
```bash
$ docker build -t app:latest .
Successfully built 7c9e4f2b3a5d

$ docker run app:latest whoami
# Returns: 1001 (numeric ID)
```

Use numeric UID instead of username for better portability.

- 1001 common for first non-system user
- More portable across systems

**Best practices:**

- Always run as non-root in production
- Create user with no shell (/usr/sbin/nologin)
- Set appropriate file permissions
- Use USER before ENTRYPOINT/CMD

**Common errors:**

- **Permission denied**: Files owned by root, not accessible by app user
- **Running as root**: Forgot USER instruction

### HEALTHCHECK - Container Health

Define health check to determine if container is running properly

**Keywords:** HEALTHCHECK, health, check, cmd, interval

#### HTTP Endpoint Health Check

```dockerfile
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/
```

_exec_
```bash
$ 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
```

Check container health by making HTTP request to application.

- 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

```dockerfile
FROM postgres:15-alpine
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=5 \
  CMD ["pg_isready", "-U", "postgres"]
```

_exec_
```bash
$ 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)
```

Use application-specific tools for health checks.

- pg_isready for PostgreSQL
- redis-cli PING for Redis
- mysql -e "SELECT 1" for MySQL

#### Script-Based Health Check

```dockerfile
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"]
```

_exec_
```bash
$ docker build -t app:latest .
Successfully built 8b4f3e2c9a1d
```

Custom script for complex health checks.

- Exit code 0 = healthy
- Exit code 1 = unhealthy
- Script can hit any endpoint

**Best practices:**

- Always define HEALTHCHECK in Dockerfile
- Keep checks lightweight and fast
- Use appropriate intervals for application
- Test health checks locally first

**Common errors:**

- **curl: command not found: Install curl in image**: undefined
- **Health check never runs**: Remember interval in seconds

### Dockerfile Shell Form

Control shell behavior with

**Keywords:** directive, shell, syntax, escape

#### Use Bash Instead of /bin/sh

```dockerfile
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
```

_exec_
```bash
$ 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
```

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

- 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

```dockerfile
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
```

_exec_
```bash
$ 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
```

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

- Available in Docker 23.09+
- Cleaner than backslash continuation
- Each line individual command (not chained)

**Best practices:**

- Use bash for complex scripts on Linux
- Document shell requirements
- Remember Alpine has different shells
- Convert CRLF line endings to LF for scripts copied into Linux images

**Common errors:**

- **Command not found**: Bash not in alpine:latest
- **Syntax error**: Mix of sh and bash syntax
