Andrew Egeler
Learning how things work.

This Blog is an 8MB Docker Container

TL;DR: I used a FROM scratch docker build to make a website (including web server) that uses 8MB of disk space and 5KB of memory.

%CPU %MEM    VSZ   RSS COMMAND
 0.0  0.2   8052  5220 /bin/site

IMAGE ID            SIZE
b3b83fa14acd        7.93MB

This blog is not a static site. Knowing me, I'll end up wanting to do something dynamic on it at some point, so I might as well set up for that anyway. Instead, it is a small C++ application, which currently keeps everything in memory.

I was first going to just build a statically linked binary to deploy it, but at least one of the libraries I wanted to use didn't have a static library (.a file) available in my package manager. Instead, I implemented modern technology's solution to the problem: a docker image (I run other services in docker, so this was a natural choice).

My first step was to create a Dockerfile that built the C++ application in an environment similar to my development machine. Starting from my distro's base image, installing the libraries and tools, and calling make on my project resulted in a working, running container serving my site. Because this is a small C++ application that directly serves HTTP, it even runs in just over 5k of memory.

FROM gentoo/stage3-amd64

# install libraries
# copy source in
WORKDIR /build
RUN make
# we now have a /build/bin/site executable

CMD ["bin/site"]

But this docker image has a full operating system in it. It's hundreds of MB. It doesn't need to be that big. The appropriate solution here is to use a multi-stage docker build, so the final image doesn't have all your build tools in it. Because I'm trying to be minimal, my final image is FROM scratch.

FROM gentoo/stage3-amd64 AS site-build

# install libraries
# copy source in
WORKDIR /build
RUN make
# we now have a /build/bin/site executable

FROM scratch AS site-deploy

COPY --from=site-build /build/bin/site /bin/

CMD ["/bin/site"]

Of course, if I try to run this, it blows up - the 'site' binary is dynamically linked, and it can't find any of the required shared libraries. We can see what libraries an application needs by using ldd

$ ldd /bin/bash
        linux-vdso.so.1 (0x00007fff315f1000)
        libreadline.so.8 => /lib64/libreadline.so.8 (0x00007f0ea09a8000)
        libtinfo.so.6 => /lib64/libtinfo.so.6 (0x00007f0ea096b000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f0ea07ab000)
        libtinfow.so.6 => /lib64/libtinfow.so.6 (0x00007f0ea076c000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f0ea0b11000)

So, on my machine, to run 'bash' in a from-scratch container, I also need to copy these libraries into the container. While I could do this manually by adding a bunch of COPY commands to the Dockerfile, I'm also a programmer, which means I'm allergic to manual work. Instead, I threw together a tiny program to parse the output of the ldd command and copy all the required files into a single target directory: Not-documented-or-cleaned-up Github repository here.

Using this tool, in the -build image, we copy everything we need into a /dist folder, and then just copy that entire folder into the scratch container.

FROM gentoo/stage3-amd64 AS site-build

# install libraries
# copy source in
WORKDIR /build
RUN make
# we now have a /build/bin/site executable
RUN cp bin/site /dist/bin
RUN ldd-dep-cp bin/site /dist/lib64

FROM scratch AS site-deploy

COPY --from=site-build /dist/ /

CMD ["/bin/site"]

The final result of this is a minimal image containing only what is required for this program to run, weighing in at just 7.93MB.