TLDR;

  • Dockerfile syntax explained with examples
  • To the point no fluff article

1. Dockerfile Fundamentals

1.1 What Is a Dockerfile?

A Dockerfile is a text file with instructions to build a Docker image. docs.docker

Each instruction creates a layer in the final image.

Written in a specific DSL (Domain Specific Language) with commands in UPPERCASE.

1
2
3
Dockerfile → docker build → Docker Image → docker run → Container

Text file    Build process   Template        Run process   Running app

1.2 Basic Structure

1
2
3
4
5
6
7
8
# Comments start with #
INSTRUCTION arguments

# Example:
FROM python:3.13
RUN apt-get update
COPY app.py /app/
CMD ["python", "/app/app.py"]

2. Dockerfile Instructions Reference

2.1 FROM - Base Image

Syntax:

1
FROM <image>[:<tag>] [AS <name>]

What it does:

  • Specifies the base image to start from
  • Must be the first instruction (except ARG before FROM)
  • Every Dockerfile starts with FROM

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Use official Python image
FROM python:3.13

# Use slim variant
FROM python:3.13-slim

# Use Alpine Linux (smallest)
FROM python:3.13-alpine

# Use specific version
FROM python:3.13.1

# Named stage for multi-stage builds
FROM python:3.13 AS builder

# Use Ubuntu base
FROM ubuntu:22.04

Common base images:

1
2
3
4
5
python:3.13           → 1.2GB (includes everything)
python:3.13-slim      → 150MB (minimal Debian)
python:3.13-alpine    → 50MB  (tiny but compatibility issues)
ubuntu:22.04          → 77MB  (general purpose)
debian:bookworm-slim  → 74MB  (Debian minimal)

2.2 RUN - Execute Commands

Syntax:

1
2
3
4
5
# Shell form (runs in /bin/sh -c)
RUN <command>

# Exec form (runs directly)
RUN ["executable", "param1", "param2"]

What it does:

  • Executes commands during image build
  • Creates a new layer
  • Used to install packages, configure environment

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Install packages (shell form)
RUN apt-get update && apt-get install -y curl

# Multiple commands with line continuation
RUN apt-get update && \
    apt-get install -y \
        curl \
        vim \
        git && \
    rm -rf /var/lib/apt/lists/*

# Run Python script
RUN python setup.py install

# Exec form (more explicit)
RUN ["/bin/bash", "-c", "echo hello"]

Best practices:

1
2
3
4
5
6
7
8
9
# ❌ BAD: Each RUN creates a new layer
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim

# ✅ GOOD: Combine into single layer
RUN apt-get update && \
    apt-get install -y curl vim && \
    rm -rf /var/lib/apt/lists/*

2.3 COPY - Copy Files

Syntax:

1
2
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]

What it does:

  • Copies files from build context (local machine) to image
  • Simple, predictable file copying

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Copy single file
COPY app.py /app/app.py

# Copy to directory (creates if doesn't exist)
COPY app.py /app/

# Copy multiple files
COPY app.py config.py /app/

# Copy entire directory
COPY src/ /app/src/

# Copy everything (respects .dockerignore)
COPY . /app/

# Copy with ownership
COPY --chown=user:group file.txt /app/

# Copy multiple sources
COPY file1.txt file2.txt /app/

Wildcards:

1
2
3
4
5
# Copy all Python files
COPY *.py /app/

# Copy files matching pattern
COPY requirements*.txt /app/

2.4 ADD - Advanced Copy

Syntax:

1
ADD <src>... <dest>

What it does:

  • Like COPY but with extra features:
    • Auto-extracts tar archives
    • Can download from URLs

Examples:

1
2
3
4
5
6
7
8
# Extract tar file automatically
ADD archive.tar.gz /app/

# Download from URL (not recommended)
ADD https://example.com/file.tar.gz /tmp/

# Regular file copy (same as COPY)
ADD app.py /app/

When to use:

1
2
3
4
5
# ✅ Use ADD for: Auto-extracting archives
ADD package.tar.gz /opt/

# ❌ Use COPY for: Everything else
COPY app.py /app/

2.5 WORKDIR - Set Working Directory

Syntax:

1
WORKDIR /path/to/directory

What it does:

  • Sets the working directory for subsequent instructions
  • Creates directory if it doesn’t exist
  • Like cd in shell but persistent

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Set working directory
WORKDIR /app

# All subsequent commands run in /app
COPY . .          # Copies to /app/
RUN ls            # Lists /app/

# Nested directories
WORKDIR /app/src/modules
# Now in /app/src/modules

# Relative paths
WORKDIR /app
WORKDIR src       # Now in /app/src

Best practice:

1
2
3
4
5
6
# ❌ BAD: Using cd in RUN
RUN cd /app && python setup.py install

# ✅ GOOD: Using WORKDIR
WORKDIR /app
RUN python setup.py install

2.6 ENV - Environment Variables

Syntax:

1
2
ENV <key>=<value> ...
ENV <key> <value>

What it does:

  • Sets environment variables
  • Available during build AND runtime
  • Can be overridden at runtime

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Single variable
ENV APP_VERSION=1.0.0

# Multiple variables
ENV APP_ENV=production \
    DEBUG=false \
    PORT=8000

# Old syntax (deprecated)
ENV APP_NAME myapp

# Using in subsequent commands
ENV DATA_DIR=/data
WORKDIR $DATA_DIR

Common patterns:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Python environment
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1

# Path modification
ENV PATH="/app/.venv/bin:$PATH"

# Application config
ENV DATABASE_URL=postgresql://localhost/db \
    REDIS_URL=redis://localhost:6379

2.7 ARG - Build Arguments

Syntax:

1
ARG <name>[=<default value>]

What it does:

  • Defines build-time variables
  • Only available during build, not at runtime
  • Can be passed with docker build --build-arg

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Define with default
ARG PYTHON_VERSION=3.13

# Use in FROM
FROM python:${PYTHON_VERSION}

# Define without default
ARG BUILD_ENV

# Use in RUN
RUN echo "Building for ${BUILD_ENV}"

# Multiple args
ARG USER_ID=1000
ARG USER_NAME=appuser
RUN useradd -u ${USER_ID} ${USER_NAME}

Usage:

1
2
3
# Build with custom argument
docker build --build-arg PYTHON_VERSION=3.12 -t myapp .
docker build --build-arg BUILD_ENV=staging -t myapp .

ARG vs ENV:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# ARG: Build-time only
ARG BUILD_DATE
RUN echo "Built on ${BUILD_DATE}" > /build-info.txt

# ENV: Build + Runtime
ENV APP_VERSION=1.0
RUN echo "Version ${APP_VERSION}"  # Available
CMD echo "Version ${APP_VERSION}"  # Available

# ARG not available at runtime
ARG TEMP_VAR
CMD echo "${TEMP_VAR}"  # Empty!

2.8 CMD - Default Command

Syntax:

1
2
3
4
5
6
7
8
# Exec form (preferred)
CMD ["executable", "param1", "param2"]

# Shell form
CMD command param1 param2

# As parameters to ENTRYPOINT
CMD ["param1", "param2"]

What it does:

  • Specifies default command to run when container starts
  • Only one CMD per Dockerfile (last one wins)
  • Can be overridden at runtime

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Run Python script (exec form)
CMD ["python", "app.py"]

# Run with arguments
CMD ["python", "app.py", "--port", "8000"]

# Shell form (runs in /bin/sh -c)
CMD python app.py

# Interactive shell
CMD ["/bin/bash"]

Override at runtime:

1
2
3
4
5
6
7
8
9
# Dockerfile has: CMD ["python", "app.py"]

# Use default
docker run myapp
# Runs: python app.py

# Override
docker run myapp python test.py
# Runs: python test.py

2.9 ENTRYPOINT - Fixed Command

Syntax:

1
2
3
4
5
# Exec form (preferred)
ENTRYPOINT ["executable", "param1", "param2"]

# Shell form
ENTRYPOINT command param1 param2

What it does:

  • Configures container to run as an executable
  • Cannot be easily overridden (more fixed than CMD)
  • Often used with CMD for default arguments

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Fixed command
ENTRYPOINT ["python", "app.py"]

# Container always runs Python script
# docker run myapp → python app.py
# docker run myapp --help → python app.py --help

# With CMD for defaults
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8000"]
# docker run myapp → python app.py --port 8000
# docker run myapp --port 9000 → python app.py --port 9000

ENTRYPOINT vs CMD:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Pattern 1: CMD only (fully replaceable)
CMD ["python", "app.py"]
# docker run myapp → python app.py
# docker run myapp bash → bash

# Pattern 2: ENTRYPOINT only (fixed command)
ENTRYPOINT ["python", "app.py"]
# docker run myapp → python app.py
# docker run myapp bash → python app.py bash (wrong!)

# Pattern 3: Both (best flexibility)
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run myapp → python app.py
# docker run myapp test.py → python test.py

2.10 EXPOSE - Document Ports

Syntax:

1
EXPOSE <port> [<port>/<protocol>...]

What it does:

  • Documents which ports the application listens on
  • Does NOT actually publish the port
  • Metadata only

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Single port
EXPOSE 8000

# Multiple ports
EXPOSE 8000 8080

# With protocol
EXPOSE 80/tcp
EXPOSE 53/udp

# Range
EXPOSE 8000-8010

Usage:

1
2
3
4
5
6
# Dockerfile has: EXPOSE 8000

# Still need -p to publish
docker run -p 8000:8000 myapp  # Maps port

# EXPOSE just documents the intent

2.11 VOLUME - Mount Points

Syntax:

1
2
VOLUME ["/path/to/volume"]
VOLUME /path/to/volume

What it does:

  • Declares a mount point for external volumes
  • Data persists outside container
  • Can be overridden at runtime

Examples:

1
2
3
4
5
6
7
8
9
# Single volume
VOLUME /data

# Multiple volumes
VOLUME ["/data", "/logs"]

# Application example
VOLUME /app/uploads
VOLUME /var/log/myapp

Usage:

1
2
3
4
5
6
7
8
# Dockerfile has: VOLUME /data

# Docker creates anonymous volume
docker run myapp
# Data in /data persists in Docker volume

# Mount specific directory
docker run -v /host/data:/data myapp

2.12 USER - Change User

Syntax:

1
2
USER <user>[:<group>]
USER <UID>[:<GID>]

What it does:

  • Sets the user for subsequent commands
  • Security best practice (don’t run as root)

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Create user first
RUN useradd -m appuser

# Switch to user
USER appuser

# All subsequent commands run as appuser
RUN whoami  # Returns: appuser

# With group
USER appuser:appgroup

# Using UID:GID
USER 1000:1000

Complete pattern:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Create non-root user
RUN groupadd -g 1001 appuser && \
    useradd -u 1001 -g appuser -m -d /app appuser

# Set ownership
COPY --chown=appuser:appuser . /app/

# Switch to user
USER appuser

# Application runs as appuser
CMD ["python", "app.py"]

2.13 LABEL - Metadata

Syntax:

1
LABEL <key>=<value> <key>=<value> ...

What it does:

  • Adds metadata to image
  • Used for documentation, version tracking

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Single label
LABEL version="1.0"

# Multiple labels
LABEL maintainer="[email protected]" \
      description="My application" \
      version="1.0.0"

# Organization labels
LABEL org.opencontainers.image.authors="your-name" \
      org.opencontainers.image.version="1.0.0" \
      org.opencontainers.image.created="2026-02-13"

2.14 HEALTHCHECK - Container Health

Syntax:

1
2
HEALTHCHECK [OPTIONS] CMD command
HEALTHCHECK NONE  # Disable health check

What it does:

  • Tells Docker how to test if container is healthy
  • Runs periodically

Options:

1
2
3
4
--interval=DURATION (default: 30s)
--timeout=DURATION (default: 30s)
--start-period=DURATION (default: 0s)
--retries=N (default: 3)

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Simple check
HEALTHCHECK CMD curl -f http://localhost/ || exit 1

# Full options
HEALTHCHECK --interval=30s \
            --timeout=10s \
            --start-period=5s \
            --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1

# Python script check
HEALTHCHECK CMD python -c "import requests; requests.get('http://localhost:8000/health')"

# Disable inherited health check
HEALTHCHECK NONE

3. Complete Dockerfile Example (Annotated)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# ============================================
# Stage 1: Builder
# ============================================

# 1. Start from base image with name
FROM python:3.13-slim AS builder

# 2. Set build argument with default
ARG PYTHON_VERSION=3.13

# 3. Set environment variables
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

# 4. Add metadata
LABEL maintainer="[email protected]" \
      version="1.0.0"

# 5. Set working directory
WORKDIR /app

# 6. Install system dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        gcc \
        curl && \
    rm -rf /var/lib/apt/lists/*

# 7. Copy requirements first (for caching)
COPY requirements.txt .

# 8. Install Python packages
RUN pip install --no-cache-dir -r requirements.txt

# 9. Copy application code
COPY . .

# ============================================
# Stage 2: Runtime
# ============================================

# 10. New stage with smaller base
FROM python:3.13-slim

# 11. Set environment for runtime
ENV PYTHONUNBUFFERED=1 \
    PATH="/app/.venv/bin:$PATH"

# 12. Create non-root user
RUN groupadd -g 1001 appuser && \
    useradd -u 1001 -g appuser -m -d /app appuser

# 13. Set working directory
WORKDIR /app

# 14. Copy from builder stage
COPY --from=builder --chown=appuser:appuser /app /app

# 15. Create volume mount point
VOLUME /app/data

# 16. Switch to non-root user
USER appuser

# 17. Document port
EXPOSE 8000

# 18. Add health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# 19. Set default command
ENTRYPOINT ["python"]
CMD ["app.py"]

4. Dockerfile Syntax Rules

4.1 General Rules

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 1. Instructions are CASE-INSENSITIVE but UPPERCASE is convention
from python:3.13  # Works but discouraged
FROM python:3.13  # Preferred

# 2. Comments start with #
# This is a comment
FROM python:3.13

# 3. Line continuation with backslash
RUN apt-get update && \
    apt-get install -y curl

# 4. Parser directives (must be first line)
# syntax=docker/dockerfile:1
FROM python:3.13

# 5. Whitespace doesn't matter (mostly)
FROM python:3.13
FROM   python:3.13   # Same

4.2 Shell vs Exec Form

1
2
3
4
5
6
7
8
9
# Shell form: Runs in /bin/sh -c
RUN apt-get update
CMD python app.py
ENTRYPOINT python app.py

# Exec form: Direct execution (no shell)
RUN ["apt-get", "update"]
CMD ["python", "app.py"]
ENTRYPOINT ["python", "app.py"]

Differences:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Shell form: Variable substitution works
ENV NAME=world
RUN echo "Hello $NAME"  # Prints: Hello world

# Exec form: No variable substitution
ENV NAME=world
RUN ["echo", "Hello $NAME"]  # Prints: Hello $NAME (literal)

# Exec form with shell for variables
RUN ["/bin/sh", "-c", "echo Hello $NAME"]  # Prints: Hello world

5. .dockerignore File

5.1 What Is It?

1
2
3
Like .gitignore but for Docker builds
Excludes files from build context
Speeds up builds, reduces image size

5.2 Syntax

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# .dockerignore

# Comments start with #
*.log

# Wildcard patterns
*.tmp
temp*

# Directories
node_modules/
.git/

# Exceptions (include something excluded)
!important.log

# Match any directory
**/*.pyc

5.3 Common Python .dockerignore

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# .dockerignore for Python projects
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
dist/
*.egg-info/
.venv/
venv/
.env
.git/
.gitignore
.vscode/
.idea/
*.md
tests/
.pytest_cache/
.coverage
htmlcov/

6. Build Context Explained

6.1 What Is Build Context?

1
2
3
4
5
6
7
When you run: docker build -t myapp .
                                    ^
                                    |
                        This is the build context

Build context = Directory sent to Docker daemon
               = What COPY/ADD can access

6.2 Context Structure

1
2
3
4
5
6
7
8
9
project/
├── Dockerfile         ← Build instructions
├── .dockerignore      ← Exclusions
├── app.py             ← Included in context
├── requirements.txt   ← Included
├── .git/              ← Excluded (via .dockerignore)
└── node_modules/      ← Excluded (via .dockerignore)

Build context = Everything except .dockerignore matches

7. Practice Exercises

7.1 Exercise 1: Simple Python App

Task: Create a Dockerfile for this app:

1
2
# app.py
print("Hello from Docker!")

Solution:

1
2
3
4
FROM python:3.13-slim
WORKDIR /app
COPY app.py .
CMD ["python", "app.py"]

7.2 Exercise 2: Flask Web App

Task: Dockerize a Flask app with requirements:

1
2
# requirements.txt
flask==3.0.0

Solution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
FROM python:3.13-slim

WORKDIR /app

# Copy requirements first
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy app
COPY app.py .

# Document port
EXPOSE 5000

# Run Flask
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"]

7.3 Exercise 3: Multi-Stage Build

Task: Create a multi-stage build that:

  1. Builds in stage 1
  2. Runs in smaller stage 2

Solution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Stage 1: Build
FROM python:3.13 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Stage 2: Runtime
FROM python:3.13-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY app.py .
CMD ["python", "app.py"]

8. Common Patterns

8.1 Python Web App Pattern

1
2
3
4
5
6
7
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

8.2 Development vs Production

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Use ARG to switch
ARG BUILD_ENV=production

FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .

# Install different dependencies based on environment
RUN if [ "$BUILD_ENV" = "development" ]; then \
        pip install -r requirements-dev.txt; \
    else \
        pip install -r requirements.txt; \
    fi

COPY . .

CMD ["python", "app.py"]

8.3 With Environment File

1
2
3
4
5
6
7
8
9
FROM python:3.13-slim
WORKDIR /app

# Copy and source .env
COPY .env .
RUN export $(cat .env | xargs)

COPY . .
CMD ["python", "app.py"]

9. Debugging Dockerfile

9.1 Build with Output

1
2
3
4
5
6
7
8
# Verbose build
docker build --progress=plain -t myapp .

# No cache (rebuild everything)
docker build --no-cache -t myapp .

# Stop at specific stage
docker build --target builder -t myapp:builder .

9.2 Inspect Layers

1
2
3
4
5
6
7
8
# Show image layers
docker history myapp

# Inspect image
docker inspect myapp

# Dive into image structure
dive myapp  # Install dive tool first

9.3 Debug Failed Build

1
2
3
4
5
6
7
8
# Build fails at RUN step
# Docker keeps intermediate container
# Find its ID in error output

# Run the failed container
docker run -it <container_id> /bin/bash

# Now you can debug interactively

10. Quick Reference Card

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ===== BASICS =====
FROM image:tag              # Base image
RUN command                 # Execute during build
COPY src dest               # Copy files
WORKDIR /path               # Set directory

# ===== CONFIGURATION =====
ENV KEY=value               # Environment variable
ARG KEY=value               # Build argument
EXPOSE port                 # Document port
VOLUME /path                # Mount point

# ===== EXECUTION =====
CMD ["exec", "args"]        # Default command
ENTRYPOINT ["exec"]         # Fixed command
USER username               # Switch user

# ===== METADATA =====
LABEL key=value             # Add metadata
HEALTHCHECK CMD command     # Health check

# ===== MULTI-STAGE =====
FROM base AS name           # Named stage
COPY --from=name src dest   # Copy from stage