Skip to content

🐳 Docker - Advanced Image Optimization

Every byte in your Docker image is a liability. It takes longer to push, longer to pull, more storage cost, and potentially more security vulnerabilities. A Programmer treats the Dockerfile like production code, optimizing for Layer Caching and Minimal Runtimes.


This is the single most important pattern for creating production-ready images. It allows you to use heavy tools (compilers, build kits, node_modules) in a temporary β€œbuild” container, and then copy only the final executable to a tiny β€œproduction” container.

# Stage 1: Build
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Production
FROM nginx:alpine
# We ONLY copy the static assets from the builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
  • Result: Your image size drops from ~1GB (Node + source + tools) to ~20MB (Nginx + static files).

Docker builds are cached by layers. If a layer hasn’t changed, Docker reuses it. If one layer changes, all subsequent layers must be rebuilt.

  • The Junior Mistake: COPY . . followed by RUN npm install. Any change to a single source file invalidates the cache for the entire npm install step.
  • The Senior Fix: Copy only the dependency files first.
    COPY package.json package-lock.json ./
    RUN npm install # This layer is cached unless dependencies change
    COPY . . # Now copy the rest of the code

Stop using the default FROM node or FROM python. These are based on full Debian/Ubuntu distributions and contain hundreds of unnecessary packages.

  • -slim: A stripped-down version of the OS. Great balance between size and compatibility.
  • alpine: Based on Alpine Linux. Tiny (~5MB base), but uses musl instead of glibc, which can occasionally cause issues with specific C-extensions in Python or Node.
  • distroless: Provided by Google. It contains only your application and its runtime dependencies. It doesn’t even have a shell (sh or bash).
    • Programmer Benefit: If a hacker gains access to your container, they can’t run scripts because there is no shell to execute them.

Each RUN, COPY, and ADD command creates a new layer. While modern Docker versions are better at handling this, it’s still best practice to combine related commands.

  • Anti-pattern:
    RUN apt-get update
    RUN apt-get install -y git
    RUN rm -rf /var/lib/apt/lists/*
  • Pattern:
    RUN apt-get update && \
    apt-get install -y git && \
    rm -rf /var/lib/apt/lists/*
    Note: Cleaning up the cache in the same layer is vital. If you delete the cache in a separate RUN command, it stays hidden in the previous layer’s history, still consuming space.

Just like .gitignore, this prevents sensitive or heavy files from even being sent to the Docker daemon (the β€œbuild context”).

  • Must-haves: .git, node_modules, dist, .env, Dockerfile, and local logs.
  • Why? A smaller build context means faster builds and prevents accidentally baking secrets into your image layers.

  • Non-Root User: By default, Docker runs as root. If your container is compromised, the attacker has root access. Always create and use a dedicated user.
    RUN useradd -m myuser
    USER myuser
  • Scan your Images: Use tools like Snyk or Trivy to scan your final images for known vulnerabilities (CVEs) in your dependencies or base OS.

πŸ’‘ Note: Use Dive (wagoodman/dive). It’s a CLI tool that lets you inspect a Docker image layer-by-layer to see exactly what files were added in each step and where you are wasting space.