Skip to content
beginner Phase 1 · Docker Fundamentals

Dockerfile Deep Dive

Write efficient Dockerfiles with multi-stage builds and best practices.

1h 30m
0 problems
Topic Progress 0%

Dockerfile Instructions

Dockerfile Instructions

A Dockerfile is a text file containing instructions that Docker reads sequentially to build an image. Each instruction creates a layer or sets metadata.

Core Instructions

# FROM: Base image (must be first instruction)
FROM node:20-alpine

# LABEL: Metadata
LABEL maintainer="team@company.com" \
      version="1.0"

# ENV: Environment variables (persist in image)
ENV NODE_ENV=production
ENV APP_HOME=/app

# ARG: Build-time variables (not in final image)
ARG NODE_VERSION=20

# WORKDIR: Set working directory (creates if missing)
WORKDIR $APP_HOME

# COPY: Copy files from build context to image
COPY package*.json ./
COPY --chown=node:node . .

# RUN: Execute commands during build
RUN npm ci --only=production && \
    npm cache clean --force

# EXPOSE: Document listening ports (does not publish)
EXPOSE 3000

# USER: Run as non-root user
USER node

# HEALTHCHECK: Container health monitoring
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

# CMD: Default command (can be overridden)
CMD ["node", "server.js"]

# ENTRYPOINT: Fixed entry command
ENTRYPOINT ["node"]

Instruction Priority

Docker executes instructions top-to-bottom. Order matters for caching:

  1. FROM — Choose the smallest base that meets your needs
  2. LABEL — Add metadata early
  3. ARG/ENV — Set build-time and runtime variables
  4. WORKDIR — Set the working directory
  5. COPY dependency files first, then RUN install, then COPY source
  6. RUN — Minimize layers by chaining commands
  7. EXPOSE — Document ports
  8. USER — Switch to non-root
  9. HEALTHCHECK — Define health monitoring
  10. CMD/ENTRYPOINT — Define the startup command

Common Mistakes

# BAD: Installs build tools in final image
RUN apt-get update && apt-get install -y gcc make python3
RUN pip install mypackage

# GOOD: Use multi-stage build instead
FROM python:3.12-slim AS builder
RUN pip install mypackage

FROM python:3.12-slim
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages

Multi-Stage Builds

Multi-Stage Builds

Multi-stage builds let you use multiple FROM statements in a single Dockerfile. Each FROM begins a new stage, and you can copy artifacts from one stage to another, leaving build tools behind.

Node.js Example

# Stage 1: Build
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Result: Final image is ~150MB instead of ~1.2GB with build tools.

Go Example

# Stage 1: Build binary
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server

# Stage 2: Scratch (empty image)
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]

Result: Final image is ~10MB — just the compiled binary and CA certificates.

Copying from Named Images

You can copy files from images without defining a stage:

FROM nginx:1.25
COPY --from=node:20 /usr/local/lib/node_modules /usr/local/lib/node_modules

Building Specific Stages

# Build only the builder stage
docker build --target builder -t myapp:build .

# Build the default (last) stage
docker build -t myapp:latest .

Stage Naming

FROM node:20 AS dependencies
# ...

FROM dependencies AS development
# ...

FROM node:20-alpine AS production
# ...

Cache Mounts (BuildKit)

# syntax=docker/dockerfile:1
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN --mount=type=cache,target=/app/.next/cache npm run build

Cache mounts persist between builds, dramatically speeding up npm install and pip install.

Dockerfile Best Practices

Dockerfile Best Practices

1. Order Instructions for Cache Efficiency

# BAD: Code change invalidates npm install cache
COPY . .
RUN npm ci

# GOOD: Dependencies change less often than code
COPY package*.json ./
RUN npm ci
COPY . .

2. Minimize Layer Count

# BAD: Three layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# GOOD: One layer, cleaned in same RUN
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

3. Use Specific Base Image Tags

# BAD: Unpredictable, may break builds
FROM node
FROM node:latest

# GOOD: Pinned version
FROM node:20.10-alpine

4. Run as Non-Root

# Create user before switching
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

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

# Switch user
USER appuser

5. Use .dockerignore

Always include a .dockerignore to exclude:

.git
node_modules
*.md
.env
.vscode

6. Combine Related Commands

# BAD
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y pip
RUN apt-get clean

# GOOD
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      python3 \
      python3-pip && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

7. Use Health Checks

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

8. Clean Up in Same Layer

RUN pip install --no-cache-dir -r requirements.txt
RUN npm ci --only=production && npm cache clean --force

Complete Production Dockerfile

FROM node:20.10-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

FROM node:20.10-alpine
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup
WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

BuildKit Advanced Features

BuildKit Advanced Features

BuildKit is Docker's modern build engine, enabled by default in Docker 23.0+. It provides parallel builds, cache mounts, and advanced syntax.

Enable BuildKit

# Docker 23.0+ (default)
docker build -t myapp .

# Older versions
DOCKER_BUILDKIT=1 docker build -t myapp .

# Or set in daemon.json
{
  "features": { "buildkit": true }
}

Syntax Directive

# syntax=docker/dockerfile:1

This line at the top of your Dockerfile enables BuildKit features.

Cache Mounts

# syntax=docker/dockerfile:1
FROM node:20
WORKDIR /app

# Cache npm download cache between builds
RUN --mount=type=cache,target=/root/.npm npm ci

# Cache apt package lists
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt/lists \
    apt-get update && apt-get install -y curl

# Cache Go build cache
RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/go/pkg \
    go build -o server .

Secret Mounts

# syntax=docker/dockerfile:1
FROM node:20
WORKDIR /app

# Mount secret during build only (not in final image)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# Build with secret
docker build --secret id=npmrc,src=.npmrc -t myapp .

SSH Mounts

# syntax=docker/dockerfile:1
FROM node:20
RUN --mount=type=ssh git clone git@github.com:company/private-repo.git
docker build --ssh default -t myapp .

Bind Mounts (Read-Only)

FROM nginx:1.25
RUN --mount=type=bind,source=./nginx.conf,target=/etc/nginx/nginx.conf nginx -t

Checkpoint Builds

# List build cache
docker builder du

# Prune build cache
docker builder prune

# Prune all cache
docker builder prune --all

Common Dockerfile Patterns

Common Dockerfile Patterns

Python Application

FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir poetry
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt -o requirements.txt
RUN pip install --no-cache-dir -r requirements.txt --target /deps

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /deps /usr/local/lib/python3.12/site-packages
COPY . .
EXPOSE 8000
CMD ["gunicorn", "main:app", "--bind", "0.0.0.0:8000"]

Java Spring Boot

FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
COPY gradle gradle
COPY gradlew build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon
COPY src src
RUN ./gradlew bootJar --no-daemon

FROM eclipse-temurin:21-jre-jammy
RUN addgroup --system spring && adduser --system --ingroup spring spring
USER spring:spring
COPY --from=builder /app/build/libs/*.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Rust Application

FROM rust:1.75 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release
COPY src src
RUN touch src/main.rs && cargo build --release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myapp /usr/local/bin/
ENTRYPOINT ["myapp"]

Static Site (Nginx)

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

Dockerfile Snippets

Wait for service:

RUN apt-get update && apt-get install -y --no-install-recommends wait-for-it

Change user mid-Dockerfile:

USER root
RUN apt-get update && apt-get install -y vim
USER node

Copy with specific permissions:

COPY --chmod=755 entrypoint.sh /usr/local/bin/

Platform-specific builds:

FROM --platform=linux/amd64 node:20