Efficiency is at the heart of every well-designed system, and Docker is no exception. As containerization technology continues to evolve, optimizing Docker images becomes increasingly important for teams that want to optimize their workflow and resource usage.
Docker image optimization is all about effectively reducing your image size without sacrificing performance or functionality, and in this article, we’ll be taking a deep dive into the best practices for achieving just that.
I have spent a significant amount of time exploring the intricacies of Docker and have discovered various methods to create optimized Dockerfiles, resulting in smaller image sizes and improved performance.
By sharing these experiences and practical tips, I hope to help you understand the importance of image efficiency and empower you with the knowledge needed to maximize the full potential of your Docker applications.
Throughout this article, we’ll discuss the essential elements of effective Docker image optimization, such as layering, caching, and minimizing unnecessary bloat.
Through practical examples and easy-to-understand explanations, you will learn the what and why behind each optimization strategy. So, let’s dive in and explore the fascinating world of Docker image optimization.
Starting with the Basics: Docker and Containerization
If you already know Docker, please skip these basics and jump to the Principles of Docker Image Optimization section.
Introducing Docker and Containers
In recent years, I have noticed that Docker has become a popular tool for application containerization, enabling efficiency and reducing operational overhead for developers across various development environments. We have already discussed the basics of Virtualization and Containerization in our article.
Various container technologies, including containerd, rkt (pronounced rocket), LXC (Linux Containers), Podman, Buildah, etc have either predated Docker, evolved alongside it, or emerged more recently. While these technologies share the fundamental concept of images and containers, each exhibits technical nuances that merit exploration.
Despite the diverse landscape of container technologies, Docker stands out as the most widely embraced and recognized. Its pervasive adoption is attributed to several key factors: Standardization, Ease of Use, Portability, Image Management, Ecosystem and Tooling, and Community Support. These factors collectively contribute to Docker’s enduring popularity as the preferred choice for containerization in the industry.
You cannot dismiss the possibility that organizations might transition to other container technologies driven by advanced features, cost optimizations, and security considerations. However, for now, let’s continue with Docker and delve into the importance of image optimization.
With Docker, I can create lightweight containers that run independently and include all the necessary dependencies for my applications to function smoothly. Containers are runtime executables, both resource-efficient and portable.
In my experience, containerization has changed the game when it comes to software development. By abstracting the software code and its required libraries, You can easily develop, test, and deploy applications across different environments without worrying about inconsistencies or incompatibilities.
The Role of Docker Images in Containerization
When working with Docker, I have found that Docker Images play a crucial role in the containerization process. A Docker Image is a read-only template containing everything required to run a containerized application, including the necessary tools and libraries.
To create a Docker container, we begin by pulling a Docker Image from a registry, like DockerHub, using the docker pull
command followed by the image name and tag. Next, we can add a container layer on top of the image to create a runtime environment, which becomes read-write capable and allows our application to run.
To further optimize my container deployment process, I often focus on improving the Docker Image build. After successfully building a Docker Image in my CI pipeline, I push it to a Docker image registry, such as Docker Hub or Cloud Provider’s Container Registry (ECR, ACR, GCR). This enables me to have a well-tuned and efficient container deployment process. By optimizing Docker Image builds, I can ensure that I am using the best practices of containerization, which, in turn, leads to increased efficiency in my software development life cycle.
Understanding Docker Image Basics
Docker Image and Its Components
As an engineer, I find it crucial to understand the basics behind a Docker image before diving into optimization techniques. In simple terms, a Docker image is a read-only template containing all the information and instructions needed to create and run a container.
Think of them as virtual shipping containers that hold software and its environment. Docker uses instructions called Dockerfiles to build these images step by step.
These instructions can include installing packages, copying files, or configuring the environment. Docker images are composed of multiple components, and understanding them can help us optimize their efficiency.
The Role of Layers in Docker Images
One essential aspect of Docker images is their layered structure. Each layer in a Docker image represents a change or an addition to the image, such as installing a dependency or modifying a configuration file. Layers can be reused by multiple images, which can save storage and bandwidth.
The docker history
command is a useful tool for investigating the layers in your image and can help you identify areas where you can optimize further. For example, it’s a good practice to group related instructions together, like installing packages, to minimize the number of layers in your image.
Image Size and Its Impact on Efficiency
An important consideration when working with Docker images is their size. Larger images require more storage space, longer download times, and additional resources during runtime. That’s why optimising your images is crucial to ensure their size stays as small as possible.
One example of a lightweight, efficient base image is Alpine Linux, which is designed for security and resource efficiency. It usually has a size of around 5 MB, making it an excellent choice for a base image in your Dockerfile.
Understanding the basics of Docker images, layers, and their impact on efficiency sets the foundation for optimizing your container environment. Implementing these best practices will ensure your images remain lightweight and efficient, improving the performance of your applications deployed in containers.
Principles of Docker Image Optimization
Choosing the Right Base Image
When optimizing Docker images, I believe that selecting the right base image plays a significant role in improving efficiency. Opting for an appropriate base image helps reduce the final image’s size and ensures better performance.
Here are some guidelines I follow when choosing the right base image:
Use official and verified Docker Images as Base Image
To ensure safety and reliability, I always choose official and verified Docker images from trusted sources like the Docker Hub and other verified sources.
This practice not only guarantees high-quality and well-maintained images but also protects my work from potential security threats.
Specify precise versions of Docker images for consistency and reliability
I always stay cautious about using the latest tag of a Docker image, as it is subject to frequent changes, which might lead to inconsistencies or compatibility issues.
Instead, I prefer specifying a particular version of the base image to maintain a consistent and stable foundation for my Dockerized applications.
Opt for compact official images
Another vital aspect I consider while selecting a base image is its size. Smaller images are more efficient and consume fewer resources.
As an engineer, I have seen how large Docker images affect application deployment times, consume storage resources, and even impact network performance. So, it is essential to minimize the size of Docker images, without sacrificing functionality or performance.
Compatibility with Dependencies
One more common factor is to consider compatibility with application dependencies and the package managers available.
Different base images come with various package managers, such as apt, apk, or yum, which play a pivotal role in installing additional software within the image.
Opting for a base image that aligns with the package manager preferred by your application ensures seamless integration of dependencies.
Different types of docker base images that are widely used in the industry
I often encounter various types of base images that are widely used in the industry. Here are some of the popular ones:
- Official: Official base images are provided by the creators of the respective technologies or platforms, such as Node.js and Python. These images offer an excellent starting point because of their widespread adoption, assured compatibility, and regular updates.
- Ubuntu/ Debian: Some developers, including myself at times, prefer to use general-purpose operating system images like Ubuntu, or Debian as a base. This can be beneficial when additional dependencies or system-level packages are required for the application.
- Alpine: Alpine base images such as the Alpine Linux is a lightweight Linux distribution which is designed to be small and secure. It is lightweight and suitable for applications that require a minimalistic environment. It uses its own package manager called apk. It can help decrease the overall image size, resulting in faster load times and reduced resource consumption.
- Slim: Slim base image is built upon a specific distribution (such as Debian) but is streamlined to be more compact by eliminating non-essential elements. The objective in crafting these images is to decrease their size, enhancing efficiency, while retaining essential functionality required for running applications.
- Distroless: Developed by Google, distroless images provide a minimal runtime environment without any unnecessary components. They usually contain just the bare essential libraries and a few language-specific runtimes. By using distroless images, I can achieve a smaller overall image size and improved security.
- Scratch: The scratch image is a minimal base image that contains no pre-built libraries or dependencies. This is ideal for applications that don’t require an operating system or any external libraries. In this scenario, I compile all necessary libraries and application code into a single binary executable, reducing potential attack surface and ensuring only required components are present in the container.
- Windows Images: Windows Images in Docker are specifically designed for running Windows-based applications within containers. Docker supports both Windows Server containers and Windows 10 containers, allowing developers to containerize and deploy Windows applications in a consistent and isolated manner. An excellent article is already published by Microsoft which will help to choose the appropriate image based on the requirements.
…and so on. In the world of containerized applications, choosing the right base image is crucial for optimal performance.
Minimizing Layer Count and Size
In Docker image optimization, one critical factor is minimizing the layer count and size. By reducing the number of layers and keeping only essential components, we create a smaller, more efficient, secure docker image and speed up the build process.
Docker layers are the result of instructions in the Dockerfile, such as RUN
, COPY
, and ADD
. To minimize the layer count, remember that each RUN
, COPY
, and ADD
instruction generates a new layer. Consequently, using fewer of these instructions leads to fewer layers.
One method to achieve this is by chaining commands in a single RUN
instruction using the &&
operator. For example, instead of writing:
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
we can write it as:
RUN apt-get update && \
apt-get install -y package1 package2
This approach reduces the number of layers from three to one.
Implementing Multi-Stage Builds
One of my favorite techniques and a powerful approach to optimizing Docker images is using multi-stage builds. With this approach, I separate the build process into different stages, allowing me to use the required tools and packages needed for each stage while keeping the final image small and clean.
This technique involves dividing the build process into multiple stages, with each stage responsible for a specific task. Only the relevant output from one stage is transferred to the next, effectively cutting down wasted space.
In many cases, I only need a runtime environment in my Docker image, so it doesn’t make sense to include compilation, build files, application code, package managers, and dependencies that are unnecessary for running the application.
Multi-stage builds help me achieve this level of optimization by allowing me to use multiple FROM
instructions in my Dockerfile. Each stage can inherit or build upon the previous stage, and I can copy specific files from one stage to another, only including what’s necessary.
For example, suppose I am developing a Java application using Maven as my build tool. In the early stages of the build process, I need to fetch dependencies, compile the source code, package the artifacts, and generate a final executable JAR file.
However, for the application to run, I only need a Java Runtime Environment (JRE) and the JAR file. Here’s how I could use multi-stage builds to optimize my Docker image:
In the first stage, start with a base image that contains the JDK and Maven. Here, I compile the source code, fetch dependencies, build the application, and generate the JAR file.
FROM maven:3.6.3-adoptopenjdk-11 AS build
WORKDIR /app
COPY . .
RUN mvn clean package
In the second stage, start with a base image that contains only the JRE. This ensures that I only include the runtime environment, leaving out unnecessary tools and dependencies.
FROM adoptopenjdk/openjdk11:jre-11.0.21_9-alpine
Finally, copy the JAR file from the first stage to the second stage, and set the entrypoint to run the application:
COPY --from=build /app/target/my-app.jar /app/my-app.jar
ENTRYPOINT ["java", "-jar", "/app/my-app.jar"]
By implementing these steps, I create a smaller and more efficient Docker image that only contains what’s necessary to run my application.
The multi-stage build approach allows me to eliminate redundancy, reduce image size, and optimize the Docker build process, leading to faster deployment times and more efficient container management.
Caching Image Layers
Another technique I use for optimizing Docker image builds is caching image layers. Caching works by storing intermediate layers during the build process, which can then be reused in subsequent builds. This significantly speeds up the build process and reduces the time needed for deployment.
I optimize caching by carefully organizing my Dockerfiles and ensuring that frequently used layers are cached effectively. For instance, I group together the installation of dependencies and the copying of source files in separate layers.
By doing this, any changes to the source code won’t invalidate the cache for the dependences layer, making the builds faster.
Additionally, I take measures to prevent secret caching within Docker layers. This includes managing sensitive information like API keys, passwords, and tokens outside of the Docker context.
Instead of including secrets in the Dockerfile, I use environment variables or other secure mechanisms like Docker Secrets.
By leveraging caching techniques and organizing my Dockerfiles efficiently, I can ensure faster Docker image builds while maintaining security and optimization.
Using .dockerignore efficiently
In my experience, using a .dockerignore
file can greatly improve the efficiency of Docker image builds. This file is similar to a .gitignore
file, allowing you to specify a list of files or directories that Docker should ignore during the build process.
By ignoring unnecessary files, you can reduce the size of the build context and thus, speed up the entire build process source.
To use a .dockerignore
file effectively, you should start by identifying the files and directories that are not required in the final Docker image. Some common items to include in a .dockerignore
file are:
- Log files: Log files can grow quite large and are generally not needed in a Docker image.
- Temporary files: Temporary files and directories can be safely excluded as they won’t be necessary in the final image.
- Source control directories: Such as
.git
or.svn
, which store version control information and aren’t needed in the Docker image. - Test files: Unit tests, integration tests, and other test files are generally not required in the final Docker image.
- IDE Files, System and Hidden Files, Dependency Directories, etc
Here’s a sample .dockerignore
file that covers these common exclusions:
# Ignore log files
*.log
# Ignore temporary files
/tmp/
/temp/
# Ignore source control directories
.git/
# Ignore test files
tests/
# Ignore IDE related files
.vscode/
.DS_Store
Another consideration for optimizing Docker image building is to order the .dockerignore
file contents strategically. You should start with the most frequently changed files, followed by the larger and less frequently changed files.
This helps to make better use of Docker’s build cache by reducing the likelihood that a slight change in the build context necessitates rebuilding unrelated layers.
Always double-check and ensure that you are not neglecting any essential files or directories and update this file as your project evolves to ensure continued optimization.
Optimizing Dockerfile Instruction
Writing Efficient Dockerfile Commands
When working with Dockerfiles, it’s essential to write efficient commands to ensure a streamlined building process and optimized images. In my experience, adhering to best practices while writing Dockerfiles results in smaller image sizes and improved container performance.
This can make a significant difference in a production environment where resources and time are valuable.
One common pitfall is the use of recursive chown
commands, which can lead to excessively large images. This occurs because Docker duplicates every affected file, consuming more space than necessary.
Another one is ignoring or forgetting cleanup steps, to clean up temporary files and dependencies after installation can result in larger image sizes.
Always use COPY judiciously to avoid copying unnecessary files into the image. Only copy files that are required for the application to run.
Dependency Management in Dockerfiles
Managing dependencies efficiently in your Dockerfiles can greatly optimize build times and image sizes. Use package managers like npm or pip to install specific versions of the required libraries or utilities, and cache them properly between builds.
To separate your application’s dependencies from the source code, you can split COPY commands in your Dockerfile. First, copy the dependency manifest file (e.g., package.json or requirements.txt), then install the dependencies, and finally copy the rest of your application code.
This way, you can leverage Docker’s caching mechanism to avoid reinstalling dependencies when the source code changes, but the dependencies remain the same.
Security and Compliance in Docker Images
As a Docker user, I understand the importance of security and compliance in image optimization. Let’s discuss some key aspects to consider when working with Docker images.
Reducing the Attack Surface
Security is a paramount concern in any environment, and Docker is no exception. Optimizing Dockerfiles for security involves minimizing the potential attack surface within a container.
In order to achieve this, I suggest following some key practices:
- Use minimal base images: Choose base images with only the necessary components for your application, reducing the overall image size and possible vulnerabilities.
- Keep images up-to-date: Regularly update base images to ensure the latest security patches are applied.
- Limit unnecessary packages: Only install what is absolutely required for your application, avoiding additional attack vectors.
- Follow the principle of least privilege: When possible, avoid running processes as the root user; employ the non-root user with the essential permissions to execute the required tasks.
Understanding the Security Implications of Image Layers
I have learned that Docker images are composed of several layers, and each layer corresponds to an instruction in the Dockerfile. Implementing proper security measures in each layer minimizes the attack surface of the final image.
Minimizing the layers can help ensure that your image is lean and secure. For instance, you can use multi-stage builds to copy only what is needed from one stage to another, reducing overall size and improving security.
Utilize Minimal Privileges
As mentioned above, we should always follow the principle of least privileges. One best practice I’ve adopted is to run containers as a non-root user by default. This way, if an attacker gains access to the container, they will have limited permissions and avenues to escalate privileges.
By incorporating a secure user and permission model in your Docker images, you can effectively enhance security and compliance. Here’s an example of setting a non-root user in a Dockerfile:
FROM node:14-alpine
USER node
Compliant Dockerfile Practices
Implementing compliant Dockerfile practices is crucial for efficient and secure applications. I try to make sure to apply Docker security features like Seccomp profiles, AppArmor, or SELinux to further enforce security policies and restrictions.
These tools allow for fine-grained control and help in minimizing the attack surface by limiting system calls and resource access.
Balancing Functionality and Size
While it’s important to have a lean Docker image for faster deployments, it’s essential that image size reductions don’t compromise the functionality and performance of the application.
When optimizing your Docker image, you may consider Alpine-based images for their small footprint and enhanced efficiency.
However, keep in mind that not all applications are compatible with Alpine-based images. Always ensure the image you choose doesn’t break your application’s functionality.
Scan the Images for Security Vulnerabilities
In light of recent events, it’s crucial to address the vulnerabilities that may exist in Docker images. Optimizing Docker images is not only about reducing their size and improving the build efficiency but also making them more secure.
In this section, I will discuss the steps to perform image scanning and explore some popular container scanning tools that can help you ensure the security of your Docker images.
First, let’s consider a common misconception. We might think that Docker images marked as “Trusted” or “Official” are free of vulnerabilities, but this would be a wrong assumption.
While these labels do signify that the images are authentic and official releases, they are not necessarily free of vulnerabilities.
To reduce potential risks, I recommend using scanning tools that help identify and address known vulnerabilities in your Docker images. There are several security scanning tools available to help you check your Docker images for vulnerabilities. Below are some popular options:
- Docker Scout: Docker Scout, mentioned in the Docker Docs, is a security scanning tool that can be used in Docker Desktop, Docker Hub, Docker CLI, and the Docker Scout Dashboard. It offers integrations with third-party systems, giving you a comprehensive view of your images’ security.
- Docker Scan: Docker Scan is another powerful tool provided by Docker that utilizes Snyk as the security scanning provider. It can be used through the Docker CLI, making it simple to scan your images during development. Remember to run
docker login
to supply your username and password before scanning. - Trivy: A simple-to-use, open-source vulnerability scanner for containers and file systems. For more information, check out this Trivy doc.
- Clair: An open-source vulnerability scanner for Docker images maintained by CoreOS. You can learn more about acquiring and using Clair here.
- Anchore: A comprehensive container security platform that can scan Docker images for vulnerabilities and enforce best practices. You can dive deeper into Anchore’s capabilities on their website.
- Black Duck: Black Duck is a powerful tool for Docker image scanning, providing comprehensive security insights into containerized applications. With Black Duck, you can identify and mitigate security vulnerabilities, license compliance issues, and open-source risks within Docker images.
To effectively secure your Docker containers, it’s essential to use one or more of these tools to scan your container images for known vulnerabilities. Doing so allows you to identify and fix any potential security issues before they become a threat.
Optimal Approaches for Ongoing Maintenance and Iteration
In my experience with Docker, there are several methods for efficient maintenance and iteration of Docker images. Let’s discuss couple of key practices:
Regularly Updating and Pruning Images
As a common practice, I regularly update Docker images to ensure they are using the latest software versions and maintain compatibility with other systems. To do this, it’s essential to keep your base image up-to-date, especially if you are using an official image.
Additionally, using proper version tags for your images is crucial, rather than relying on the latest
tag. This way, you can be more certain about the specific version you are working with, making the updating process more controlled.
Another practice I use is pruning unused images to free up space and prevent wasting bandwidth. Docker provides useful commands for this purpose, such as docker image prune
and docker system prune
.
Implementing these practices keeps my Docker images optimized and reduces the buildup of unnecessary images on my system.
Analyzing Image Performance Over Time
Using tools, I often analyze the performance of my Docker images over time. Through such analysis, I can ensure that the images remain efficient and consume minimal resources.
To optimize the image size, I often follow the red-green-refactor cycle. First, I ensure that my application runs correctly (green), and then I focus on shrinking the image by refactoring the Dockerfile (red).
Finally, I measure the performance to confirm that the changes have indeed made a positive impact (refactor). This continuous improvement process helps me maintain high-performance Docker images.
Tools and Utilities for Image Optimization
Utilizing Open-source and Licensed Analysis Tools
One of the essential aspects of Docker image optimization is to identify and analyze what contributes to the size and performance of the image. Various open-source tools can help with this task, and I find some of them quite useful.
For instance, the Dive tool provides a clear and interactive GUI for exploring layers within a Docker image, helping identify where improvements can be made.
And Slim tool allows you to inspect, optimize and debug their containers using its xray
, lint
, build
, debug
(and other) commands. It makes your containers better, smaller and more secure while providing advanced visibility and improved usability working with the original and minified containers
In terms of licensed tools, options like Scout can analyze and optimize the structure of Docker images, further streamlining them for efficient deployment.
Optimizing with Docker’s Built-in Tools
While it’s undoubtedly beneficial to have external tools at our disposal, it’s also essential to leverage Docker’s built-in utilities.
We have extensively covered all the best practices within this article.
The synergy of these defined practices, when combined, results in the creation of Docker images that are not only efficient, secure, but also well-optimized.
Community Resources and Documentation
For further guidance on optimizing Docker images, you can turn to various community resources and documentation, such as:
- Docker’s official documentation: Docker’s own best practices guide for building efficient Dockerfiles is a great starting point for anyone looking to optimize their images. This guide covers various aspects of Dockerfile optimization, including minimizing the number of layers, using multi-stage builds, and reducing build context size.
- Docker forums and communities: Docker has a large and active community of users who are always sharing their knowledge and experiences. Join Docker forums and communities to get real-life examples, practical tips, and insights from other developers working to optimize their Docker images.
- GitHub repositories and examples: Since Docker is widely used, you can find a vast number of GitHub repositories containing examples of optimized Dockerfiles and projects. By studying these examples, you may learn from others’ experiences and gain inspiration for your own Docker image optimization efforts.
Frequently Asked Questions
In what ways can Docker image optimization improve container efficiency?
Docker image optimization not only reduces the storage space needed on the host machine but also speeds up deployment, scaling, and network transfer times. It results in faster build time, smaller build context, and lower cloud service costs.
How can layers be analyzed and optimized within Docker images?
Layers within Docker images can be analyzed using tools such as Dive, Slim or Scout which displays detailed information about each layer and its impact on overall image size. To optimize layers, you can implement the practices mentioned above.
Can you explain the role of ‘dive’ in optimizing Docker images?
Dive is an open-source tool that helps analyze a Docker image and its layers. It identifies ways in which you can shrink the size of your Docker image by providing detailed information about how each layer affects the overall size.
How can I ensure security while optimizing Docker images?
Ensuring security while optimizing Docker images involves only using reliable base images, avoiding unnecessary packages, using minimal privileges, and keeping images up-to-date. Regularly scanning images for security vulnerabilities is also important.
How can JVM applications be containerized with minimal Docker image sizes?
To containerize JVM applications with minimal image sizes, consider using Alpine Linux as the base image, which is a lightweight distribution. Additionally, you can utilize the JRE instead of JDK unless you specifically need JDK features, and consider using multi-stage builds.
How is Docker image layering impactful on the overall size and efficiency?
Docker image layering directly impacts overall size and efficiency since each layer adds to the image size. By consolidating and optimizing layers in the Dockerfile, we can reduce overall image size and improve efficiency throughout the entire container lifecycle.
Can you outline a step-by-step process to audit and decompose existing Docker images?
- Inspect the Existing Image: Utilize inspection tools to analyze the structure of the current Docker image.
- Identify Inefficient Layers: Identify large or unnecessary layers that contribute significantly to image size.
- Optimize the Dockerfile: Modify the Dockerfile by combining RUN statements, removing redundant files, and considering multi-stage builds to enhance efficiency.
- Rebuild the Image: Rebuild the Docker image after Dockerfile modifications.
- Iterative Optimization: Repeat the inspection, modification, and rebuilding process iteratively until the desired level of optimization is achieved.
What is the need for Docker image optimization, and why is it important?
Docker image optimization is important to minimize the footprint of a container, which in turn reduces storage and network transfer requirements. This leads to faster deployment, scaling, and better resource utilization, significantly lowering costs while improving application performance.
Does caching reduce the size of the Docker image?
While caching doesn’t directly reduce the size of the Docker image, it significantly accelerates the build process by reusing cached layers. This speed improvement can be vital for iterative development processes and continuous integration pipelines. To reduce the size of the final image, other strategies like minimizing dependencies, choosing a minimal base image, and optimizing the Dockerfile are more relevant.
What techniques are effective in minimizing the footprint of a Docker container?
Techniques like using minimal base images, multi-stage builds, creating minimal layers, compressing files, and removing unnecessary files can help minimize the footprint of a Docker container, leading to improved efficiency and performance.
What are the key features and benefits of using Black Duck for Docker image scanning?
Black Duck offers robust Docker image scanning, identifying vulnerabilities, ensuring license compliance, and providing remediation guidance. For more details, refer to Black Duck’s official documentation.
What are some future trends in Docker image optimization?
Future trends in Docker image optimization may include enhanced tools and techniques for optimizing images, better integration with continuous integration and deployment processes, and machine learning-driven optimization algorithms that can recommend better practices for creating efficient images.
Fantastic deep dive into Docker image optimization
Nice work. I learned something new today.
I appreciate how you broke down this complex docker optimization concept into manageable steps