Skip to main content

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.