Optimization & Best Practices — Module Overview
Why optimize Docker images?
Section titled “Why optimize Docker images?”When you first build a Docker image you can easily end up with something that weighs 1–2 GB. That size compounds in every direction: every docker pull on CI costs seconds (or minutes), every push to a registry costs bandwidth, and every megabyte in the final image is a megabyte an attacker could potentially exploit.
Optimization is not about micro-tweaking. It is about applying a handful of well-known techniques — multi-stage builds, smart layer ordering, tight base images, and secure defaults — that routinely cut images by 70–90 % while making them faster to ship and safer to run.
The three dimensions of optimization
Section titled “The three dimensions of optimization”| Dimension | Goal | Techniques covered |
|---|---|---|
| Size | Smaller image = faster pulls, less storage | Multi-stage builds, small base images, .dockerignore |
| Build speed | Faster iterative builds | Layer-cache ordering, --mount=type=cache |
| Security | Reduced attack surface | Non-root user, no baked-in secrets, HEALTHCHECK, scanning |
Before and after
Section titled “Before and after”Here is a realistic comparison. The left column is a naive single-stage Node.js image; the right column is the same app built with the techniques in this module:
REPOSITORY TAG SIZEmyapp naive 1.21GBmyapp optimized 112MBThat is a 10× reduction with no change to application behaviour — only to the Dockerfile.
What this module covers
Section titled “What this module covers”| Lesson | What you will learn |
|---|---|
| This page | Why optimize, three dimensions, before/after teaser |
| Multi-stage builds | Separate build and runtime stages; COPY --from |
| Cache ordering | Order instructions to maximize cache hits; pin versions |
Small images & .dockerignore | Alpine/distroless bases, exclude noise from the build context |
| Security & HEALTHCHECK | Non-root user, no secrets in images, HEALTHCHECK, docker scout |
Hands-on: inspect image sizes
Section titled “Hands-on: inspect image sizes”The snippet below builds two minimal images — one bloated, one slim — so you can see docker images output side by side.
# Build a "before" image with unnecessary dev tools
# syntax=docker/dockerfile:1
FROM node:22 AS before
WORKDIR /app
RUN echo '{"name":"demo","version":"1.0.0"}' > package.json
RUN npm install --save-dev typescript eslint
RUN echo "console.log('hello');" > index.js
CMD ["node","index.js"]
# Build a "after" image with only what is needed
FROM node:22-alpine AS after
WORKDIR /app
RUN echo '{"name":"demo","version":"1.0.0"}' > package.json
RUN echo "console.log('hello');" > index.js
CMD ["node","index.js"]
# --- build both and compare sizes ---
# docker build --target before -t myapp:before .
# docker build --target after -t myapp:after .
# docker images myapp