Back to Blog
Lesson 26 of the Docker: Containers & Your First Image course
DevOpsAugust 13, 20264 min read

Optimizing Image Size: Multi-Stage Builds and Alpine Linux

Learn how to achieve significant optimization for your Docker image size using multi-stage builds and Alpine Linux to create leaner, production-ready containers.

dockeroptimizationmulti-stagealpineimage sizecontainersdevops
A modern creative workspace featuring a laptop and monitor displaying digital images and editing software.

Previously in this course, we discussed finalizing your Docker project structure. Now that our services are organized, we need to address a critical production concern: image size. Large images are slow to pull, consume unnecessary storage, and increase your attack surface by including unnecessary tools.

Understanding Image Bloat

When you build an image using a standard base like node:18 or python:3.11, you inherit hundreds of megabytes of OS utilities, build tools (like compilers and headers), and caches. If you only need the final binary or a few static files to run your application, everything else is "bloat."

To solve this, we use two primary strategies:

  1. Alpine Linux: A security-oriented, lightweight Linux distribution that is significantly smaller than Debian-based images.
  2. Multi-Stage Builds: A pattern where you use a "heavy" image to build your application and then copy only the necessary artifacts into a "light" production image.

Implementing Multi-Stage Builds

In a multi-stage build, you use multiple FROM statements in a single Dockerfile. Each FROM instruction begins a new stage. You can name these stages and copy files between them.

Here is a concrete example of a Node.js application build process:

Dockerfile
# Stage 1: The Build Stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: The Production Stage
FROM node:18-alpine
WORKDIR /app
# We only copy the built assets, not the source or node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm install --production
CMD ["node", "dist/index.js"]

In this example, the builder stage contains everything needed to compile the code. The final stage starts fresh from node:18-alpine and only pulls in the dist folder and production dependencies. The resulting image won't contain the original source code or the heavy development tools used during the build.

Why Switch to Alpine?

Alpine Linux is built on musl libc and busybox. A standard Debian-based Python image might exceed 800MB, whereas an alpine equivalent often sits under 50MB.

FeatureDebian-basedAlpine-based
Size~600MB - 1GB+~5MB - 50MB
Package Manageraptapk
Libraryglibcmusl libc
Primary UseCompatibilityProduction Efficiency

Note: Because Alpine uses musl instead of glibc, some compiled C extensions (like certain Python packages) may require extra steps to install or might not be compatible.

Hands-on Exercise

  1. Check your current size: Run docker images and note the size of your current project image.
  2. Refactor: Modify your Dockerfile to use a multi-stage build. If you aren't using a compiled language, you can still use a multi-stage build to separate your npm install (or equivalent) into a cleaner structure.
  3. Build and Compare: Run docker build -t my-app:optimized . and compare the new image size against your original.

Common Pitfalls

  • Missing Dependencies: Because Alpine is so stripped down, you might find that common utilities like curl or git are missing. If your app requires them at runtime, you must install them specifically using apk add.
  • The "latest" Trap: Always pin your base image versions (e.g., node:18-alpine instead of node:alpine). This prevents unexpected breaking changes when the upstream image updates.
  • Over-optimizing: Don't sacrifice stability for size. If your application relies on specific glibc behaviors, stick to a slim Debian base rather than fighting Alpine.

For a deeper dive into analyzing why your layers are large, you can read more about how to analyze layers with Dive or explore broader best practices in Docker image optimization.

FAQ

Q: Does a smaller image run faster? A: It doesn't necessarily run faster, but it starts faster because the node has to pull fewer bytes from the registry.

Q: Are multi-stage builds just for production? A: They are best practice for production, but they also keep your local Docker daemon cleaner during development.

Q: Can I use Alpine for everything? A: Almost, but verify your language's library compatibility first. If you face "file not found" errors for shared libraries, you are likely hitting a glibc vs musl mismatch.

Recap

We've moved beyond simple Dockerfiles by adopting multi-stage builds to discard build-time artifacts and switching to Alpine base images to minimize our footprint. These changes lead to faster deployment pipelines and more efficient resource usage in your cluster.

Up next: Troubleshooting Connectivity Issues — we will debug network timeouts and use diagnostic tools inside your containers to ensure your services can actually talk to one another.

Similar Posts