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:
- You can cherry-pick specific files from another image and toss them into our new image.
- 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
andfoo: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:
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:
-
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.
-
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:
#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.
-
This Reddit post shows some interesting facts about
ldd
. ↩︎