A Deep Dive into Container Images - Part 1

infra6 min

byLucas Santos

This page was machine translated. Read original / Suggest a fix

Part 1 of 3 of the series A deep dive into container images

I recently read a series of articles by Scott Coulton123 on Medium about how to pick base images for your containers. So I decided to write these articles so other people can also understand how to best choose where to start building their containers with Docker.

Base Images#

When we start building our images, the first thing we have to worry about is: what base image are we going to use for our application?

For example, I build a lot of images that use Node.js, so my natural choice would be to use a base image from Node itself, like node:14, which is an official image on DockerHub.

But I could just as easily build my own image with Node installed as a tool instead of having it natively from my base image. To do that, I just need to pick a different base image that only contains the operating system. We could use the Debian image and then write a Dockerfile like this one:

FROM debian:buster
RUN sudo apt update \
&& sudo apt upgrade -y \
&& sudo apt install -y curl \
&& curl -sL https://deb.nodesource.com/setup_14.x | bash - \
&& sudo apt install -y nodejs

Which isn’t too different from what’s done in the official image. But are we actually building an image that has the basic requirements to be used by other people?

What I’m getting at with this introduction is that we have to think way beyond the base image. Any Docker image needs to be designed for the best possible scalability and usability. That comes down to the following requirements:

  • The image size is small enough
  • The application running inside the image performs well
  • Security is guaranteed inside the base image

To tackle all of these, we’ll work with compiled languages, like Golang, in the next part of this article, and in the last part we’ll work with dynamic languages, like JavaScript or Python.

Types of base images#

To leave the choice entirely up to you, the reader, I won’t say one operating system is better than another. That’s a completely personal choice. Instead, we’ll work with three naming conventions:

  • full images
  • slim images
  • alpine images

Full Images#

departure from Euromax terminal, 400 meters long

Photo of a ship loaded with containers by Andrey Sharpilo / Unsplash

full images (or complete images) are images that contain a full extent of an operating system. For example, a debian-full image is an image that has the entire Debian Linux distribution, with every package and module loaded.

This type of image is the best type to start with, and probably the simplest way to get an application up and running. That’s because it has every tool and package already installed by default. In this article we’ll just compare the images and won’t worry much about the applications we’ll run inside them.

One of the big problems with full images is that, since they have so many packages installed by default, that also means a lot of packages with known vulnerabilities come installed by default too. That opens the door for an attack to happen.

Docker Hub itself has a vulnerability scanning tool you can use straight from the site, through the Tags tab. As shown in the image below.

Docker Hub's vulnerability detection

As you can see, the image itself already has vulnerabilities you’re “inheriting” before you’ve even deployed any of your own code.

Slim Images#

Photo of a label that reads "the slim" by Mat Reding / Unsplash

A slim image is a somewhat smaller image. Every slim image only has the packages the operating system needs to function, and you’re expected to install any extra packages your application needs on top of it. Right now, only Debian has a slim image.

One direct benefit of using slim images is that, with fewer packages, you have fewer dependencies that could be exposed to vulnerabilities. Take a look at an example.

Vulnerabilities of a slim image

Another interesting gain is that, precisely because they’re “cleaner”, slim images are much smaller in size than a full image. In more extreme cases, a slim image can be up to 50% smaller than a full image. Take a look at the comparison between debian:buster and debian:buster-slim.

Size of the slim image of Debian Buster

Now look at the size of the complete image

Size of the full image of Debian Buster

Notice that if we take the amd64 architecture, which is the most common for 64-bit processors, and compare the full version at 48.06MB against the 25.84MB of the same image, only in the slim version, that’s a 47% reduction in image size. All of that just from removing unused packages.

Alpine Linux Images#

Photo of the Alps by Benni Asal / Unsplash

Alpine Linux images are the most optimized type of image out there. This kind of OS was built from the ground up to be a container-native operating system. The main difference between this type of image and a traditional image is that this system doesn’t use glibc. Alpine relies on a different library called musl libc. On top of that, by default, Alpine only ships with the base packages installed, meaning anything else your application needs, you’ll have to install manually.

That might sound a bit complicated, but look at how much we cut down the number of vulnerabilities just by trimming the packages. Notice there isn’t a single critical vulnerability.

Vulnerabilities of an Alpine image

On top of that, an alpine image is less than 6MB in total size. With newer images, that size can get down to just over 2MB. That’s a reduction of more than 95% in overall image size.

Size of an Alpine Linux image

The downside of working with an Alpine image is that it’s a completely different system. You’ll have to learn to work with its native package manager, and since it doesn’t use the standard Linux libraries, chances are most of your software will need to be recompiled using Alpine’s tools.

Scratch Images#

Photo of a black sheet of paper with nothing written on it and a white pencil on top by Kelly Sikkema / Unsplash

Finally, let’s take a look at scratch type images. When we use scratch as a base image, we’re telling Docker that we want the next command in our Dockerfile to be the first layer of our filesystem inside that image.

That means we don’t have any package manager, not even packages. Just an empty filesystem. You can start by using FROM scratch, since this is a reserved namespace on DockerHub and doesn’t have any kind of vulnerability or package installed.

FROM scratch
ADD rootfs.tar.xz /
CMD ["bash"]

Most operating system images start out this way, since most of them are considered empty images. They start from absolute zero.

Conclusion#

We’ll explore a bit more about images and put these concepts into practice in the next articles, writing an application in Go.

Stay tuned for more!

Footnotes#

  1. I Chose You Container Image - Part 1

  2. I Chose You Container Image - Part 2

  3. I Chose You Container Image - Part 3