DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Docker Tutorial

Introduction to docker
Why Use Docker?
Docker vs Virtual Machines
Installing Docker
Key Docker Concepts
Docker Images
Docker Containers
Writing Dockerfiles
Docker Volumes

Optimization

PreviousPrev
Next

Practical techniques to shrink Docker image sizes, speed up builds, and harden images for production use.

1. Choose the Right Base Image

Your base image has the single biggest impact on final image size. Most people just grab node:20 or python:3.12 without realizing there are much leaner alternatives:

Base ImageApproximate SizeWhen to Use
node:20~1.1GBAvoid in production
node:20-slim~240MBWhen Alpine causes issues
node:20-alpine~135MBMost production workloads
ubuntu:22.04~80MBWhen you need a familiar Linux environment
debian:bookworm-slim~75MBNeed Debian packages with less bloat
scratch0BStatically compiled binaries (Go, Rust)

Alpine Linux is the most popular minimal base. At roughly 5MB, it's tiny, but it uses a different C library (musl libc instead of glibc), which occasionally causes compatibility issues with some pre-compiled software. It's worth testing before committing to Alpine for complex applications.

# Before
FROM node:20         # ~1.1GB

# After
FROM node:20-alpine  # ~135MB

That one-line change can save nearly a gigabyte.


2. Combine Related RUN Commands

Every RUN instruction creates a new layer. If you install packages in one layer and delete the cache in a separate layer, the cache files still exist in the earlier layer, they're just hidden. The image size doesn't shrink. Therfore, it's a best practice to combine the RUN statement like:


# Install and clean up in one layer, nothing left behind
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl git && \
    rm -rf /var/lib/apt/lists/*

The --no-install-recommends flag is worth adding too. It tells apt to skip suggested packages that aren't strictly required, which can save a surprising amount of space.


3. Set Up a .dockerignore File

Before Docker builds your image, it sends your project folder to the build engine. If that folder contains node_modules, build artifacts, or your .env file, all of that gets included, even if your Dockerfile never explicitly copies it. A .dockerignore file works exactly like .gitignore and prevents this. Create one in the same directory as your Dockerfile:

node_modules/
.git/
.env
__pycache__/
tests/

Without this file, the build context could be hundreds of megabytes. With it, it often drops to just a few kilobytes. It also prevents secrets from accidentally leaking into your image.


4. Don't Install Tools You Don't Need at Runtime

Every package you add increases image size and introduces more potential vulnerabilities, so try not to include this.

# Install only what the app actually needs to run
RUN apt-get install -y --no-install-recommends libpq5

If you need to debug a running container, you can install tools temporarily with docker exec without it affecting the image itself:

docker exec -it my-container sh -c "apk add --no-cache curl"

5. Clean Up in the Same Layer

If you need build tools to compile something (like gcc for a Python package with native extensions), install them, use them, and remove them all in the same RUN instruction:

RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc python3-dev && \
    pip install --no-cache-dir -r requirements.txt && \
    apt-get purge -y --auto-remove gcc python3-dev && \
    rm -rf /var/lib/apt/lists/*

If you remove tools in a separate RUN, the files are hidden from the final image but still physically present in an earlier layer, which means they still take up space.


6. Pin Package Versions

Installing packages without specifying a version means your image might build differently tomorrow than it does today. That's a debugging nightmare in production.


# Always installs this exact version
RUN apt-get install -y curl=7.88.1-10+deb12u5

For production images especially, pinning versions ensures your builds are reproducible and you're always aware of exactly what's in your image.


7. Never Run as Root

By default, containers run as the root user. This is a real security risk, if an attacker finds a vulnerability in your application, running as root gives them far more power than they should have.

Always create a dedicated non-root user and switch to it before the final CMD:

FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

# Create a non-root user and group
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Give the user ownership of the app directory
RUN chown -R appuser:appgroup /app

# Switch to the non-root user
USER appuser

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

8. Use COPY Instead of ADD

ADD and COPY both bring files into your image, but ADD has extra hidden behaviors as it automatically extracts .tar.gz archives and can even download files from URLs. These implicit behaviors are easy to misuse.

Unless you specifically need those features, always use COPY. It does exactly one thing and does it predictably:

# Don't use this
ADD ./archive.tar.gz /app/          # Auto-extracts the archive
ADD https://example.com/file /app/  # Downloads from a URL

# Better Option
COPY ./archive.tar.gz /app/         # Copies the file as-is

9. Use Multi-Stage Builds

If your application has a build step, like compiling TypeScript, building a React app, compiling a Go binary then multi-stage builds are by far the biggest size optimization available to you.

The idea is simple: use a fully-equipped build stage to produce your artifacts, then copy only those artifacts into a clean, minimal final stage. The build tools never make it into the production image.

FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
RUN npm ci --omit=dev
CMD ["node", "dist/server.js"]

For a full walkthrough of multi-stage builds, see the Multi-Stage Builds page.


10. Scan Your Images for Vulnerabilities

Even a well-optimized image built from an official base can contain known security vulnerabilities (CVEs) in its packages. Make scanning part of your routine:

# Docker Scout: built into Docker Desktop and Docker Hub
docker scout cves myapp:latest

# Trivy: popular open source scanner, very fast
trivy image myapp:latest

# Grype: another solid open source option
grype myapp:latest

The best time to catch vulnerabilities is before they reach production. Add a scan step to your CI pipeline so every image gets checked automatically before it's deployed.


Key Takeaways

  • Base image first: switching from node:20 to node:20-alpine alone can save nearly 1GB.
  • Combine RUN commands: deleting files in a separate layer doesn't actually remove them from the image; it just hides them.
  • Always use .dockerignore as it prevents bloated build contexts and stops secrets from leaking into images.
  • Never run as root and create a dedicated app user and switch to it before CMD.
  • Use COPY not ADD, unless you specifically need archive extraction or URL downloads.
  • Multi-stage builds are the biggest win for apps with a build step as it ship only the output, not the tools.
  • Scan regularly even a small image with known CVEs is still a liability.

Frequently Asked Questions

Do I need to apply all of these techniques to every image?

No, prioritize based on impact. The biggest wins in order are: choosing a smaller base image, using multi-stage builds (if you have a build step), setting up .dockerignore, and combining RUN commands. The others are good habits worth building over time.

Is Alpine always the best base image choice?

It's usually the smallest option, but not always the right one. Some packages behave differently under Alpine's musl libc vs the standard glibc. If you run into strange errors with an Alpine-based image, try the -slim variant instead as it is a Debian-based and much more compatible, while still being significantly smaller than the full image.

My image is still large after switching to Alpine. Why?

The base image is just the starting point. If you're copying in node_modules or running apt-get install without cleaning up, those layers add to the total size regardless of the base. Check your layer sizes with docker history myimage to see where the bulk is coming from.

How often should I scan my images?

At minimum, scan every time you build a new image in CI before deploying. For long-running production images, set up a scheduled scan (weekly is common) since new CVEs are discovered constantly even if your image hasn't changed.

Is it safe to use scratch as a base image?

For statically compiled binaries like Go or Rust programs, yes, scratch is an empty image with nothing in it at all, which is as minimal as it gets. But it means no shell, no package manager, and no OS utilities, so debugging becomes much harder. It's best suited for simple, self-contained services in well-understood production environments.

PreviousPrev
Next

Continue Learning

Building Images

Learn to write Dockerfiles and use docker build to create custom images tailored to your applications.

10 min·Easy

Layers & Caching

Understand how Docker's layer system works and how to use build caching to make your builds dramatically faster.

8 min·Easy

Multi-Stage Builds

Use multi-stage builds to create lean, secure production images by separating build-time and runtime environments in a single Dockerfile.

9 min·Easy

Explore Related Topics

Ku

Kubernetes Tutorials

Orchestrate your Docker containers at scale

CI

CI/CD Tutorials

Automate Docker builds and deployments in pipelines

Try the Tool

Dockerfile Linter

Instantly lint your Dockerfile for best-practice violations and security issues.

On This Page

1. Choose the Right Base Image2. Combine Related `RUN` Commands3. Set Up a `.dockerignore` File4. Don't Install Tools You Don't Need at Runtime5. Clean Up in the Same Layer6. Pin Package Versions7. Never Run as Root8. Use `COPY` Instead of `ADD`9. Use Multi-Stage Builds10. Scan Your Images for VulnerabilitiesKey TakeawaysFrequently Asked Questions