#Dockerfile Best Practices

Master writing efficient, secure, and maintainable Dockerfiles.


#🎯 Learning Objectives

  • Write optimized Dockerfiles
  • Use multi-stage builds
  • Implement security best practices
  • Minimize image size

#Basic Dockerfile Structure

dockerfile
1# Base image
2FROM node:20-alpine
3
4# Set working directory
5WORKDIR /app
6
7# Copy dependency files first (better caching)
8COPY package*.json ./
9
10# Install dependencies
11RUN npm ci --only=production
12
13# Copy application code
14COPY . .
15
16# Expose port
17EXPOSE 3000
18
19# Set non-root user
20USER node
21
22# Start command
23CMD ["node", "server.js"]

#Essential Instructions

InstructionPurposeExample
FROMBase imageFROM node:20-alpine
WORKDIRSet working directoryWORKDIR /app
COPYCopy filesCOPY . .
ADDCopy + extractADD app.tar.gz /app
RUNExecute commandRUN npm install
ENVSet environment variableENV NODE_ENV=production
EXPOSEDocument portEXPOSE 3000
USERSet userUSER node
CMDDefault commandCMD ["npm", "start"]
ENTRYPOINTFixed commandENTRYPOINT ["python"]
ARGBuild-time variableARG VERSION=1.0
LABELMetadataLABEL maintainer="dev@example.com"

#Multi-Stage Builds

Reduce image size by separating build and runtime:

#Node.js Example

dockerfile
1# Build stage
2FROM node:20-alpine AS builder
3WORKDIR /app
4COPY package*.json ./
5RUN npm ci
6COPY . .
7RUN npm run build
8
9# Production stage
10FROM node:20-alpine AS production
11WORKDIR /app
12COPY --from=builder /app/dist ./dist
13COPY --from=builder /app/package*.json ./
14RUN npm ci --only=production
15USER node
16EXPOSE 3000
17CMD ["node", "dist/server.js"]

#Go Example

dockerfile
1# Build stage
2FROM golang:1.21-alpine AS builder
3WORKDIR /app
4COPY go.* ./
5RUN go mod download
6COPY . .
7RUN CGO_ENABLED=0 GOOS=linux go build -o /app/main
8
9# Production stage
10FROM alpine:3.18
11RUN apk --no-cache add ca-certificates
12COPY --from=builder /app/main /main
13USER nobody
14ENTRYPOINT ["/main"]

#Python Example

dockerfile
1# Build stage
2FROM python:3.11-slim AS builder
3WORKDIR /app
4RUN pip install --user pipenv
5COPY Pipfile* ./
6RUN pipenv install --deploy --system
7
8# Production stage
9FROM python:3.11-slim
10WORKDIR /app
11COPY --from=builder /root/.local /root/.local
12COPY . .
13ENV PATH=/root/.local/bin:$PATH
14USER nobody
15CMD ["python", "app.py"]

#Layer Optimization

#Order Instructions by Change Frequency

dockerfile
1# ✅ GOOD: Dependencies change less often than code
2FROM node:20-alpine
3WORKDIR /app
4
5# 1. Copy dependency files (rarely change)
6COPY package*.json ./
7
8# 2. Install dependencies (cached if package.json unchanged)
9RUN npm ci
10
11# 3. Copy source code (changes often)
12COPY . .
13
14CMD ["node", "server.js"]

#Combine RUN Commands

dockerfile
1# ❌ BAD: Multiple layers
2RUN apt-get update
3RUN apt-get install -y curl
4RUN apt-get install -y wget
5RUN apt-get clean
6
7# ✅ GOOD: Single layer, cleanup included
8RUN apt-get update && \
9    apt-get install -y --no-install-recommends \
10      curl \
11      wget && \
12    apt-get clean && \
13    rm -rf /var/lib/apt/lists/*

#Security Best Practices

#Use Non-Root User

dockerfile
1FROM node:20-alpine
2
3# Create app user
4RUN addgroup -g 1001 appgroup && \
5    adduser -u 1001 -G appgroup -D appuser
6
7WORKDIR /app
8COPY --chown=appuser:appgroup . .
9
10USER appuser
11CMD ["node", "server.js"]

#Use Specific Tags

dockerfile
1# ❌ BAD: Unpredictable
2FROM node:latest
3FROM python
4
5# ✅ GOOD: Specific versions
6FROM node:20.10.0-alpine3.18
7FROM python:3.11.6-slim-bookworm

#Minimal Base Images

BaseSizeUse Case
alpine~5MBMost applications
slim~50MB+When Alpine doesn't work
distroless~20MBMaximum security
scratch0MBStatic binaries
dockerfile
1# Smallest possible for Go
2FROM scratch
3COPY --from=builder /app/main /main
4ENTRYPOINT ["/main"]
5
6# Google distroless
7FROM gcr.io/distroless/static-debian11
8COPY --from=builder /app/main /main
9ENTRYPOINT ["/main"]

#Don't Store Secrets

dockerfile
1# ❌ BAD: Secret in image
2ENV API_KEY=secret123
3
4# ✅ GOOD: Pass at runtime
5# docker run -e API_KEY=secret123 myapp

#.dockerignore

Create .dockerignore to exclude files:

1# Dependencies
2node_modules
3vendor
4
5# Build artifacts
6dist
7build
8*.pyc
9__pycache__
10
11# Development
12.git
13.gitignore
14*.md
15Dockerfile*
16docker-compose*
17
18# IDE
19.vscode
20.idea
21
22# OS
23.DS_Store
24Thumbs.db
25
26# Secrets
27.env
28*.pem
29*.key

#Image Size Comparison

ApproachExample Size
Full base1.2 GB
Slim base200 MB
Alpine base50 MB
Multi-stage + Alpine30 MB
Distroless20 MB
Scratch (Go)10 MB

#Build Commands

bash
1# Basic build
2docker build -t myapp:1.0 .
3
4# Build with different Dockerfile
5docker build -t myapp:1.0 -f Dockerfile.prod .
6
7# Build with arguments
8docker build --build-arg VERSION=1.0 -t myapp:1.0 .
9
10# No cache (fresh build)
11docker build --no-cache -t myapp:1.0 .
12
13# Show build output
14docker build --progress=plain -t myapp:1.0 .

[!TIP] Pro Tip: Use docker build --target builder to build only up to a specific stage for debugging.

[!IMPORTANT] Security: Scan your images with docker scan myapp:1.0 or tools like Trivy before deploying.