Creating your Own Base Image for Docker

Have you ever wondered what it's like to create a docker base image that users can import and build on? This tutorial will show you how to make your own Docker base images from scratch in two different approaches.

Creating your Own Base Image for Docker
Photo by David Knox / Unsplash

Hello World! Until now, you and I have relied on the Docker team's base images (ubuntu, alpine, and so on), but have you considered how we may construct our own base image from the ground up, rather than relying on someone else's base image? No more wait! In this post, I will guide you with two ways to create your own images from scratch: via File System and Scratch image

Before moving forward, I would like to clear some confusion between parent image, base image, and scratch image. If you already know that, feel free to skip this. Every Dockerfile starts with the FROM directive which is used to tell the docker runtime which image to use as an underlying base image and perform operations on top of it.

A parent image is what tells the runtime to pull the image from the remote repository or use from the one already pulled. Most of the docker files start from this image instead of creating their own base image.

The base image is the one that has a special FROM scratch directive. It will send the signal to the build system to not pull any image but to create from the start. You can say, the base image is the parent image of the parent image.

The logical view of how images are inherited or built on top of another

Creating from the File System

Docker image is nothing but information of files to be mounted via overlay filesystem that has all the available tools to run your application without installing any particular OS-level libraries like libc and other standard things. To have these files, I have copied the libraries and binaries from /usr directory to myimage directory as shown below

Recursively copy libraries and binaries from /usr directory to myimage

Now create a secret.txt file with some random string. I am using openssl rand function here. After you have done this, create the tar archive of the image and save it into the tar file of any name.

Import the files using docker import command which requires the tar archive file and the image name. Once this is done, you will see the sha256 hash digest of the image imported and docker images output will show the entry of the same.

Archiving the files and importing in the docker runtime

Now run the docker container of myimage:latest image we have just imported as shown below

Starting the docker container of myimage and reading contents of the secret file
The default command executed by docker is defined in the ENTRYPOINT instruction. Since that was not defined while importing the image, you can provide it manually via --entrypoint </path/to/bin> in docker run

Let's use our image as the parent image. In the following Dockerfile, I am using the filesystem from myimage:latest image, creating a file with name mysecret.txt and appending another string to the secrets.txt. Lastly, the entrypoint is mentioned to spawn a shell so we don't have to provide it in the run command.

FROM myimage:latest

RUN touch mysecret.txt
RUN echo $(openssl rand -base64 32) >> /secret.txt

ENTRYPOINT ["/bin/sh"]
Simple Dockerfile to use the base image as parent image and set default entry point to /bin/sh

Build the docker with different image name or tag or both. In this I am using the same image name myimage but using different tag v2. Let the runtime parse dockerfile and build the image.

Building the image from the Dockerfile importing our base image

Run the docker image with tag v2. Now this time since the entrypoing is already defined, it is not required in the following command. However, docker still provides you an option to override it with the --entrypoint option.

Run the container from the newly created image and validate files

Creating from Scratch

The docker team has already noticed the need that users might require to create their own images. Keeping that in mind, they have already created a special base image known as scratch and released on the Docker Hub

Despite being hosted on the public repository, you cannot pull it and save it to the local registry but provide it in the Dockerfile FROM instruction which will trigger a signal in the build process that next coming file as the first one.

// file main.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
	if (argc < 2) {
    	fprintf(stderr, "usage: %s <name>\n", argv[0]);
    	return 0x1;
    }
    
	printf("Hello %s!\n", argv[1]);
    
    return 0x0;
}
Print greeting message to the user's name passed in the first CLI argument

Compile the above code using GCC in the Linux system using the following command

gcc -o hello main.c -static
Compile the C code with static linking
Since the base image has no other libraries required by the binary, we have used -static flag in the GCC to tell the compiler that embed all the libraries in the binary  (static linking)

Now create a Dockerfile in the current directory with the following instructions

FROM scratch
ADD hello /
ENTRYPOINT ["/hello"]
Dockerfile configuration for the hello world

You can see the build succeeded in a very short time and the size of the image is less than the size of the binary created by the GCC build. This is because of the compressed image size and also has some metadata with it.

Image built from the scratch

When you will export the image using docker save and then check for the file systems, you will find only 1. Extract it, you will see that there is the same file that we have added from the local file system.

The file in the Docker image file system is exactly the same as the one compiled via GCC

Now let's run the docker image and test whether our image is working as expected or not.

Compiled binary working fine in the docker container created from scratch

References