We already know that Docker is a fantastic tool for developing containerized versions of our apps, but in order to fully utilize this technology, we must learn how to construct images effectively. Doing so will enable us to produce images that are smaller and more quickly.

When trying to push or pull an image to Docker Hub or our individual Docker registry, having smaller images can have a significant positive influence. I'll demonstrate two techniques to update our Docker images in this post.

Use smaller images

When we initially attempt to Dockerize our application, our natural tendency is to recreate the standard environment in which that application would function, such as pulling an Ubuntu image, installing all the necessary dependencies, etc. However, if we don't take that into account, we may end up having a lot of dependencies and tools that we don't actually need, which would cause our image queue to balloon. To illustrate, let's create a Dockerfile to create an image that, for instance, runs a Go application.

This is our go application, we will build a docker image to run a container with running this simple application.

Next we will write the docker image

Here, we start with a basic docker image—pretty straightforward, huh?—or base image, which is an Ubuntu 20.04 installation. Next, we install the go compiler and all the system prerequisites before compiling and running the go program. Good enough, but there are a few things to note. For instance, using a base image like Ubuntu creates an image with dependencies and tools that are not strictly necessary for our application to run, so if we're trying to optimize our, we need to remove everything that is not strictly necessary given that the sole function of our container is to run our app and nothing else.

After running the building process we end up with an image of 912MB, let’s se how can we lower that size.

We are really fortunate to have access to a variety of base images in the Docker community, such as Alphine, which is described on its Docker Hub website as "A basic Docker image based on Alpine Linux with a comprehensive package index and only 5 MB in size!" It is also minimal because no other tools are installed; it only includes what is required to execute our container. Additionally, we can utilize official images that have been created by the language's creators directly. For instance, Go has its own Docker hub where they have official images; therefore, in order to optimize our dockerfile, we'll use golang:1.16-alpine as our base image.

So we can se here that we change our base image and now that we are using a golang base image we don’t need to manually install the go compiler or any other dependency in fact, we don’t even need to use any package manager. When we run the building process we end up with a smaller image, it’s reduced almost by half.

Multistage build:

We have already seen the benefits of using official, minimalistic images; we were able to reduce our docker image almost in half, but in some cases, we can still reduce the size of our image by getting rid of components that, while necessary during the build process, can be discarded after it is complete. In this case, since we are running a Go application, which requires compilation before execution, components that, once our code is completed, are no longer needed can be removed.

We're going to use the following Dockerfile to demonstrate how we can implement multi stage in this situation. Multistage builds can help us divide our code into numerous stages and copy just the items that are truly necessary from previous stages.

As we can see in this Dockerfile we have split the building process in two parts separated by the FROM command, basically every time we use this command we start a new stage and we can tag it using “AS <tag name>” so we can use it later in the code.

The other important thing we can do with multistage is copying things from previous stages, in this dockerfile we can se in our COPY line that we use the –FROM flag, that flag is used to tell Docker from what stage we want to make the copy, in this case we cant to copy the resultant binary from the compilation in the first stage tagged as build.

And last but least important on the last stage we’re not using an alphine base image, instead we’re using a distrortionles debian image, if Alphine is minimal and really small a distortionless image can be even smaller by a 50% and is wey more secure since it doesn’t even has a package manager or any other thing besides only the necessary to run your application.

So comparing the 3 docker image we can end up with this:

As we can see we got from almost 1GB image to a smaller 26mb image.

We can conclude...

In this post, I've provided two methods for reducing the size of your Docker images. These methods are useful since they allow us to run multiple images concurrently. There are many other ways to optimize your images, including reducing the layers so you can benefit from caching during build time. For the time being, I'll stop here, but perhaps in a later post I'll go over how to deploy your images to Docker Hub and even better, how to manage your own Docker private registry.

Also I’m leaving a github repository with this images and the go app so you can play with it.

Read more of Jeffrys entries Docker 101: Let's write our first Docker image!