Docker images are created from a Dockerfile that defines a base image and a series of instructions that add your own filesystem layers. What happens if you want to make your own “base image” though? Here’s how to start from scratch and create a complete container filesystem from the ground up.
What’s An Image?
Docker images generally use a popular Linux distribution as their base image. If you’ve written FROM ubuntu:latest, FROM debian:latest or FROM alpine:latest, you’ve used an operating system as your base. You could also be using an image that’s preconfigured for a particular programming language or framework, such as FROM php:8.0 or FROM node:16.
All of these images provide a useful starting point for your applications. They come with common Unix utilities and key software packages. This all increases the size of your final image though. A truly minimal image should be built by constructing your own filesystem from first principles.
The “scratch” Image
Docker provides a special base image that indicates you want to control the first filesystem layer. This is the lower-most layer of your image, usually defined by the base image indicated by your FROM instruction.
When you want to create an image “from scratch,” writing FROM scratch in your Dockerfile is the way to go about it! This gives you a filesystem that’s a blank slate to begin with.
You should then use the rest of your Dockerfile as usual to populate the container’s filesystem with the binaries and libraries you need.
What Is “scratch”?
The scratch “image” looks and feels like a regular Docker image. It’s even listed in Docker Hub. scratch isn’t actually an image though – it’s a reserved keyword that denotes the lowest filesystem layer of a functioning image. All Docker images sit atop scratch as their common foundation.
You can’t docker pull scratch and it’s not possible to run containers using it. It represents an empty image layer so there’s nothing for Docker to run. Images cannot be tagged as scratch either due to its reserved nature.
What Can Be Added to scratch-Based Images?
You don’t need very much to build a functioning image atop scratch. All you need to add is a statically compiled Linux binary that you can use as your image’s command.
Here’s a working demo that runs a tiny “hello world” program compiled from C:
Compile your C-code to a binary:
Run your binary and observe that “hello world” is printed to your terminal:
Now you can create a scratch-based Docker container that runs your binary:
Build your image:
Inspecting the image with docker inspect will show that it has a single layer. This image’s filesystem contains just one file, the helloworld binary.
Now run a container using your image:
You’ll see “hello world” in your terminal as your compiled binary is executed. Your scratch-based image solely contains your binary so it will be just a few KBs in size. Using any operating system base image would raise that to multiple megabytes, even with a minimal distribution like Alpine.
Virtually all images will have some dependencies beyond a simple static binary. You’ll need to add these to your image as part of your Dockerfile. Remember that none of the tools you take for granted in standard Linux distributions will be available until you manually add them to the image’s filesystem.
When to Use Scratch?
The decision to start from scratch should be based on your application’s dependencies and your objectives around image portability. Images built from scratch are most suited to hosting statically compiled binaries where image size and build times matter.
scratch provides you with a clean slate to work from so it requires some initial investment to correctly write your Dockerfile and maintain it over time. Some Docker commands like attach won’t work by default as there’ll be no shell inside your container unless you add one.
Using scratch could be more trouble than it’s worth when you’re using interpreted languages with heavy environmental dependencies. You’ll need to continually update your base image to reference the latest versions of those packages. It’s usually more convenient and maintainable to use a minimal flavor of an existing Docker Hub base image.
Summary
FROM scratch in a Dockerfile indicates you want to start from an empty filesystem where you’re in control of every layer that’s added. It facilitates highly streamlined images purged of everything except the dependencies your application needs.
Most developers are unlikely to use scratch directly as it’s unsuitable for the majority of container use cases. You might choose to use it if you want to containerize self-contained static binaries with few environmental requirements.
scratch also functions as a clear indicator of the difference between “containers” and VMs. An image containing just one executable file is a usable Docker container as the process is run on your host’s kernel. A regular VM needs to start up independently of its host so it must include a full operating system kernel within its image.