DEV Community

Tony Metzidis
Tony Metzidis

Posted on

Build 100kB Docker Images from Scratch

­čôô 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


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


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

The Dockerfile


# this prevents our host binary from sneaking into the build


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

FROM scratch
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 '[^ ]+$'

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 

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.

Top comments (8)

suntong profile image

Not trying to be discouraging, but I've gone along that path before, and found the solution only good enough for hello-world toy programs. Real programs interact with OS in all kinds of ways. I.e., I'd like to see how a real-world program that interacts with OS in all kinds of ways can benefit from such 100kB Smaller images. That'll be really interesting. Thanks.

tonymet profile image
Tony Metzidis

Good point. With a little diligence it can be done.

Here's a broader example building a rest api into a tiny container

Later on I'll show tooling you can use to help identify dependencies methodically so it seems less like voodoo

suntong profile image

Loving it -- looking forward to your updates.

david_j_eddy profile image
David J Eddy

Nicely done Tony. I look forward to the next article in the series.

paulotruta profile image
Paulo Truta

This is top notch! Eagerly awaiting the next one! ;)

tonymet profile image
Tony Metzidis

did a fast-follow on this, more to come in the coming posts

adaamz profile image
Adam ┼Żurek

Why you use Docker? If you use only tar it is much faster.

tonymet profile image
Tony Metzidis

agreed about tar. Using docker here as an illustration on how to build optimized Docker images.