Docker scratch base use cases

Docker scratch base use cases

The scratch base (FROM scratch) is a Docker’s reserved blank image, or an empty filesystem, that acts like an empty layer to create parent images. It is like an empty canvas. It’s where you start building containers from scratch (no pun intended!), adding only what your application needs, making it super minimal. This gives us complete control over what can be shipped inside the container.

This post will show

  • Sharing files between images during build time.
  • Use container registries like Docker hub or AWS ECR to store files as Docker layers.
  • Using the scratch base layer for deploying single-binary applications.

Sharing files between images

When building images, Docker gives us the ability to pull files from other images (remote or local) using the --from= option with the COPY instruction in Dockerfile as follows:

FROM ubuntu:latest
COPY --from=foo:1.2 /content /content
# Other build commands ...

What’s neat about this is:

  1. You can cherry-pick specific files from another image and toss them into our new image.
  2. You can even pick files from a specific image’s tag by specifying in its tag. So if you have two tags for the same image image “foo” like: foo:latest and foo:1.2, you can pull files from the version 1.2 on the fly.

Use container registries as remote storage

Since we can copy files from remote images into a new Dockerfile, we can actually store project files in the container registry as container images. We can do something like this:

Create a scratch image

We don’t need a distro based image since we’re going to use this image just to store files. I’m going to copy a file called foo that has some text:

image_foo:latest
FROM scratch
COPY foo /foo
# copy files as you wish...

Build and push the image to your container registry

Here I’m pushing to my Docker hub account:

user@localhost$ docker build -t image_foo:latest .
user@localhost$ docker tag image_foo:latest shakir85/image_foo:latest
user@localhost$ docker push shakir85/image_foo:latest

Copy from the scratch image.

To access the remote file in other docker files:

FROM nginx:latest
COPY --from=shakir85/image_foo:latest /foo /foo

Test the new container:

user@localhost$ docker run -it nginx_foo:latest bash
root@f2924a57e71e:/# cat /foo
This is foo file

You might wonder, why would you do that? Why not just use object storage like AWS S3 or even a Git repo to store and fetch files dynamically?

Well, it’s just an additional option that comes with its own set of benefits:

  1. You don’t need to fuss with remote storage authentication since you’re already authenticated with your container registry. This is super handy in CI/CD pipelines.

  2. It brings reproducibility to the table. Every image in your pipeline can fetch files from a single source (image) that the pipeline is already has access to. This consistency makes it easy to replicate builds.

But, be aware that poorly planning or excessively using this method across many containers can turn it into a dependency hill, and you might end up shooting yourself in the foot. So, use it wisely and be sure to document your approach.

Use scratch base for single binary containers

The scratch base layer can be an excellent choice for creating single-binary containers when your application and its dependencies are entirely self-contained within a one or a handful of executable files.

The catch is that, since the scratch layer is essentially an empty filesystem, your application must be statically compiled. Also, keep in mind that because your application is going to be statically compiled, a small-sized container is not guaranteed. The container’s size really depends on the type and requirements of the application and the number of libraries or dependencies that need to be included (compiled) along with the application.

That being said, let’s take a look at this simple hello-world C code:

hello.c
#include <stdio.h>

int main(void) {
     printf("hello world");
     return 0;
}

Compile it using --static flag to include the required libraries in the final executable:

gcc -o hello --static hello.c

Create the Dockerfile:

FROM scratch
COPY hello /
CMD [ "/hello" ]

Build the image and run the container:

docker build --no-cache -t my-scratch:latest .
docker run --rm my-scratch

If we try to send an echo command to the container, it will fail because there is no such a binary or application in the scratch container

docker run --rm my-scratch echo hi

docker: Error response from daemon: failed to create shim task: OCI runtime create failed: 
runc create failed: unable to start container process: exec: 
"echo": executable file not found in $PATH: unknown.

Bonus

Say, for example, we want to add the echo command to the scratch container. Since echo is a compiled binary, we may think we can copy it from another parent image into the scratch image using COPY --from=ubuntu:latest /usr/bin/echo / in the Dockerfile.

However, since echo is a dynamically linked binary, the echo binary will need other dependencies in order to run. We can use the ldd command1 to view what libraries echo depends on. Let’s jump into an Ubuntu container and examine that:

docker run -it --rm ubuntu:latest bash
root@cd3dd0afeb53:/# which echo
/usr/bin/echo

root@cd3dd0afeb53:/# ldd /usr/bin/echo
    linux-vdso.so.1 (0x00007ffe99d81000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fecf34c3000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fecf36f9000)

The output shows the echo command’s dependencies that must be in the container, which without them, the echo command will not work.


  1. This Reddit post shows some interesting facts about ldd↩︎