Chapter 3: Dockerfile Best Practices
Authored by syscook.dev
What is a Dockerfile?
A Dockerfile is a text file containing a series of instructions that Docker uses to automatically build images. It defines the environment, dependencies, and configuration needed to run your application in a container.
Key Concepts:
- Instructions: Commands that define how to build the image
- Layers: Each instruction creates a new layer in the image
- Build Context: Files and directories sent to Docker daemon
- Multi-stage Builds: Using multiple FROM instructions for optimization
- Build Cache: Docker reuses layers that haven't changed
Why Use Dockerfile Best Practices?
1. Optimize Image Size
Smaller images mean faster downloads, less storage, and better security.
# Bad: Large image with unnecessary files
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
build-essential \
git \
curl \
wget \
vim \
nano \
htop \
tree \
unzip
COPY . /app
RUN make build
# Image size: ~800MB
# Good: Minimal image with only necessary files
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Image size: ~150MB
2. Improve Build Performance
Efficient Dockerfiles build faster and use less resources.
# Bad: Inefficient layer ordering
FROM node:18
COPY . /app
WORKDIR /app
RUN npm install
RUN npm run build
# Dependencies reinstalled on every code change
# Good: Optimized layer ordering
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Dependencies cached, only rebuild when package.json changes
3. Enhance Security
Secure Dockerfiles reduce attack surface and vulnerabilities.
# Bad: Running as root
FROM node:18
COPY . /app
WORKDIR /app
RUN npm install
CMD ["node", "app.js"]
# Good: Running as non-root user
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
COPY --chown=nextjs:nodejs . .
USER nextjs
CMD ["node", "app.js"]
Dockerfile Instructions Deep Dive
1. FROM Instruction
Choose the Right Base Image
# Official images are preferred
FROM node:18-alpine
# Avoid latest tag in production
FROM node:18.19.0-alpine3.18
# Use specific versions for reproducibility
FROM python:3.11.5-slim-bullseye
Multi-Architecture Support
# Use multi-platform base images
FROM --platform=$BUILDPLATFORM node:18-alpine
# Build for specific platform
FROM --platform=linux/amd64 nginx:alpine
2. RUN Instruction Best Practices
Combine Commands and Clean Up
# Bad: Multiple RUN instructions
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# Good: Single RUN with cleanup
RUN apt-get update && \
apt-get install -y \
curl \
wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Use Specific Package Versions
# Bad: Install latest versions
RUN pip install requests flask
# Good: Pin specific versions
RUN pip install \
requests==2.31.0 \
flask==2.3.3
3. COPY and ADD Instructions
Use COPY Instead of ADD
# Bad: Using ADD for simple file copying
ADD . /app
ADD package.json /app/
# Good: Using COPY for file operations
COPY . /app
COPY package.json /app/
# Use ADD only for URLs and archives
ADD https://example.com/file.tar.gz /tmp/
ADD archive.tar.gz /app/
Optimize Layer Caching
# Bad: Copy everything first
COPY . /app
WORKDIR /app
RUN npm install
# Good: Copy package files first
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
4. WORKDIR and USER Instructions
Set Working Directory
# Always set WORKDIR
WORKDIR /app
# Use absolute paths
WORKDIR /usr/src/app
Create and Use Non-Root User
# Create user and group
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Or use existing user from base image
USER node
# Set user before copying files
USER appuser
COPY --chown=appuser:appuser . .
Multi-Stage Builds
1. Basic Multi-Stage Build
Separate Build and Runtime Environments
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Runtime stage
FROM node:18-alpine AS runtime
WORKDIR /app
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
USER nextjs
CMD ["node", "dist/app.js"]
2. Advanced Multi-Stage Patterns
Multiple Build Stages
# Dependencies stage
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Test stage
FROM builder AS tester
RUN npm run test
# Runtime stage
FROM node:18-alpine AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/app.js"]
3. Language-Specific Multi-Stage Examples
Go Application
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
CMD ["./main"]
Python Application
# Build stage
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
# Runtime stage
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
Security Best Practices
1. Minimize Attack Surface
Use Minimal Base Images
# Bad: Full OS with many packages
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
build-essential \
git \
curl \
wget \
vim \
nano
# Good: Minimal base image
FROM alpine:3.18
RUN apk add --no-cache \
curl \
ca-certificates
Remove Package Managers
# Remove package managers after installation
FROM node:18-alpine
RUN apk add --no-cache dumb-init
RUN apk del apk-tools
2. Run as Non-Root User
Create Dedicated User
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
COPY --chown=nextjs:nodejs . .
USER nextjs
CMD ["node", "app.js"]
3. Handle Secrets Securely
Use Build Arguments for Build-Time Secrets
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
RUN npm install
RUN rm .npmrc
Use Multi-Stage Builds for Secrets
# Build stage with secrets
FROM node:18-alpine AS builder
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
COPY package*.json ./
RUN npm ci
RUN rm .npmrc
# Runtime stage without secrets
FROM node:18-alpine AS runtime
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["node", "app.js"]
Performance Optimization
1. Layer Optimization
Order Instructions by Change Frequency
# Instructions that change rarely
FROM node:18-alpine
WORKDIR /app
# Dependencies (change less frequently)
COPY package*.json ./
RUN npm ci --only=production
# Application code (changes frequently)
COPY . .
RUN npm run build
# Runtime configuration (changes rarely)
EXPOSE 3000
CMD ["node", "dist/app.js"]
Use .dockerignore
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.nyc_output
.coverage
.vscode
.idea
*.swp
*.swo
*~
2. Build Context Optimization
Minimize Build Context
# Bad: Large build context
docker build .
# Good: Use .dockerignore and specific context
docker build -f Dockerfile.prod .
Use BuildKit for Better Performance
# Enable BuildKit
export DOCKER_BUILDKIT=1
docker build .
# Or use buildx
docker buildx build --platform linux/amd64 .
3. Caching Strategies
Leverage Build Cache
# Cache dependencies
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Copy source code (invalidates cache when code changes)
COPY . .
RUN npm run build
Use Build Cache Mounts
# Use cache mount for npm
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
Common Patterns and Examples
1. Web Application (Node.js)
Production-Ready Dockerfile
FROM node:18-alpine AS base
RUN apk add --no-cache dumb-init
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
USER nextjs
EXPOSE 3000
CMD ["dumb-init", "node", "dist/app.js"]
2. Python Application
Production-Ready Dockerfile
FROM python:3.11-slim AS base
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
FROM base AS deps
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
FROM python:3.11-slim AS runtime
WORKDIR /app
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=deps /root/.local /root/.local
COPY --chown=appuser:appuser . .
USER appuser
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
3. Static Website (Nginx)
Production-Ready Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS runtime
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Testing and Validation
1. Dockerfile Linting
Use hadolint for Linting
# Install hadolint
curl -L https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o hadolint
chmod +x hadolint
sudo mv hadolint /usr/local/bin/
# Lint Dockerfile
hadolint Dockerfile
Common Linting Rules
# Use specific tags
FROM node:18-alpine # Not FROM node:latest
# Combine RUN commands
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean
# Use COPY instead of ADD
COPY . /app # Not ADD . /app
# Set WORKDIR
WORKDIR /app
# Use non-root user
USER node
2. Image Scanning
Scan for Vulnerabilities
# Install Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
# Scan image
trivy image myapp:latest
# Scan Dockerfile
trivy config Dockerfile
3. Build Testing
Test Build Process
# Test build
docker build -t myapp:test .
# Test with different platforms
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:test .
# Test multi-stage build
docker build --target builder -t myapp:builder .
docker build --target runtime -t myapp:runtime .
Troubleshooting Common Issues
1. Build Failures
Debug Build Process
# Build with verbose output
docker build --progress=plain -t myapp .
# Build without cache
docker build --no-cache -t myapp .
# Build specific stage
docker build --target builder -t myapp:builder .
2. Large Image Sizes
Analyze Image Layers
# Show image history
docker history myapp:latest
# Show layer sizes
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
# Use dive to analyze layers
dive myapp:latest
3. Security Issues
Check for Vulnerabilities
# Scan image
trivy image myapp:latest
# Check for secrets
trivy secret myapp:latest
# Check configuration
trivy config Dockerfile
Summary
Dockerfile best practices are essential for creating efficient, secure, and maintainable container images:
- Optimize image size with minimal base images and multi-stage builds
- Improve build performance with proper layer ordering and caching
- Enhance security by running as non-root users and minimizing attack surface
- Use proper instructions like COPY instead of ADD and combine RUN commands
- Implement testing with linting, scanning, and build validation
- Follow patterns that are specific to your application type and requirements
By following these best practices, you'll create Docker images that are production-ready, secure, and efficient.
This tutorial is part of the SysCook DevOps series. Continue to the next chapter to learn about Docker Compose.