Docker revolutionizes application deployment, but with images that aren't optimized correctly, you might get slow builds, storage-consuming images, and wasteful deployments. Optimized Docker images are faster to build, smaller in size, and overall more performant. We will discuss realistic strategies to get your Docker images leaner and faster in this guide.
Why Optimize Docker Images?
Well-structured Dockerfiles cuts the build time, speeding up CI/CD pipelines.
Lightweight images consume less bandwidth and save storage costs.
Minimizing unnecessary components reduces the attack surface of your application.
Some of the elements that we can tune to obtain our Ideal Builds.
1. Choose a Minimal Base Image
The base image significantly impacts the final image size. Using lightweight base images can drastically reduce image size.
Best Minimal Base Images:
alpine
(~5 MB) – Lightweight, fast, and secure.distroless
– Only includes runtime dependencies, reducing security risks.scratch
– An empty base image, best for statically compiled binaries.
Example: Switching from Ubuntu to Alpine
# Bad (Large Size)
FROM ubuntu:latest
# Good (Lightweight)
FROM alpine:latest
2. Optimize Layering & Leverage Build Caching
Docker builds images layer by layer, caching steps to avoid unnecessary rebuilds. Poor layering can break caching and slow down builds.
Best Practices:
Place frequently changing instructions (e.g.,
COPY .
) at the bottom of the Dockerfile.Install dependencies before copying application code to improve caching.
Reduce the number of layers by combining commands.
Example: Optimized caching for a Node.js app
# Bad (Breaks Caching)
FROM node:18
COPY . /app
RUN npm install
# Good (Optimized Caching)
FROM node:18
WORKDIR /app
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app/
Here, Docker caches the npm install step if the package.json
remains unchanged, speeding up rebuilds.
3. Remove Unnecessary Dependencies
Unused packages increase image size and security risks. Clean up temporary files and avoid installing unnecessary dependencies.
Example: Cleaning up after package installation
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
4. Use Multi-Stage Builds to Reduce Image Size
Multi-stage builds help by building artifacts in one stage and copying only necessary files into the final image, reducing size and dependencies.
Example: Multi-stage build for a Go application
# First stage (Build)
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o app
# Second stage (Runtime)
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/app .
CMD ["./app"]
The final image is much smaller since it doesn’t include compilers or build tools.
5. Use .dockerignore
to Exclude Unnecessary Files
Prevent unnecessary files from being copied into the image by using a .dockerignore
file.
Example: .dockerignore file
node_modules
.git
*.log
.env
6. Compress & Optimize Image Layers
Use Docker image squashing:
--squash
flag indocker build
.Remove unused images & containers:
docker system prune -a
7. Use Slim Variants of Base Images
Many official images provide slim variants that remove unnecessary tools and files.
Recommended Slim Variants:
python:3.11-slim
instead ofpython:3.11
debian:bullseye-slim
instead ofdebian:bullseye
8. Avoid ADD
in Favor of COPY
ADD
has hidden behaviors like auto-extracting archives and supporting URLs, which can introduce unintended files and security risks
Use COPY
instead of ADD
COPY file.tar.gz /app/
Instead of:
ADD file.tar.gz /app/
If extraction is needed, use RUN tar -xzf file.tar.gz
.
8. Use LABEL
to Add Metadata Efficiently
Adding metadata like versioning, authors (maintainers), and descriptions help in image management and debugging.
LABEL maintainer="Aditya Dhopade <[email protected]>"
LABEL version="1.0.0"
LABEL description="Optimized lightweight Node.js app"
9. Parallelize Package Installations
Package managers often support parallel downloads, which can drastically cut build times.
Parallel Package Installations for apt-get
RUN apt-get update && apt-get install -y --no-install-recommends \
curl git jq \
&& rm -rf /var/lib/apt/lists/*
10. Use Shells Depending on Images
Many images come with Bash, but if your app doesn’t need it, you can use sh
or ash
instead.
Switch to ash for Alpine-based images
RUN apk add --no-cache busybox-extras
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
Can use dash instead of bash in Debian-based images
RUN apt-get install -y dash
SHELL ["/bin/dash", "-eo", "pipefail", "-c"]
11. [BONUS] Use lazy-pull
for Faster Deployments
From Docker 24.0 onwards they have introduced lazy-pulling where only the needed parts of an image are pulled instead of downloading the full image upfront
To enable it can be done using
docker run --pull=always --lazy-pull my-image
This can significantly speed up deployments in cloud environments.
Congratulations you made it to the last !! Stay ahead; Subscribe to the EzyInfra Knowledge Base for more DevOps wisdom.
Conclusion
By following these best practices, you can create leaner, faster, and more secure Docker images:
Use minimal base images
Leverage caching effectively
Remove unnecessary dependencies
Adopt multi-stage builds
Utilize .dockerignore efficiently
Optimizing your Docker images isn’t just about speed—it’s about efficiency, security, and scalability.
EzyInfra.dev – Expert DevOps & Infrastructure consulting! We help you set up, optimize, and manage cloud (AWS, GCP) and Kubernetes infrastructure—efficiently and cost-effectively. Need a strategy? Get a free consultation now!
Share this post