If you’re familiar with docker, you probably know that docker image storage uses Union FS’s tiered storage technology. When you build a docker image, it is built one layer at a time, with the previous layer serving as the foundation for the next layer, and each layer is not changed after it is built. Because of this, when building a docker image, we have to be especially careful to include only what is needed in each layer, and to remove as much extra stuff as possible at the end of the build. For example, if you are building a simple application written in Go, in principle you only need a Go-compiled binary, and there is no need to keep the tools and environment for the build.
docker officially provides a special empty image scratch, using this image means that we don’t need any existing image as a base, and directly use our custom instructions as the first layer of the image.
In fact, we can create our own scratch image.
|
|
So, the question arises, what images can we make using scratch images as a base? The answer is that all executables that don’t need any dependencies can be made using scratch as the base image. Specifically, for statically compiled programs under linux, there is no need for runtime support from the operating system, everything is already included in the executable, for example, many applications developed in Go language use the direct FROM scratch
method to create images, so the final image size is very small.
Here is a simple web application code developed in Go language.
We can use go build
to compile this program and make a docker image based on scratch, with the following dockerfile.
Next, start compiling and building the docker image.
|
|
So the image is built successfully, let’s take a look at its size.
But running this image, you will find that the container cannot be created.
The reason is that our helloworld executable runs with some libraries like libc that are still dynamically linked, and the scratch image is completely empty, so when building the helloworld executable specify the static link flag -static
and other parameters to make the generated helloowrld binary statically link all the libraries.
|
|
Then recreate the docker image.
|
|
Run the docker image.
|
|
But the question arises, if we compile helloowrld binaries and make images on MacOS, can we run docker containers? The answer is no!
We need to specify GOOS=linux
, which means that the full compile command should look like this.
|
|
For some OCD programmers who want to go a step further, why not compile the executable inside the container and then build the image? This has the advantage of controlling the Go language compilation environment, ensuring repeatable compilation, and is friendly for some projects that integrate with continuous integration tools.
Docker-CE 17.5 introduces a new feature for building images from scratch, called “Multi-Stage Builds”. With this new feature, we can write our dockerfile like this.
Yes, you read that right, it is indeed a dockerfile that contains two FROM directives, with the following caveats
FROM golang as compiler
is to give the first stage of the build a name calledcompiler
COPY --from=compiler /go/bin/helloworld .
is a reference to the output of the first stage build to build the second stage
If you don’t have a name for the first stage, you can use the build stage number (starting with 0) to specify it, like this: --from=0
, but for readability reasons, it feels better to name it.
After the build is complete let’s look at the size of the image.
Run the docker image.
|
|