Containers and Virtualization DockerSlim 
 

Minifying container images with DockerSlim

Dieting

DockerSlim minifies your Docker container images up to 30x and adds security, too. By Chris Binnie

Although not always the first choice, Docker Engine will be used extensively as the default container runtime in Kubernetes and OpenShift on cloud-native infrastructure for some time to come. So, what would you say if I told you that you could automatically reduce the size of your Docker images 30x? Why are smaller container images a good thing? When it comes to containers, less is more: Your containers start quicker, they contain fewer security threats and bugs, they take up less storage, and they take less time to download and upgrade. In other words, it's a really good idea to keep your container images trim.

A sophisticated piece of open source software allows you to do just that; it's called DockerSlim [1]. The clever tool also makes a welcome foray into further securing your container images. The DockerSlim docs declare: "Don't change anything in your Docker container image and minify it by up to 30x (and for compiled languages even more) making it secure too!"

As well as clear and useful docs in the GitHub page README file, you'll also appreciate the slick introduction on a single-page website [2] that has a simple and effective example (before pointing you back to the GitHub repository again). There, you'll also find a useful video that runs through some command-line options to get you started.

In this article, I look at how to automate the slimming of your container images and how to start tweaking the security profiles of your containers, as well.

Trimming the Fat

Before looking at how to get DockerSlim working, I'll show you a few examples on how trim you can expect certain container images to become. The GitHub page has some eye-watering benchmarks that you might expect to achieve with a number of languages. The Python examples in Listing 1 from the README file [1] are impressive, although Node.js, Ruby, Golang, Java, PHP, and other languages can be expected to achieve similar results.

Listing 1: Python Minify Results

from ubuntu:14.04 - 438MB => 16.8MB (minified by 25.99X)
from python:2.7-alpine - 84.3MB => 23.1MB (minified by 3.65X)
from python:2.7.15 - 916MB => 27.5MB (minified by 33.29X)
from centos:7 - 647MB => 23MB (minified by 28.57X)
from centos/python-27-centos7 - 700MB => 24MB (minified by 29.01X)
from python2.7:distroless - 60.7MB => 18.3MB (minified by 3.32X)

Teeny Tiny

Although you can install the binaries required to run DockerSlim directly, for obvious reasons (because you're dealing with container images), you should focus on using the dslim/docker-slim Docker image that's been made available instead.

I'm using Linux Mint 19 (which uses Ubuntu 18.04 LTS under the bonnet), so to install Docker I'll use the command:

$ apt install docker.io

I would recommend, for anything other than testing, to use Docker CE [3], so you get the latest stable release with the latest features.

The super-slight DockerSlim provides lots of options, but to get started, I'll pull down one of the most popular images locally and try and minify it:

$ docker pull dslim/docker-slim
[snip...]
Status: Downloaded newer image for dslim/docker-slim:latest

Now that it is available locally, I'll pull down the nginx image, one of the most popular images in use today. In Listing 2, you can see the two images I pulled from Docker Hub and their disk sizes. In this case, nginx is coming in at an unwieldy 126MB.

Listing 2: Beginning Image Sizes

REPOSITORY         TAG          IMAGE ID          CREATED       SIZE
dslim/docker-slim  latest       2622a843b5f5      3 weeks ago   21.3MB
nginx              latest       231d40e811cd      4 weeks ago   126MB
KE:

Having clicked the link for the latest version of the Nginx Dockerfile [4] on the Docker Hub website [5], I now have the ability to test DockerSlim.

By reputation, Nginx is not only a super-lightweight champion in the world of web servers, but because of the sheer number of downloads it experiences from Docker Hub (and therefore the scrutiny it is under), I would expect it to be extremely trim already. As a result, running it past DockerSlim's watchful eye is a big challenge.

The Dockerfile is too long (103 lines) to display here; I chose the "Raw" option for the Dockerfile on GitHub, copying its contents and then saving it locally to a file called "Dockerfile." From within the same directory as the downloaded Dockerfile (as root user, and not a less-privileged user, out of laziness mostly), the next command should do something interesting (Listing 3):

$ docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock dslim/docker-slim build nginx

Listing 3: Putting DockerSlim to the Test

docker-slim[build]: info=http.probe message='using default probe'
docker-slim[build]: state=started
docker-slim[build]: info=params target=nginx continue.mode=probe
docker-slim[build]: state=image.inspection.start
docker-slim[build]: info=image id=sha256:231d40e811cd970168fb0c4770f2161aa30b9ba6fe8e68527504df69643aa145 size.bytes=126323486 size.human=126 MB
docker-slim[build]: info=image.stack index=0 name='nginx:latest' id='sha256:231d40e811cd970168fb0c4770f2161aa30b9ba6fe8e68527504df69643aa145'
docker-slim[build]: info=image.exposed_ports list='80'
docker-slim[build]: state=image.inspection.done
docker-slim[build]: state=container.inspection.start
docker-slim[build]: info=container status=created name=dockerslimk_1_20191223125148 id=d87e572be9182325c7d0af5cd672648a3ea13938013fd31c8627cf948d66015b
docker-slim[build]: info=cmd.startmonitor status=sent
docker-slim[build]: info=event.startmonitor.done status=received
docker-slim[build]: info=container name=dockerslimk_1_20191223125148 id=d87e572be9182325c7d0af5cd672648a3ea13938013fd31c8627cf948d66015b target.port.list=[32770] target.port.info=[80/tcp => 0.0.0.0:32770] message='YOU CAN USE THESE PORTS TO INTERACT WITH THE CONTAINER'
docker-slim[build]: state=http.probe.starting message='WAIT FOR HTTP PROBE TO FINISH'
docker-slim[build]: info=continue.after mode=probe message='no input required, execution will resume when HTTP probing is completed'
docker-slim[build]: info=prompt message='waiting for the HTTP probe to finish'
docker-slim[build]: state=http.probe.running
docker-slim[build]: info=http.probe.ports count=1 targets='80'
docker-slim[build]: info=http.probe.commands count=1 commands='GET /'
docker-slim[build]: info=http.probe.call status=200 method=GET target=http://172.17.0.3:80/ attempt=1  time=2020-11-11T12:52:01Z
docker-slim[build]: info=http.probe.summary total=1 failures=0 successful=1
docker-slim[build]: state=http.probe.done
docker-slim[build]: info=event message='HTTP probe is done'
docker-slim[build]: state=container.inspection.finishing
docker-slim[build]: state=container.inspection.artifact.processing
docker-slim[build]: state=container.inspection.done
docker-slim[build]: state=building message='building minified image'
docker-slim[build]: state=completed
docker-slim[build]: info=results status='MINIFIED BY 16.16X [126323486 (126 MB) => 7817018 (7.8 MB)]'
docker-slim[build]: info=results  image.name=nginx.slim image.size='7.8 MB' data=true
docker-slim[build]: info=results  artifacts.location='/bin/.docker-slim-state/images/231d40e811cd970168fb0c4770f2161aa30b9ba6fe8e68527504df69643aa145/artifacts'
docker-slim[build]: info=results  artifacts.report=creport.json
docker-slim[build]: info=results  artifacts.dockerfile.original=Dockerfile.fat
docker-slim[build]: info=results  artifacts.dockerfile.new=Dockerfile
docker-slim[build]: info=results  artifacts.seccomp=nginx-seccomp.json
docker-slim[build]: info=results  artifacts.apparmor=nginx-apparmor-profile
docker-slim[build]: state=done
docker-slim[build]: info=report file='slim.report.json'

Lo and behold, according to Listing 3, the line reporting

MINIFIED BY 16.16X [126323486 (126 MB) => 7817018 (7.8 MB)]

would be a remarkable achievement – if true. As shown in Listing 4, I check the Docker Engine output, which shows further confirmation of the DockerSlim report.

Listing 4: Output of docker images

REPOSITORY         TAG       IMAGE ID         CREATED          SIZE
nginx.slim         latest    3557e77e8e91     5 minutes ago    7.82MB
dslim/docker-slim  latest    2622a843b5f5     3 weeks ago      21.3MB
nginx              latest    231d40e811cd     4 weeks ago      126MB

The remarkable 7.82MB nginx.slim image needs testing now, so I'll fire up a container with that image and see what happens (hit Ctrl+C to kill the container):

$ docker run -it -p80:80 nginx.slim

By pointing my browser at TCP port 80 on localhost, I can see the very welcome and familiar result of running an Nginx instance (Figure 1). I think anybody would agree that the reduction from 126MB to less than 8MB (as seen in Listing 4) is a sight to behold.

The minuscule, super-fit version of Nginx on display uses an image weighing in at a teeny, tiny 7.82MB!
Figure 1: The minuscule, super-fit version of Nginx on display uses an image weighing in at a teeny, tiny 7.82MB!

You should test your container images carefully before deploying them into critical environments. The README file talks about debugging and checking the process, so you have some support from the documentation if an image ever misbehaves.

DockerSlim needs to talk to Docker Engine in a specific way; usually using the Unix socket is easiest. Therefore, the command I used to minify my Nginx image contained the -v /var/run/docker.sock:/var/run/docker.sock option. The documentation says:

If you are using the docker-slim container make sure you run it configured with the Docker IPC information, so it can communicate with the Docker daemon. The most common way to do it is by mounting the Docker Unix socket to the docker-slim container.

It goes on to note that some services like GitLab won't expose the socket in the same way, so you can use environment variables such as DOCKER_HOST to get around this, if required. I'm paraphrasing, so check out the docs for more details.

Super Lightweight Champion

Now that you're suitably impressed, I'll look at some of the (many) features that DockerSlim brings to the table, beginning with the DockerSlim output (see Listing 3 again). You'll note that DockerSlim has quite rightly spotted that TCP port 80 is exposed within the Dockerfile – because Nginx is a web server. There are further details on how to interact with DockerSlim when it runs in the docs, because you can connect to a temporary container that's used while DockerSlim weaves its magic trimming down an image to check that things are working as expected.

Security!

Earlier I promised that DockerSlim can also help your Docker container security posture. If you want, you can go a step further and add a security profile to your build. With this in mind, I'll take a brief look at seccomp (secure computing) profiles [6].

seccomp provides clever functionality from within the Linux kernel and allows a number of settings to be passed into your container to restrict its access to the host machine on which it's running. As stated on the Linux man page [7], seccomp rules are enforced in a way that limits a process from performing a very small number of operations (e.g., read, write, sigreturn, and exit). The use of fine-grained rules, then, make it impossible for a rogue process to get access into a host machine's innards in a way other than those explicitly permitted.

Knowing the power of Linux, you probably won't be surprised to learn that the Android operating system, Google Chrome, Firefox, and even OpenSSH all use seccomp to enforce important security rules. More accurately, some of the seccomp adopters mentioned use seccomp-bpf, which is a clever way of extending seccomp with the popular Berkeley Packet Filter rules to offer more flexibility and help mitigate any performance concerns. Table 1 lists examples of what seccomp limits and, conversely, permits.

Tabelle 1: Some Kernel Syscalls for seccomp

Syscall

Function

mount

Mounts disk volumes

clock_adjtime

Gives permission to change the host machine's time and date

init_module

Affects importable kernel modules

keyctl

Controls access to the kernel keyring, which contains encryption keys and other sensitive items, such as security tokens

kexec_load

Readies a new kernel to be run at a later time (potentially with malicious code)

quotactl

Prevents containers from altering their own resource quotas (disk space limits, in this case)

reboot

Allows containers to reboot the host machine (usually not a very clever thing to allow)

For a full list of the more than 300 seccomp profiles enabled by default in Docker, check out the Moby project's GitHub repository [8]. The default Docker seccomp profile then denies access to around 44 of the system calls [9], which are "effectively blocked because they are not on the whitelist."

Now it's time to see how DockerSlim can help make use of seccomp effectively. The docs offer this syntax to follow as a simple example:

$ docker run --security-opt seccomp:<docker-slim directory>/.images/<YOUR_APP_IMAGE_ID>/artifacts/<your-name-your-app>-seccomp.json <your other run params> <your-name>/<your-app>

To translate that to the Nginx example from earlier, first make sure that you have seccomp set up for Docker when you installed it initially and run:

$ grep CONFIG_SECCOMP= /boot/config-$(uname -r)

If you see CONFIG_SECCOMP=y, then all is well; otherwise, some searching online might be needed relative to your software version or system.

After you've run the build command shown earlier to minify the nginx image, you should be able to find your auto-generated seccomp profile with:

$ find /var/lib/docker -name *.json

If your system uses strange filesystem paths, you should change the path shown. The two results that I'm interested in are:

The creport.json file is the report I generated with the docker-slim profile --report nginx.slim command used earlier to generate some interesting information related to the process through which DockerSlim runs. Incidentally, if you spot a file ending with the extension .fat, that's the reverse-engineered Dockerfile that the clever DockerSlim has created while going about its business. In my case, the file is named: /var/lib/docker/volumes/docker-slim-state/_data/images/2343aa145/Dockerfile.fat

Now that the generated seccomp profile has been found, Listing 5 shows what the top of the file looks like, so you can differentiate it from other files you've seen. In my case, the JSON file was called nginx-seccomp.json. Isn't having a profile generated for you so you can refine it handy?

Listing 5: Top of a seccomp Profile

01 {
02   "defaultAction": "SCMP_ACT_ERRNO",
03   "architectures": [
04     "SCMP_ARCH_X86_64"
05   ],
06   "syscalls": [
07     {
08       "names": [
09         "lstat",
10         "exit_group",
11         "arch_prctl",
12         "chdir",
13         "unlink",
14         "geteuid",
15         "setitimer",
16         "write",
17         "sendmsg",
18         "mprotect",
19         "capget",
20         "getuid",
21         "wait4",
22         "pread64",
23         "Capset",
24 [snip...]

Unfortunately, I can't go too deep into seccomp in this article, but it would be sensible to look online to learn a few security tips about how many syscalls should be used by your containers.

Once you've tweaked a profile, the Docker documentation offers this command format for running a container with a specific seccomp profile:

$ docker run --rm -it --security-opt seccomp=/<path>/profile.json nginx.slim

You can use the generated security profile with both your slimmed image and the original. It might help you spot when a version upgrade changes a container's system access requirements. The esteemed Jessie Frazelle has a nice piece about seccomp that is well worth a read [10].

Trouble Ahead

If you get stuck, fret not. seccomp has been available since Docker version 1.10 [11], and the comprehensive docs [12] offer some useful troubleshooting tips, including tips about Nginx images that might be useful at some point.

The End Is Nigh

Doubtless this tool is exceptionally valuable to a containerized estate, but further investigation clearly is required into how stable DockerSlim might be for production services. However, the docs suggest the DockerSlim author is happy to give it the thumbs up for production use, so I trust you'll soon be trying this highly accessible open source tool.