Build 100kB Docker Images from Scratch

Posted on Mon 06 May 2019 in docker

📓 The Gist

You may think your 100mB Alpine images are small--but how about 100kB? Smaller images ship more quickly, and contain fewer attack vectors. Moreover, by optimizing images, you discover and isolate exactly what is needed for your app to run.

Let's Optimize.

There are two key characteristics of scratch-based docker images: 1. The Dockerfile has two build stages: * a builder--which contains all of the build dependencies including source, libraries and tools and.. * a final image, containing the binary and any run-time dependencies (config files, certificates and dynamically linked libraries) 2. The final image is FROM scratch -- the empty docker image

With this approach, your run-time image will contain exactly what is needed for your app to run -- no additional config files, daemons or libraries that could be misconfigured or exploited.

Let's go over a basic static Hello World scratch image. We'll use C since it requires the fewest dependencies and produces tiny ELF object binaries.

Create Hello World

hello.c

#include <stdio.h>
int main(void){
    puts("Hello World\n");
}

Makefile

hello: hello.c
    gcc -o $@  $< -static 

The Dockerfile

.dockerignore

# this prevents our host binary from sneaking into the build
hello

Dockerfile

FROM alpine:latest as builder
WORKDIR /build
RUN apk update && \
    apk add gcc make libc-dev
COPY .  ./
RUN make

FROM scratch
WORKDIR /
COPY --from=builder /build/hello .
CMD ["/hello"]

Notice that we have two FROMs, one called builder. The final image will be our runner. Using COPY --from we can select which files go into the scratch image. With a static ELF binary we only need /build/hello

Build and Run

$ docker build . -t hello-scratch|tail -n 1
Successfully tagged hello-scratch:latest
$ docker run hello-scratch                 
Hello World

Our Final Image is 82.7kB

$ docker images |grep hello-scratch | egrep -o '[^ ]+$'
82.7kB

What's Inside?

Using docker save you can inspect the image and see that layer.tar only contains a single file: hello

$ mkdir hello-scratch && cd hello-scratch 
$ docker save hello-scratch |tar -x
$ ls
3e69d91b5842be72dcd4175adcf218a03f78826504be6a46ed41c099e97520e8.json  e599e214ce17b356493f9524fa57f7ef816d21dd78020019196020c770a39954  manifest.json  repositories
$ ✗ tar -tf e599e214ce17b356493f9524fa57f7ef816d21dd78020019196020c770a39954/layer.tar 
hello

Next Steps

In upcoming posts I'll show some more sophisticated examples. This process is easiest with statically-compiled apps like C, C++, golang & rust. But with the proper tooling, any image can be built assuming you collect all runtime dependencies into the final scratch image.