Quick Links
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.
FROM scratch
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:
#include <stdio.h>int main() {
printf("Hello World");
return 0;
}
Compile your C-code to a binary:
gcc -o helloworld hello.c
Run your binary and observe that "hello world" is printed to your terminal:
./helloworld
Now you can create a scratch-based Docker container that runs your binary:
FROM scratchCOPY helloworld /
CMD ["helloworld"]
Build your image:
docker build -t hello:latest .
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:
docker run hello:latest
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.