Learn to write production-ready Dockerfiles using best practices, multi-stage builds, and proper instruction ordering for efficient, secure container images.
A Dockerfile is a text document containing instructions to build a Docker image. Each instruction creates a layer in the final image, and understanding how to write efficient Dockerfiles is crucial for creating small, secure, and fast-building images. In this lesson, you will learn the complete Dockerfile syntax, best practices for instruction ordering, multi-stage builds for production optimization, and security considerations that every containerized application should implement.
Every Dockerfile starts with a base image and contains instructions that modify that image layer by layer:
Essential Dockerfile Instructions:
1# Comment - Ignored by Docker
2# FROM - Sets the base image (required, must be first instruction)
3FROM ubuntu:22.04
4
5# LABEL - Adds metadata to the image
6LABEL maintainer="[email protected]"
7LABEL version="1.0.0"
8LABEL description="Application server image"
9
10# ENV - Sets environment variables
11ENV NODE_ENV=production
12ENV APP_HOME=/app
13ENV PATH="${APP_HOME}/bin:${PATH}"
14
15# ARG - Build-time variables (not available in running container)
16ARG BUILD_DATE
17ARG VERSION=1.0.0
18
19# WORKDIR - Sets working directory for subsequent instructions
20WORKDIR /app
21
22# COPY - Copies files from build context to image
23COPY package.json ./
24COPY src/ ./src/
25
26# ADD - Similar to COPY but with extra features
27# - Can extract tar archives
28# - Can download from URLs (not recommended)
29ADD archive.tar.gz /app/
30ADD https://example.com/file.txt /app/ # Not recommended
31
32# RUN - Executes commands during build
33RUN apt-get update && apt-get install -y \
34 curl \
35 git \
36 && rm -rf /var/lib/apt/lists/*
37
38# EXPOSE - Documents which ports the container listens on
39EXPOSE 3000
40EXPOSE 8080/tcp
41EXPOSE 8443/udp
42
43# USER - Sets the user for subsequent instructions
44USER node
45
46# VOLUME - Creates mount point for external volumes
47VOLUME ["/data", "/logs"]
48
49# CMD - Default command when container starts (can be overridden)
50CMD ["node", "server.js"]
51
52# ENTRYPOINT - Configures container as executable (harder to override)
53ENTRYPOINT ["./docker-entrypoint.sh"]Instruction Execution:
1# Shell form - Runs command in shell (/bin/sh -c)
2RUN apt-get update
3CMD npm start
4
5# Exec form - Runs command directly (preferred)
6RUN ["apt-get", "update"]
7CMD ["npm", "start"]
8ENTRYPOINT ["node", "server.js"]Build Context:
1# The build context is the directory sent to Docker daemon
2docker build -t myapp:latest .
3
4# Current directory (.) is the build context
5# COPY and ADD reference paths relative to build context
6# Use .dockerignore to exclude filesDocker builds images layer by layer, caching each layer. Understanding this is crucial for fast builds:
How Layer Caching Works:
1┌─────────────────────────────────────────────────────┐
2│ Docker Build Cache │
3├─────────────────────────────────────────────────────┤
4│ │
5│ Build #1 (All layers cached): │
6│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
7│ │ FROM │→ │ COPY │→ │ RUN │→ │ CMD │ │
8│ │ cached │ │ cached │ │ cached │ │ cached │ │
9│ └────────┘ └────────┘ └────────┘ └────────┘ │
10│ │
11│ Build #2 (Code changed - COPY invalidates): │
12│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
13│ │ FROM │→ │ COPY │→ │ RUN │→ │ CMD │ │
14│ │ cached │ │ rebuild│ │ rebuild│ │ rebuild│ │
15│ └────────┘ └────────┘ └────────┘ └────────┘ │
16│ │
17└─────────────────────────────────────────────────────┘Bad Example (Cache Invalidated Early):
1# BAD - Cache busted every time code changes
2FROM node:18-alpine
3
4WORKDIR /app
5
6# Copying all files first invalidates cache for npm install
7COPY . .
8
9# This runs every build because previous COPY changed
10RUN npm install
11
12CMD ["npm", "start"]Good Example (Optimized Cache Usage):
1# GOOD - Dependencies cached separately from code
2FROM node:18-alpine
3
4WORKDIR /app
5
6# Copy only dependency files first
7COPY package.json package-lock.json ./
8
9# Install dependencies (cached unless package files change)
10RUN npm ci --only=production
11
12# Copy source code (changes frequently)
13COPY . .
14
15CMD ["npm", "start"]Cache Optimization Strategies:
1# 1. Order instructions from least to most frequently changing
2FROM node:18-alpine
3
4# Rarely changes - system dependencies
5RUN apk add --no-cache tini
6
7# Changes occasionally - application dependencies
8COPY package*.json ./
9RUN npm ci
10
11# Changes frequently - application code
12COPY . .
13
14# 2. Combine RUN commands to reduce layers
15# Bad - Multiple layers
16RUN apt-get update
17RUN apt-get install -y curl
18RUN apt-get clean
19
20# Good - Single layer
21RUN apt-get update \
22 && apt-get install -y curl \
23 && apt-get clean \
24 && rm -rf /var/lib/apt/lists/*
25
26# 3. Use specific COPY instead of COPY . .
27COPY src/ ./src/
28COPY public/ ./public/
29COPY package*.json ./Multi-stage builds allow you to use multiple FROM instructions, copying only necessary artifacts to the final image:
Basic Multi-Stage Build:
1# Stage 1: Build
2FROM node:18-alpine AS builder
3
4WORKDIR /app
5
6COPY package*.json ./
7RUN npm ci
8
9COPY . .
10RUN npm run build
11
12# Stage 2: Production
13FROM node:18-alpine AS production
14
15WORKDIR /app
16
17# Copy only built artifacts from builder stage
18COPY /app/dist ./dist
19COPY /app/node_modules ./node_modules
20COPY /app/package.json ./
21
22USER node
23EXPOSE 3000
24
25CMD ["node", "dist/server.js"]Go Application Multi-Stage:
1# Build stage
2FROM golang:1.21-alpine AS builder
3
4WORKDIR /app
5
6# Download dependencies
7COPY go.mod go.sum ./
8RUN go mod download
9
10# Build the application
11COPY . .
12RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
13
14# Final stage - minimal image
15FROM alpine:3.18
16
17# Add CA certificates for HTTPS
18RUN apk --no-cache add ca-certificates
19
20WORKDIR /root/
21
22# Copy only the binary
23COPY /app/main .
24
25EXPOSE 8080
26CMD ["./main"]Python Application Multi-Stage:
1# Build stage
2FROM python:3.11-slim AS builder
3
4WORKDIR /app
5
6# Install build dependencies
7RUN apt-get update && apt-get install -y --no-install-recommends \
8 build-essential \
9 && rm -rf /var/lib/apt/lists/*
10
11# Create virtual environment
12RUN python -m venv /opt/venv
13ENV PATH="/opt/venv/bin:$PATH"
14
15# Install Python dependencies
16COPY requirements.txt .
17RUN pip install --no-cache-dir -r requirements.txt
18
19# Production stage
20FROM python:3.11-slim
21
22WORKDIR /app
23
24# Copy virtual environment from builder
25COPY /opt/venv /opt/venv
26ENV PATH="/opt/venv/bin:$PATH"
27
28# Copy application code
29COPY . .
30
31# Create non-root user
32RUN useradd -m -r appuser && chown -R appuser:appuser /app
33USER appuser
34
35EXPOSE 8000
36CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]Multi-Stage with Testing:
1# Base stage
2FROM node:18-alpine AS base
3WORKDIR /app
4COPY package*.json ./
5
6# Dependencies stage
7FROM base AS dependencies
8RUN npm ci
9
10# Test stage
11FROM dependencies AS test
12COPY . .
13RUN npm run lint
14RUN npm run test
15
16# Build stage
17FROM dependencies AS build
18COPY . .
19RUN npm run build
20
21# Production stage
22FROM node:18-alpine AS production
23WORKDIR /app
24COPY /app/dist ./dist
25COPY /app/node_modules ./node_modules
26USER node
27CMD ["node", "dist/index.js"]Secure Dockerfiles are essential for production deployments:
Run as Non-Root User:
1FROM node:18-alpine
2
3WORKDIR /app
4
5# Create non-root user
6RUN addgroup -g 1001 -S appgroup && \
7 adduser -u 1001 -S appuser -G appgroup
8
9# Set ownership
10COPY . .
11
12# Switch to non-root user
13USER appuser
14
15CMD ["node", "server.js"]Minimize Attack Surface:
1# Use minimal base images
2FROM node:18-alpine # Good - minimal
3# FROM node:18 # Avoid - includes unnecessary packages
4
5# Use distroless for even smaller images
6FROM gcr.io/distroless/nodejs18-debian11
7COPY /app /app
8CMD ["server.js"]
9
10# Use scratch for static binaries
11FROM scratch
12COPY /app/binary /binary
13ENTRYPOINT ["/binary"]Handle Secrets Properly:
1# BAD - Secrets in image layers
2ENV API_KEY=secret123
3RUN echo "password" > /app/config
4
5# GOOD - Use build secrets (BuildKit)
6# syntax=docker/dockerfile:1
7FROM node:18-alpine
8RUN npm ci
9
10# GOOD - Use runtime environment variables
11ENV API_KEY="" # Set at runtime with docker run -e
12
13# GOOD - Use Docker secrets in Swarm/Kubernetes
14# Secrets mounted at runtime, not in imagePin Versions:
1# BAD - Version can change unexpectedly
2FROM node:latest
3RUN apt-get install -y curl
4
5# GOOD - Pin all versions
6FROM node:18.19.0-alpine3.18
7RUN apk add --no-cache curl=8.5.0-r0Scan for Vulnerabilities:
1# Add healthcheck
2HEALTHCHECK \
3 CMD wget -q --spider http://localhost:3000/health || exit 1
4
5# Remove unnecessary setuid/setgid binaries
6RUN find / -perm /6000 -type f -exec chmod a-s {} \; 2>/dev/null || trueSecure Dockerfile Template:
1# syntax=docker/dockerfile:1
2FROM node:18.19.0-alpine3.18 AS base
3
4# Security hardening
5RUN apk add --no-cache dumb-init && \
6 rm -rf /var/cache/apk/*
7
8# Create non-root user
9RUN addgroup -g 1001 -S nodejs && \
10 adduser -u 1001 -S nodejs -G nodejs
11
12WORKDIR /app
13
14# Production stage
15FROM base AS production
16
17ENV NODE_ENV=production
18
19COPY package*.json ./
20RUN npm ci --only=production && npm cache clean --force
21
22COPY . .
23
24USER nodejs
25
26HEALTHCHECK \
27 CMD node healthcheck.js
28
29EXPOSE 3000
30
31ENTRYPOINT ["dumb-init", "--"]
32CMD ["node", "server.js"]Modern Dockerfiles support advanced features for complex build scenarios:
Build Arguments (ARG):
1# Define arguments with defaults
2ARG NODE_VERSION=18
3ARG BUILD_ENV=production
4
5# Use in FROM
6FROM node:${NODE_VERSION}-alpine
7
8# Use in instructions
9ARG BUILD_ENV
10ENV NODE_ENV=${BUILD_ENV}
11
12# Build with custom arguments
13# docker build --build-arg NODE_VERSION=20 --build-arg BUILD_ENV=staging .Conditional Logic:
1ARG INSTALL_DEV_DEPS=false
2
3# Conditional package installation
4RUN if [ "$INSTALL_DEV_DEPS" = "true" ]; then \
5 npm install; \
6 else \
7 npm ci --only=production; \
8 fiONBUILD Instructions:
1# Base image Dockerfile (mycompany/node-base)
2FROM node:18-alpine
3WORKDIR /app
4ONBUILD COPY package*.json ./
5ONBUILD RUN npm ci
6ONBUILD COPY . .
7
8# Child Dockerfile - inherits ONBUILD instructions
9FROM mycompany/node-base
10CMD ["npm", "start"]
11# When built, ONBUILD instructions execute automaticallyBuildKit Features:
1# syntax=docker/dockerfile:1
2
3# Mount cache for faster rebuilds
4FROM node:18-alpine
5WORKDIR /app
6COPY package*.json ./
7RUN npm ci
8
9# Mount secrets securely
10RUN \
11 GITHUB_TOKEN=$(cat /run/secrets/github_token) npm install
12
13# SSH mounting for private repos
14RUN git clone [email protected]:private/repo.git
15
16# Bind mount for large files
17RUN \
18 unzip /tmp/file.zip -d /appHeredoc Support:
1# syntax=docker/dockerfile:1
2
3# Multi-line scripts with heredoc
4RUN <<EOF
5apt-get update
6apt-get install -y curl git
7rm -rf /var/lib/apt/lists/*
8EOF
9
10# Create files inline
11COPY <<EOF /app/config.json
12{
13 "port": 3000,
14 "env": "production"
15}
16EOF
17
18# Multi-line with variable substitution
19ARG APP_VERSION=1.0.0
20RUN <<EOF
21echo "Building version: $APP_VERSION"
22npm version $APP_VERSION --no-git-tag-version
23npm run build
24EOFTarget-Specific Builds:
1FROM node:18-alpine AS base
2WORKDIR /app
3COPY package*.json ./
4
5FROM base AS development
6RUN npm install
7COPY . .
8CMD ["npm", "run", "dev"]
9
10FROM base AS test
11RUN npm ci
12COPY . .
13CMD ["npm", "test"]
14
15FROM base AS production
16RUN npm ci --only=production
17COPY . .
18CMD ["npm", "start"]
19
20# Build specific target
21# docker build --target development -t myapp:dev .
22# docker build --target production -t myapp:prod .Build a complete full-stack application with optimized multi-stage Dockerfiles for both frontend and backend.
1# ============================================
2# Backend Dockerfile (Node.js API)
3# File: backend/Dockerfile
4# ============================================
5# syntax=docker/dockerfile:1
6
7# ==========================================
8# Stage 1: Base
9# ==========================================
10FROM node:18.19.0-alpine3.18 AS base
11
12# Install security updates and dumb-init
13RUN apk update && apk upgrade && \
14 apk add --no-cache dumb-init && \
15 rm -rf /var/cache/apk/*
16
17WORKDIR /app
18
19# Create non-root user
20RUN addgroup -g 1001 -S nodejs && \
21 adduser -u 1001 -S nodejs -G nodejs
22
23# ==========================================
24# Stage 2: Dependencies
25# ==========================================
26FROM base AS dependencies
27
28# Copy package files
29COPY package.json package-lock.json ./
30
31# Install all dependencies with cache mount
32RUN --mount=type=cache,target=/root/.npm \
33 npm ci && \
34 npm cache clean --force
35
36# ==========================================
37# Stage 3: Development
38# ==========================================
39FROM dependencies AS development
40
41# Install dev dependencies
42RUN npm install
43
44# Copy source code
45COPY --chown=nodejs:nodejs . .
46
47USER nodejs
48
49EXPOSE 3000
50
51CMD ["npm", "run", "dev"]
52
53# ==========================================
54# Stage 4: Test
55# ==========================================
56FROM dependencies AS test
57
58COPY . .
59
60# Run linting
61RUN npm run lint
62
63# Run type checking
64RUN npm run type-check
65
66# Run unit tests
67RUN npm run test:unit
68
69# Run integration tests
70RUN npm run test:integration
71
72# ==========================================
73# Stage 5: Build
74# ==========================================
75FROM dependencies AS build
76
77# Copy source code
78COPY . .
79
80# Build the application
81ARG BUILD_VERSION=0.0.0
82ENV BUILD_VERSION=${BUILD_VERSION}
83
84RUN npm run build
85
86# Prune dev dependencies
87RUN npm prune --production
88
89# ==========================================
90# Stage 6: Production
91# ==========================================
92FROM base AS production
93
94# Labels for image metadata
95LABEL org.opencontainers.image.title="Backend API" \
96 org.opencontainers.image.description="Production Node.js API" \
97 org.opencontainers.image.version="${BUILD_VERSION}" \
98 org.opencontainers.image.vendor="MyCompany"
99
100ENV NODE_ENV=production
101
102# Copy built application
103COPY --from=build --chown=nodejs:nodejs /app/dist ./dist
104COPY --from=build --chown=nodejs:nodejs /app/node_modules ./node_modules
105COPY --from=build --chown=nodejs:nodejs /app/package.json ./
106
107# Security hardening
108RUN find / -perm /6000 -type f -exec chmod a-s {} \; 2>/dev/null || true
109
110USER nodejs
111
112# Health check
113HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
114 CMD wget -q --spider http://localhost:3000/health || exit 1
115
116EXPOSE 3000
117
118ENTRYPOINT ["dumb-init", "--"]
119CMD ["node", "dist/server.js"]
120
121# ============================================
122# Frontend Dockerfile (React/Next.js)
123# File: frontend/Dockerfile
124# ============================================
125# syntax=docker/dockerfile:1
126
127# ==========================================
128# Stage 1: Dependencies
129# ==========================================
130FROM node:18.19.0-alpine3.18 AS dependencies
131
132WORKDIR /app
133
134COPY package.json package-lock.json ./
135
136RUN --mount=type=cache,target=/root/.npm \
137 npm ci
138
139# ==========================================
140# Stage 2: Build
141# ==========================================
142FROM dependencies AS build
143
144# Build arguments
145ARG NEXT_PUBLIC_API_URL
146ARG NEXT_PUBLIC_GA_ID
147
148# Set build-time environment variables
149ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
150ENV NEXT_PUBLIC_GA_ID=${NEXT_PUBLIC_GA_ID}
151ENV NEXT_TELEMETRY_DISABLED=1
152
153COPY . .
154
155# Build Next.js application
156RUN npm run build
157
158# ==========================================
159# Stage 3: Production Runner
160# ==========================================
161FROM node:18.19.0-alpine3.18 AS runner
162
163WORKDIR /app
164
165# Install security updates
166RUN apk update && apk upgrade && \
167 rm -rf /var/cache/apk/*
168
169ENV NODE_ENV=production
170ENV NEXT_TELEMETRY_DISABLED=1
171
172# Create non-root user
173RUN addgroup -g 1001 -S nodejs && \
174 adduser -u 1001 -S nextjs -G nodejs
175
176# Copy necessary files
177COPY --from=build /app/public ./public
178COPY --from=build /app/package.json ./package.json
179
180# Copy Next.js standalone output
181COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
182COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
183
184USER nextjs
185
186EXPOSE 3000
187
188ENV PORT=3000
189ENV HOSTNAME="0.0.0.0"
190
191HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
192 CMD wget -q --spider http://localhost:3000/api/health || exit 1
193
194CMD ["node", "server.js"]
195
196# ============================================
197# Docker Compose for Full Stack
198# File: docker-compose.yml
199# ============================================
200# version: "3.9"
201#
202# services:
203# backend:
204# build:
205# context: ./backend
206# target: production
207# args:
208# BUILD_VERSION: ${VERSION:-1.0.0}
209# environment:
210# - DATABASE_URL=${DATABASE_URL}
211# - REDIS_URL=redis://redis:6379
212# - JWT_SECRET=${JWT_SECRET}
213# ports:
214# - "3001:3000"
215# depends_on:
216# - postgres
217# - redis
218# restart: unless-stopped
219# healthcheck:
220# test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
221# interval: 30s
222# timeout: 10s
223# retries: 3
224#
225# frontend:
226# build:
227# context: ./frontend
228# args:
229# NEXT_PUBLIC_API_URL: http://localhost:3001
230# ports:
231# - "3000:3000"
232# depends_on:
233# - backend
234# restart: unless-stopped
235#
236# postgres:
237# image: postgres:15-alpine
238# environment:
239# POSTGRES_USER: app
240# POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
241# POSTGRES_DB: appdb
242# volumes:
243# - postgres_data:/var/lib/postgresql/data
244# restart: unless-stopped
245#
246# redis:
247# image: redis:7-alpine
248# volumes:
249# - redis_data:/data
250# restart: unless-stopped
251#
252# volumes:
253# postgres_data:
254# redis_data:Your progress is saved in this browser, on this device only.