Dockerfile Best Practices

Build faster, smaller, and more secure Docker images in 2025. Learn the patterns that top engineering teams use to optimize their containerized applications.

Updated 2025
18 min read
Intermediate

Why Dockerfile Optimization Matters

In 2025, container image size and build speed directly impact your deployment velocity, CI/CD costs, and application security. A well-optimized Dockerfile can reduce your image size by 10x, cut build times in half, and significantly reduce your attack surface.

Faster Deployments

Smaller images = faster pulls and deploys

Lower Costs

Reduced storage and bandwidth costs

Better Security

Fewer packages = smaller attack surface

1. Choose Minimal Base Images

Alpine vs Debian vs Distroless

Your base image choice has the biggest impact on final image size and security. In 2025, we have better options than ever.

❌ Avoid

# Using full Ubuntu base (800MB+)
FROM ubuntu:latest

RUN apt-get update && apt-get install -y \\
    python3 \\
    python3-pip \\
    curl \\
    vim \\
    git

# Image size: ~1GB

Problems: Huge size, many unnecessary packages, security vulnerabilities

✅ Better

# Using Alpine (5MB base)
FROM python:3.11-alpine

RUN apk add --no-cache \\
    gcc \\
    musl-dev

# Image size: ~50MB
# Or even better: Use distroless for production

Benefits: 20x smaller, fewer vulnerabilities, faster deploys

Image Size Comparison 2025

  • python:3.11 (Debian-based): ~920MB
  • python:3.11-slim: ~130MB
  • python:3.11-alpine: ~50MB
  • gcr.io/distroless/python3: ~60MB (most secure)

2. Master Multi-Stage Builds

Separate Build and Runtime Dependencies

Multi-stage builds let you use heavy build tools without including them in your final image. This is the #1 technique for reducing image size in 2025.

❌ Single-Stage Build

FROM node:18

WORKDIR /app

# Install ALL dependencies (dev + prod)
COPY package*.json ./
RUN npm install

# Build application
COPY . .
RUN npm run build

# Final image includes node_modules, build tools, source code
# Image size: ~1.2GB
CMD ["npm", "start"]

✅ Multi-Stage Build

# Stage 1: Build
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

# Build the application
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:18-alpine

WORKDIR /app

# Copy only production dependencies and built files
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./

# Image size: ~120MB (10x smaller!)
CMD ["node", "dist/index.js"]

Why This Works

The final image only includes the runtime dependencies and compiled code. All the build tools (TypeScript compiler, webpack, test frameworks) are left behind in the builder stage.

3. Optimize Layer Caching

Order Matters: From Least to Most Frequently Changed

Docker caches each layer. When a layer changes, all subsequent layers must be rebuilt. Order your instructions to maximize cache hits.

❌ Poor Caching

FROM python:3.11-slim

WORKDIR /app

# Copy everything first (cache invalidated on ANY file change)
COPY . .

# Dependencies reinstalled on EVERY build
RUN pip install -r requirements.txt

CMD ["python", "app.py"]

Problem: Changing app.py invalidates the pip install cache

✅ Optimized Caching

FROM python:3.11-slim

WORKDIR /app

# Copy only dependency files first
COPY requirements.txt .

# Install dependencies (cached unless requirements.txt changes)
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code last
COPY . .

CMD ["python", "app.py"]

Benefit: Dependencies cached across app code changes

Layer Order Best Practice

  1. Base image and system packages
  2. Application dependencies (package.json, requirements.txt)
  3. Application source code
  4. Configuration files (if they change frequently)

4. Security Best Practices

Run as Non-Root User

Running containers as root is a major security risk in 2025. Always create and use a non-root user.

❌ Security Risk

FROM node:18-alpine

WORKDIR /app

COPY . .
RUN npm install

# Running as root!
CMD ["node", "server.js"]

✅ Secure

FROM node:18-alpine

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \\
    adduser -S nodejs -u 1001

WORKDIR /app

COPY --chown=nodejs:nodejs . .
RUN npm ci --only=production

# Switch to non-root user
USER nodejs

CMD ["node", "server.js"]

Other Security Best Practices

Use specific image tags, not "latest"

# Bad: FROM node:latest
# Good: FROM node:18.17.0-alpine

Scan images for vulnerabilities

# Add to your CI/CD pipeline
docker scan my-image:latest

# Or use Trivy
trivy image my-image:latest

Don't include secrets in the image

# Bad: COPY .env .
# Good: Use environment variables or secrets management
# docker run -e API_KEY=$API_KEY my-image

Use .dockerignore to exclude sensitive files

# .dockerignore
.git
node_modules
.env
.env.local
*.log
.DS_Store

Complete Production-Ready Example

Node.js Application (2025 Best Practices)

# syntax=docker/dockerfile:1.4
# Use BuildKit features

# Stage 1: Dependencies
FROM node:18-alpine AS deps
WORKDIR /app

# Copy package files
COPY package.json package-lock.json ./

# Install dependencies with cache mount
RUN --mount=type=cache,target=/root/.npm \\
    npm ci --only=production

# Stage 2: Builder
FROM node:18-alpine AS builder
WORKDIR /app

# Copy dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Build the application
RUN npm run build

# Stage 3: Production
FROM node:18-alpine AS runner
WORKDIR /app

# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \\
    adduser --system --uid 1001 nodejs

# Copy only necessary files
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

# Security: Run as non-root
USER nodejs

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
    CMD node healthcheck.js

# Start application
CMD ["node", "dist/index.js"]

# Final image size: ~120MB
# Build time with cache: ~10 seconds
# Security: Non-root user, minimal attack surface

✅ Multi-stage

Minimal final image

✅ Optimized caching

Fast rebuilds

✅ Secure

Non-root user

Python FastAPI Application

# Stage 1: Builder
FROM python:3.11-slim AS builder

WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y \\
    gcc \\
    && rm -rf /var/lib/apt/lists/*

# Copy requirements and install
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: Production
FROM python:3.11-slim

WORKDIR /app

# Create non-root user
RUN useradd -m -u 1000 appuser

# Copy Python packages from builder
COPY --from=builder /root/.local /home/appuser/.local

# Copy application code
COPY --chown=appuser:appuser . .

# Update PATH
ENV PATH=/home/appuser/.local/bin:$PATH

# Switch to non-root user
USER appuser

EXPOSE 8000

# Health check
HEALTHCHECK CMD curl --fail http://localhost:8000/health || exit 1

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

# Final image size: ~180MB

Quick Wins Checklist for 2025

  • Use Alpine or slim base images
  • Implement multi-stage builds
  • Copy package files before source code
  • Run as non-root user
  • Use specific image tags
  • Add .dockerignore file
  • Use --no-cache-dir for pip/npm
  • Scan images for vulnerabilities
  • Add health checks
  • Clean up in the same layer