Optimised Docker images
In the world of modern application development, Docker containers have become a critical tool for packaging and running applications in a consistent environment. However, one common challenge we developers face is managing the size of Docker container images. Large images increase build times, consume more storage, and slow down deployment speeds. Optimizing Docker images ensures faster downloads, less bandwidth usage, and quicker scaling in production environments.
In this post, I’m going to attempt to cover various techniques to optimize Docker container image size, as well as providing some code examples. This post isn’t extensive, of course, but it should at least give you a firm starting point.
Why Optimize Docker Images?
Before diving into the technical details, it’s important to understand why image optimization matters. Here are a few key reasons:
- Faster deployments: Smaller images lead to faster pull times, reducing the time it takes to spin up new instances in a cloud or CI/CD environment.
- Reduced storage costs: Large images consume significant storage space, especially in large-scale environments.
- Efficient scaling: In a microservices architecture where services need to scale quickly, smaller images help in optimizing resources.
Choosing the Right Base Image
The base image is the foundation of your Docker image. By choosing a smaller and more appropriate base image, you can dramatically reduce the overall size of your container.
Alpine Linux
Alpine Linux is a popular choice for a minimalistic base image. It’s a lightweight Linux distribution that provides a small footprint, typically around 5 MB, making it a good starting point for many applications.
# Use a minimal base image
FROM alpine:3.14
# Install necessary packages
RUN apk add --no-cache python3 py3-pip
COPY . /app
WORKDIR /app
CMD ["python3", "app.py"]
Alpine’s small size ensures you only install the essential packages, helping to keep your image slim.
Beware of Compatibility Issues
While Alpine is a great choice, it may not always be compatible with every library or feature your application requires. In such cases, it might be better to use a larger base image, like Ubuntu, but still optimize it using other techniques discussed in this post.
Minimizing Layers in the Dockerfile
Every RUN
, COPY
, or ADD
instruction in your Dockerfile creates a new layer. More layers mean a larger image, so minimizing the number of layers can significantly shrink your image.
Combine Multiple Commands
Instead of having separate RUN
instructions, combine them into a single instruction wherever possible.
Example Dockerfile
# Bad practice: separate RUN instructions
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# Optimized: combine RUN instructions
RUN apt-get update && apt-get install -y curl && apt-get clean
By combining multiple commands in a single RUN
, you minimize the number of layers in the final image.
Reducing the Number of Files in Containers
Including unnecessary files (like documentation, logs, or large assets) in your image will bloat its size. You can reduce the image size by copying only essential files into the container.
Example Dockerfile
# Only copy necessary files to the container
COPY ./src /app/src
COPY ./requirements.txt /app/requirements.txt
Avoid copying large folders like .git
or files not necessary for your application to run. A good way to manage this is by using the .dockerignore
file.
Utilizing Multi-Stage Builds
Multi-stage builds are an advanced feature of Docker that allow you to use multiple FROM
statements in your Dockerfile. This technique enables you to copy only the necessary artifacts (e.g., binaries or executables) into the final image, thus reducing size.
Example Dockerfile
# Stage 1: Build the application
FROM golang:1.17 AS build
WORKDIR /app
COPY . .
RUN go build -o main .
# Stage 2: Create the final image with only the built binary
FROM alpine:3.14
COPY --from=build /app/main /app/main
CMD ["/app/main"]
In this example, the application is built in the first stage using a full-featured Golang image, but the final image only contains the built binary and uses the minimal Alpine image, significantly reducing the size.
Using .dockerignore
Effectively
Similar to .gitignore
, the .dockerignore
file tells Docker which files and directories to exclude when building an image. This is especially useful for preventing unnecessary files from being copied into the image.
Example .dockerignore file
.git
node_modules
*.log
*.md
By adding unnecessary files and directories to .dockerignore
, you ensure they are not part of the final image, saving space.
Cleaning Up Unnecessary Packages
If you’re installing software or packages in your Dockerfile, ensure you remove them after they’ve served their purpose. This can be particularly important with package managers like apt-get
.
Example Dockerfile
# Install, use, and clean up in one layer
RUN apt-get update && apt-get install -y \
build-essential \
curl && \
rm -rf /var/lib/apt/lists/*
In the above example, the rm -rf /var/lib/apt/lists/*
command removes cached package data, ensuring it doesn’t remain in the final image.
Compressing Your Image
Another powerful optimization technique is to compress your Docker images. Docker does not automatically compress images when they are built, but compression happens during the push/pull process.
You can manually compress the image layers using tools like docker-slim
, which removes unnecessary parts of the image that are not in use.
Example: Bash
docker-slim build your-image-name
docker-slim
analyzes your Docker image and removes any unnecessary layers, files, and libraries, often reducing image sizes by 30-50%.
Analyzing and Monitoring Docker Image Sizes
Keeping track of the size of your Docker images is crucial for maintaining optimal performance. Docker provides built-in commands to inspect the size of your images.
Checking the size of an image:
Example: Bash
docker images
This command will display a list of all your images along with their sizes.
Docker Image History
You can also use the docker history
command to view the size of individual layers in your image.
Example: Bash
docker history your-image-name
This can help you identify large layers that may need optimization.
Using Dive
A more visual tool for inspecting and optimizing Docker images is dive
. It allows you to explore your Docker image layers and understand what contributes to its size.
Example: Bash
dive your-image-name
Dive will display a detailed breakdown of the image layers and files, helping you identify opportunities for optimization.
Conclusion
Optimizing Docker container images is an important practice for improving deployment speed, reducing storage requirements, and making your application more scalable. As noted above, this post isn’t exhaustive but, hopefully, by following the techniques outlined in this post, you should be well on your way to effectively reducing the size of your images and benefit from faster builds, deployments, and smoother scaling.