Building a small docker container

Minimise the dependencies

In previous posts I’ve written about small containers; don’t bundle a whole OS image with your app, just have the minimum necessary files and support.

The Go language makes it easy to build a static executable, so let’s use this for an example:

$ cat hello.go
package main

import "fmt"

func main() {
  fmt.Println("Hello, World")
}
$ go build hello.go
$ strip hello
$ ls -l hello
-rwxr-xr-x. 1 sweh sweh 1365448 Jun  4 13:48 hello*

We can use this as the basis of a docker container (I’m using “docker” here because it’s a very common technology that’s used by lots of people):

$ cat Dockerfile                 
FROM scratch
COPY hello /
CMD ["/hello"]

$ sudo docker build -t hello:go .
Sending build context to Docker daemon 1.369 MB
Step 1 : FROM scratch
 ---> 
Step 2 : COPY hello /
 ---> 921eb0866d50
Removing intermediate container e518d193b269
Step 3 : CMD /hello
 ---> Running in 0080bf495dd0
 ---> 0480444b8e0a
Removing intermediate container 0080bf495dd0
Successfully built 0480444b8e0a

$ sudo docker run --rm -t hello:go 
Hello, World

$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
hello               go                  0480444b8e0a        58 seconds ago      1.365 MB

That’s a nice small container! Let’s see what it consists of

$ mkdir EXTRACT         
$ cd EXTRACT
$ sudo docker save 0480444b8e0a | tar xf -
$ for a in */layer.tar
do
tar tvf $a
done
-rwxr-xr-x 0/0         1365448 2016-06-04 13:48 hello

So the whole container image contains just one file.

Compare that to a more traditional container built with a Debian base:

$ cat Dockerfile                  
FROM debian:jessie
COPY hello /
CMD ["/hello"]
$ sudo docker build -t hello2:go .
Sending build context to Docker daemon  1.37 MB
Step 1 : FROM debian:jessie
Trying to pull repository docker.io/library/debian ... jessie: Pulling from library/debian
23286f48d129: Pull complete 
7a4c9a4d5e7a: Pull complete 
Digest: sha256:2ca1d757fce75accad6ff84339c3327c7aa96ad6e7b7d6fdde22b2a537fac703
Status: Downloaded newer image for docker.io/debian:jessie

 ---> 7a4c9a4d5e7a
Step 2 : COPY hello /
 ---> 53afe819cb68
Removing intermediate container b502f926d128
Step 3 : CMD /hello
 ---> Running in 47c55d108c18
 ---> 047bf1fe5c58
Removing intermediate container 47c55d108c18
Successfully built 047bf1fe5c58

Yikes, we’ve just pulled in external content. That’s a “no no” from a controls perspective.

The container is now a lot bigger:

$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
hello2              go                  047bf1fe5c58        About a minute ago   126.4 MB
docker.io/debian    jessie              7a4c9a4d5e7a        11 days ago          125.1 MB

$ mkdir EXTRACT 
$ cd EXTRACT
$ sudo docker save 047bf1fe5c58 | tar xf -
$ for a in */layer.tar
> do
> tar tf $a
> done | wc -l
8249

8,249 files are now present in this container! That’s a larger footprint for an attacker to exploit; if an RCE is found in your ‘hello’ app then they now have a full shell environment to exploit. It’s also more complicated to patch and manage; if a bug is found in the underlying ‘jessie’ image then that needs updating and any image that depends on it needs to be restarted, as well.

A PaaS such as Cloud Foundry tries to make the layering a bit more invisible; the underlying “OS image” is part of a stem cell. Your application and dependencies are part of a droplet and are created using buildpacks. In some ways this makes it easier to manage; the team managing cloud foundry can update/replace the stem cells and your app shouldn’t notice; it just gets restarted with the new stem cell. However it does mean there’s a full OS image hiding inside the container, which could be used by an attacker.

Conclusion

Now, admittedly this is a simple case; no networking, no web listener, no additional content. But we can see how simple it is to build a container with no hidden “layered” dependencies. You need to spend more time working out your minimal dependencies first (statically compiled languages such as “go” make this easier, but it can be done with traditional binaries such as httpd, just by following the library dependency chains).

The end result is a smaller, more secure image. There’s less to assist attackers (no unnecessary files so harder to get a shell) and simpler to maintain (smaller dependencies).

It also helps segregate the application from the OS by making the dependencies explicit; switching from a Debian base to a CentOS base won’t suddenly cause application breakage because there is no Debian or CentOS base :-)