Tiny Docker images that believed in themselves*

[reference to the American children's story 'The Little Engine That Could' — note from the translator]*

Tiny Docker images that believed in themselves*

How to automatically create tiny Docker images for your needs

An unusual obsession

For the past couple of months, I've been obsessed with the idea: how small can a Docker image be while still keeping the application running?

I understand, it's a strange idea.

Before delving into the details and technicalities, I would like to explain why this issue grabbed my attention and how it relates to you.

Why size matters

By reducing the content of a Docker image, we also reduce the list of vulnerabilities. Additionally, we make images cleaner, as they only contain what is necessary to run applications.

There's one more small advantage — images download a little faster, but to me, that's not so important.

Note: If you're concerned about size, Alpine images are small by themselves and will likely suit your needs.

Distroless images

The Distroless project offers a collection of basic 'distroless' images that do not contain package managers, shells, or other utilities you might be accustomed to in the command line. As a result, you cannot use package managers like pip and apt with it:

FROM gcr.io/distroless/python3
RUN  pip3 install numpy

Dockerfile using distroless Python 3 image

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM gcr.io/distroless/python3
 ---> 556d570d5c53
Step 2/2 : RUN  pip3 install numpy
 ---> Running in dbfe5623f125
/bin/sh: 1: pip3: not found

Pip is not included in the image

Usually, this problem is resolved by using multi-stage builds:

FROM python:3 as builder
RUN  pip3 install numpy

FROM gcr.io/distroless/python3
COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.5/

Multi-stage build

As a result, the resulting image is 130MB. Not too bad! For comparison, the default Python image weighs 929MB, while the 'slimmed-down' version (3.7-slim) is 179MB, and the Alpine image (3.7-alpine) is 98.6MB, while the basic distroless image used in this example is 50.9MB.

One could rightly point out that in the previous example, we're copying an entire directory /usr/local/lib/python3.7/site-packages, which may contain unnecessary dependencies. Although it's clear that the size difference varies among all existing base Python images.

At the time of writing, Google distroless does not support many images: Java and Python are still in the experimental stage, and Python exists only for 2.7 and 3.5.

Tiny images

Let’s return to my obsession with creating small images.

In fact, I wanted to see how distroless images are structured. The distroless project uses Google’s build tool bazel. However, installing Bazel and writing my own images required quite a bit of effort (and to be completely honest, reinventing the wheel is fun and educational). I wanted to simplify the creation of minimal images: the act of creating an image should be as straightforward as possible, mundane. No configuration files, just one line in the console: simply build an image for.

So, if you want to create your own images, know this: there is a unique Docker image, scratch. Scratch is an 'empty' image, it contains no files, although it weighs by default — wow! — 77 bytes.

FROM scratch

The scratch image

The idea behind the scratch image is that you can copy any dependencies from the host machine into it and either use them within the Dockerfile (just like copying them into apt and installing from scratch), or later when the Docker image has materialized. This allows for complete control over the contents of the Docker container, and thus, complete control over the size of the image.

Now we need to somehow gather these dependencies. Existing tools like apt allow downloading packages, but they are tied to the current machine and ultimately do not support Windows or MacOS.

So I set out to create my own tool that would automatically assemble a base image of the smallest possible size that could run any application. I used Ubuntu/Debian packages, pulling them directly from the repositories and recursively finding their dependencies. The program was supposed to automatically download the latest stable version of the package, minimizing security risks.

I named the tool fetchy, because it... finds and brings... what you need [from the English 'fetch', meaning 'to bring' — translator’s note.]. The tool works via the command-line interface, but it also offers an API.

To build an image using fetchy (this time let’s take the Python image), you just need to use the CLI like this: fetchy dockerize python. You might be asked for the target operating system and codename, since fetchy currently only uses Debian and Ubuntu-based packages.

You can now choose which dependencies are completely unnecessary (in our context) and exclude them. For example, Python depends on Perl, although it works perfectly fine without having Perl installed.

Results

The Python image created with the command fetchy dockerize python3.5 weighs only 35MB (I'm more than sure it can be further slimmed down in the future). Thus, we managed to shave off another 15MB from the distroless image.

All the images collected so far can be viewed here.

Project — here.

If you're missing features, just create a request — I'd be happy to help 🙂 Even more, I am currently working on integrating other package managers into fetchy, so the need for multi-stage builds will no longer exist.

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster