DTTooleras

Dockerfile Best Practices: Multi-Stage Builds, Security, and Optimization

Learn how to write production-ready Dockerfiles — multi-stage builds, layer caching, security hardening, image size optimization, and templates for every language.

DevToolsHub Team20 min read536 words

Why Dockerfile Quality Matters

A poorly written Dockerfile leads to:

  • Huge images — 1GB+ images that take minutes to pull
  • Security vulnerabilities — Running as root, including unnecessary packages
  • Slow builds — Rebuilding everything on every code change
  • Unreliable deployments — Missing health checks, no graceful shutdown

The Basics

# Always use specific version tags
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy dependency files first (layer caching!)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Copy application code
COPY . .

# Use non-root user
USER node

# Document the port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# Start command
CMD ["node", "server.js"]

Multi-Stage Builds

Multi-stage builds are the single most impactful optimization. They separate the build environment from the runtime environment:

# Stage 1: Build (includes dev dependencies, compilers, etc.)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production (only runtime dependencies)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Result: Build stage might be 500MB+, but the final image is only 100-150MB.

Layer Caching

Docker caches each layer. If a layer hasn't changed, Docker reuses the cached version. Order your Dockerfile from least-frequently-changed to most-frequently-changed:

# ✅ Good order (dependencies change less often than code)
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

# ❌ Bad order (code changes invalidate npm install cache)
COPY . .
RUN npm ci

Security Best Practices

1. Never Run as Root

# Create a non-root user
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

# Switch to non-root user
USER appuser

2. Use Minimal Base Images

# ❌ Full image (900MB+)
FROM node:20

# ✅ Alpine (150MB)
FROM node:20-alpine

# ✅ Distroless (even smaller, no shell)
FROM gcr.io/distroless/nodejs20-debian12

3. Don't Include Secrets

# ❌ Never do this
ENV API_KEY=sk_live_abc123

# ✅ Pass at runtime
# docker run -e API_KEY=sk_live_abc123 myapp

4. Scan for Vulnerabilities

# Scan with Docker Scout
docker scout cves myimage:latest

# Scan with Trivy
trivy image myimage:latest

Image Size Optimization

TechniqueSavings
Alpine base image60-80%
Multi-stage build50-70%
.dockerignore10-30%
Combine RUN commands5-15%
Remove package caches5-10%

.dockerignore

node_modules
.git
.gitignore
README.md
docker-compose*.yml
.env*
.next
coverage
tests

Generate optimized Dockerfiles with our Dockerfile Generator — select your language and get a production-ready Dockerfile with multi-stage builds and security best practices.

Related Tools

dockerfiledockermulti-stage builddocker best practicesdockerfile optimizationdocker securitycontainer

Related articles

All articles

Practice with free tools

200+ free developer tools that run in your browser.

Browse all tools →