Tony Metzidis
Posted on May 6, 2019
π 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:
- 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)
- 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 FROM
s, 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.
Posted on May 6, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.