Skip to content

Optimization & Best Practices — Module Overview

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.

DimensionGoalTechniques covered
SizeSmaller image = faster pulls, less storageMulti-stage builds, small base images, .dockerignore
Build speedFaster iterative buildsLayer-cache ordering, --mount=type=cache
SecurityReduced attack surfaceNon-root user, no baked-in secrets, HEALTHCHECK, scanning

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 SIZE
myapp naive 1.21GB
myapp optimized 112MB

That is a 10× reduction with no change to application behaviour — only to the Dockerfile.

LessonWhat you will learn
This pageWhy optimize, three dimensions, before/after teaser
Multi-stage buildsSeparate build and runtime stages; COPY --from
Cache orderingOrder instructions to maximize cache hits; pin versions
Small images & .dockerignoreAlpine/distroless bases, exclude noise from the build context
Security & HEALTHCHECKNon-root user, no secrets in images, HEALTHCHECK, docker scout

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
Which of the following is NOT one of the three dimensions of Docker image optimization covered in this module?
A naive single-stage Node.js image weighs 1.21 GB. After applying multi-stage builds and a slim base image, what is a realistic optimized size?
Why does reducing image size improve security?