Dockerfile Patterns
Best practices and patterns for writing production-ready Dockerfiles based on official Docker documentation and industry standards.
Sources: This guide is based on Docker Official Documentation, Docker Multi-stage Builds, and current security best practices as of 2025.
Dockerfile Structure Template
# syntax=docker/dockerfile:1
# Build stage
FROM node:20.11.0-alpine AS builder
# Metadata
LABEL maintainer="team@example.com"
LABEL version="1.0"
# Set working directory
WORKDIR /app
# Install dependencies first (better caching)
COPY package*.json ./
RUN npm ci
# Copy application code
COPY . .
# Build application
RUN npm run build
# Production stage
FROM node:20.11.0-alpine AS production
# Install security updates
RUN apk update && apk upgrade && rm -rf /var/cache/apk/*
# Create non-root user (UID > 10000 for security)
RUN addgroup -g 10001 -S nodejs && \
adduser -S nodejs -u 10001
WORKDIR /app
# Copy built assets from builder
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs package*.json ./
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Health check (production-necessary, not optional)
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node healthcheck.js || exit 1
# Start application
CMD ["node", "dist/server.js"]
Base Image Selection
Choose from Trusted Sources
Docker recommends using images from:
- •Docker Official Images: Curated collection with clear documentation
- •Verified Publisher images: Authenticated by Docker
- •Docker-Sponsored Open Source: Maintained projects
Source: Docker Best Practices - Base Images
Alpine Linux (Minimal)
Alpine is recommended as a minimal starting point, currently under 6 MB while providing a full Linux distribution.
# Smallest size, minimal attack surface
FROM node:20.11.0-alpine
FROM python:3.12-alpine
FROM ruby:3.3-alpine
# Install common dependencies on Alpine
RUN apk add --no-cache \
bash \
curl \
git \
postgresql-dev
Pin to Specific Digests for Supply Chain Security
# Pin to specific digest for guaranteed immutability FROM node:20.11.0-alpine@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c # Note: Requires active maintenance for security updates # Use Docker Scout for automated remediation workflows
Source: Docker Best Practices - Supply Chain Security
Debian Slim (Balanced)
# Good balance of size and compatibility
FROM node:20-slim
FROM python:3.12-slim
FROM ruby:3.3-slim
# CRITICAL: Always combine apt-get update with install
# Prevents cache issues that could install outdated packages
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git \
postgresql-client && \
rm -rf /var/lib/apt/lists/*
Distroless (Maximum Security)
# Google's distroless images - no shell, minimal attack surface FROM gcr.io/distroless/nodejs20-debian12 FROM gcr.io/distroless/python3-debian12 FROM gcr.io/distroless/java17-debian12 # Note: No shell available, debugging is harder # Best for production security
Multi-Stage Builds (Security Critical)
Multi-stage builds are foundational for production security. They allow you to "selectively copy artifacts from one stage to another, leaving behind everything you don't want in the final image."
Security Benefits
- •Reduced Attack Surface: Excludes build tools, compilers, and development dependencies
- •Smaller Image Size: Final image contains only runtime necessities
- •No Build Tools in Production: Eliminates unnecessary security risks
Source: Docker Multi-stage Builds Official Documentation
Basic Multi-Stage Pattern
# Build stage FROM golang:1.24 AS build WORKDIR /app COPY . . RUN go build -o /bin/hello ./main.go # Production stage - minimal runtime FROM scratch COPY --from=build /bin/hello /bin/hello CMD ["/bin/hello"]
Copy from External Images
# Copy from any image, not just previous stages COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
Named Stages for Maintainability
FROM node:20-alpine AS dependencies RUN npm ci --only=production FROM node:20-alpine AS builder COPY --from=dependencies /app/node_modules ./node_modules RUN npm run build FROM node:20-alpine AS production COPY --from=builder /app/dist ./dist
Use AS <NAME> to reference stages by name rather than numeric index—improves maintainability when reordering instructions.
Layer Optimization
Order by Change Frequency (Critical for Caching)
# ❌ BAD - Changes frequently, invalidates cache COPY . . RUN npm install # ✅ GOOD - Dependencies change less often COPY package*.json ./ RUN npm ci COPY . .
Understanding cache invalidation is critical for efficient builds.
Combine RUN Commands
# ❌ BAD - Creates multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*
# ✅ GOOD - Single layer, smaller image
# CRITICAL: Combine apt-get update with install in same RUN
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git && \
rm -rf /var/lib/apt/lists/*
Sort arguments alphanumerically for maintainability.
Use Pipefail for Error Detection
# IMPORTANT: Prepend set -o pipefail to catch errors at any stage
RUN set -o pipefail && \
curl -sL https://example.com/install.sh | bash
Without pipefail, errors in piped commands may be silently ignored.
Source: Docker Best Practices - RUN Instructions
Use .dockerignore (Essential)
Including unnecessary files like .git, node_modules, and .env increases image size and exposes secrets. The .dockerignore file excludes files and directories from the build context.
# .dockerignore node_modules npm-debug.log .git .env .env.local .env.*.local *.md .DS_Store coverage .next dist build *.log .vscode .idea Dockerfile docker-compose*.yml
Source: Docker Best Practices - Build Context
Security Best Practices
Run as Non-Root User (Critical)
By default, Docker containers run as UID 0 (root). If the container is compromised, the attacker will have host-level root access to all resources allocated to the container.
UIDs below 10,000 are a security risk on several systems. If someone manages to escalate privileges outside the Docker container, their Docker container UID may overlap with a more privileged system user's UID, granting them additional permissions.
Best Practice: Always run processes as a UID above 10,000.
# Alpine - Use UID > 10000 for security
RUN addgroup -g 10001 -S appuser && \
adduser -S appuser -u 10001
# Debian/Ubuntu - Use UID > 10000
RUN useradd -m -u 10001 appuser
# Switch to non-root user
USER appuser
# Set proper ownership when copying
COPY --chown=appuser:appuser . .
Source: Docker USER Instruction Best Practices
Scan for Vulnerabilities
# Use specific versions, never :latest FROM node:20.11.0-alpine # Keep base image updated RUN apk update && apk upgrade && rm -rf /var/cache/apk/*
Scan images regularly:
docker scout cves my-image:latest # or trivy image my-image:latest
Secrets Management
# ❌ BAD - Never hardcode secrets
ENV API_KEY=secret123
# ✅ GOOD - Use build args with cleanup
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && \
npm install && \
rm -f .npmrc
# ✅ BETTER - Pass at runtime
# docker run -e API_KEY=$API_KEY my-image
Health Checks (Production-Necessary)
According to Docker best practices, "adding a HEALTHCHECK to your Dockerfile is not a nice-to-have—it's a production necessity." Without health checks, Docker cannot detect if a containerized service is unhealthy, preventing automatic restarts or recovery.
HEALTHCHECK Syntax
HEALTHCHECK [OPTIONS] CMD command
Options:
- •
--interval=DURATION(default: 30s) - •
--timeout=DURATION(default: 30s) - •
--start-period=DURATION(default: 0s) - Grace period for slow-starting apps - •
--start-interval=DURATION(default: 5s) - •
--retries=N(default: 3)
Exit Codes:
- •
0: success - container is healthy - •
1: unhealthy - container isn't working correctly
Examples
# Web server check HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost/ || exit 1 # Database check HEALTHCHECK --interval=10s --timeout=5s --retries=5 \ CMD pg_isready -U postgres || exit 1 # With start-period for slow-starting apps HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \ CMD node healthcheck.js || exit 1
Best Practices
- •Use appropriate health checks - Don't use shallow checks (e.g., ping when you need connection tests)
- •Use
start-periodfor slow apps - Gives containers grace period before failures count - •Verify tool availability - Tools like
curlmay not be in slim/alpine images - •Lightweight checks - Use
wget --spideror native commands when possible
Source: Docker HEALTHCHECK Reference