Mastering Docker: The Art of Shrinking Images

Let’s Unleash the Power of Compact Docker Images

Abhishek Sharma
Cloud Native Daily
8 min readJun 9, 2023

--

In the world of containerization, Docker has completely changed how programs/software/applications are managed and deployed. Building blocks for containers, Docker images contain all the configurations and dependencies required. Generally, the size of Docker images increases along with the complexity and number of applications. Bloated pictures slow down deployment speed and efficiency in addition to taking up valuable storage space.

In this post, you may improve resource utilization, streamline your Docker workflow, and increase the effectiveness of application deployment overall by examining how to reduce the size of Docker images without sacrificing performance or functionality. We will look at several recommendations, pointers and tools that help DevOps/developer teams to build Docker images more optimized and leaner.

Whether you are a Docker beginner or a seasoned containerization expert, this article will equip you with practical knowledge and actionable tips to optimize your Docker images effectively. So, if you’re ready to embark on a journey toward leaner, more efficient Docker images, let’s dive in and uncover the secrets to minimizing their size while maximizing their potential.

Let's dive in …

Base Images minimization: Base images should be selected carefully that are specifically designed to be lightweight, such as BusyBox, Alpine Linux etc., to minimize the initial size of the Docker image.

Considering scratch or distroless base images where a minimal runtime environment is sufficient as they have the smallest possible footprint which contain only the bare essentials for running your application.

Multi-Stage builds: Extract compiled binaries or artifacts from earlier stages and copy them to the final stage in Dockerfile, as it allows to exclude unnecessary build dependencies and libraries from the final image. It basically separates the build environment from the runtime environment.

Structuring Dockerfile efficiently with layered caching: Minimizing the number of separate RUN instructions by combining related commands using "&&" or "". This reduces the number of intermediate layers created, resulting in a smaller image.

Additionally, order the instructions in your Dockerfile strategically to maximize the use of Docker’s layer caching mechanism. Place static instructions at the top to take advantage of cached layers and reduce rebuild times.

Ensure the size of each individual layer is optimized in your Docker image. Large layers can lead to increased disk space consumption and longer image pull times.

FROM node:19-alpine
RUN apk add --no-cache git
RUN mkdir -p /application
RUN chown node:node /application
WORKDIR /application
USER node
RUN git clone https://github.com/Rose-stack/node_app.git .
RUN npm install
EXPOSE 4000
CMD npm start

--> Change To :

FROM node:19-alpine
RUN apk add --no-cache git && \
mkdir -p /application && \
chown node:node /application
WORKDIR /application
USER node
RUN git clone https://github.com/Rose-stack/node_app.git . && \
npm install
EXPOSE 4000
CMD npm start

Image Layer ordering: Place frequently changing instructions or files towards the end of the Dockerfile to maximize layer reuse. This helps minimize the number of layers that need to be rebuilt when changes occur.

Every Dockerfile command, e.g. FROM, WORKDIR, COPY, RUN, COPY, EXPOSE, and RUN, creates an image layer of its own. The layers are then used to assemble the final image. The FROM command creates the first layer, followed by the other commands in series.

It’s recommended to add the lines which are used for installing dependencies & packages earlier inside the Dockerfile before the COPY commands , as docker would be able to cache the image with the required dependencies, and this cache can then be used in the following builds when code gets modified.

FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y
RUN apt-get upgrade -y
RUN apt-get install vim -y
RUN apt-get install net-tools -y
COPY . .

--> Change To ...

FROM ubuntu:latest
COPY . .
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y
RUN apt-get upgrade -y
RUN apt-get install vim -y
RUN apt-get install net-tools -y

Optimizing your File copying: Be specific when copying files into the image, instead of copying entire directories, selectively copy only the necessary files to avoid unnecessary bloat.

If you copy files into your image with ADD or COPY you might unintentionally copy over files or folders (like .git) that you don’t want to bake into your image.

Create a .dockerignore file to exclude unnecessary files and directories from being copied into the Docker image. This reduces the size of the final image by excluding irrelevant content.

Dependencies/Packages optimization: Using apt-get or yum package managers with proper cleanup steps to remove temporary files and cached package data, reducing the final image size. Carefully review and remove any unnecessary dependencies or packages that are not essential for the application’s functionality.

Config & Env Variables minimization: Including only environment variables that are essential for app to run. Unnecessary environment variables can increase the size of the Docker image without adding any value.

Libraries and Runtimes minimization : Explore lightweight runtimes like OpenJDK's Alpine-based images for Java apps to reduce the overall image size while maintaining the necessary runtime environment.

Consider statically linking the application or libraries instead of dynamically linking them. This eliminates the need to include shared libraries in the Docker image.

If you’re installing packages with apt-get add the --no-install-recommends tag. This avoids the installation of packages that are recommended but no required alongside a package that you are installing.

FROM python:3.9-slim
RUN apt-get update -y && apt install -y --no-install-recommends \
package_A \
package_B \
package_C

Update Dependencies regularly: Periodically review the dependencies used in your application to benefit from bug fixes, performance improvements, and potentially smaller package sizes. Stay vigilant to avoid vulnerabilities associated with outdated dependencies.

Unnecessary documentation: During installing packages, exclude unnecessary documentation or man pages using package manager flags or configuration options resulting reduction of the overall image size.

Logging and Debugging optimization: Adjusting the logging levels to reduce verbosity, especially in production environments will helps avoid excessive logging which increases the size of log files within the Docker image.

Remove unnecessary debugging tools in production-ready Docker images as it may increase the image size without providing any significant benefit in a live environment.

Smaller or lightweight alternatives to popular tools or libraries , like consider using Flask instead of Django for Python web app when a smaller footprint is desired.

Some packages and frameworks provide build-time options to exclude unnecessary features or modules. Utilize these flags to reduce the size of the final Docker image by excluding unused components.

Clean Up Temporary Files and Caches: Your Dockerfile must include cleanup steps to remove any temporary files/artifacts generated during the build process. This helps reduce the size of the final image by eliminating unnecessary clutter.

FROM python:3.9-slim
RUN apt-get update -y && apt install package_A \
package_B \
package_C \
&& rm -rf /var/lib/apt/lists/*

Compress Files within the Image: Compress files within the Docker image using tools like zstd/gzip which reduces the file sizes within the image, resulting smaller overall image size.

Docker Image Squashing Techniques: Explore tools like “docker-compose” with the “ — squash” flag or “docker-squash” to squash multiple image layers into a single layer resulting overall image size reduction.

Docker Image Size monitoring: You can set up a process to monitor/track the size of Docker images over time which identifies any unexpected increases in size for proactive size optimization measures.

Regularly test the functionality of your Docker image to ensure that size optimization efforts do not compromise the application’s expected behavior.

Measure and compare the size of the Docker image after implementing size reduction techniques to assess the effectiveness of the optimizations

We have now discovered how crucial it is to comprehend the elements — such as layers, dependencies, and extra files — that affect image size. Effective image optimization strategies have been examined in detail, including multi-stage builds, minimal base images, and sound dependency management. We have also learned the value of utilizing Docker capabilities like image layer caching, and effective image tagging.

Remember that optimizing Docker images is an ongoing process. As your applications evolve and grow, it is crucial to periodically revisit and fine-tune your image optimization strategies to maintain the desired level of efficiency.

Optimizing Docker images plays a crucial role in DevOps CI/CD transformations, enabling faster and more efficient application deployments, streamlined testing, and improved resource utilization.

Check more on DevOps CICD Transformation strategies here: Automate and Conquer: Transform Your DevOps Strategy with These Innovative Ideas.

Happy optimizing!

--

--

Abhishek Sharma
Cloud Native Daily
0 Followers
Writer for

AVP Of DevOps/Cloud Technologies — Technology Stack :Jenkins | Gitlab | Ansible | CICD | Docker | Azure | AWS | OCP | Shell | Terraform | Linux |Team leadership