This document was translated from polish original with GPT-4 and slightly corrected manually.

how to create a docker image from scratch

Here's a description of how to create a very simple but correct docker image from scratch. First, I will create an empty image - so it's non-runnable, but otherwise correct. Then I'll create a runnable image.

empty image

First, I create a tar of the file system - so called layer. I want to create an empty image (having no files or directories), so I create an empty tar (using method from stack exchange):

$ head --bytes=10240 /dev/zero > mylayer.tar

Then I check what the checksum of this tar is, as I will need it soon:

$ sha256sum mylayer.tar

For me, this sum came out to be 84ff92691f909a05b224e1c56abb4864f01b4f8e3c854e4bb4c7baf1d3f6d652.

Then I write (in the same directory) two configuration files. First, a layer description file named mylayer.json, with the following content:

{
    "id": "my_layer",
    "created": "2023-06-12T14:00:00Z",
    "container_config": {},
    "rootfs": {
        "type": "layers",
        "diff_ids": [
            "sha256:84ff92691f909a05b224e1c56abb4864f01b4f8e3c854e4bb4c7baf1d3f6d652"
        ]
    }
}

You can see that in this file I used the checksum that I just checked, right?

Then I create a configuration file with a description of the whole image - which could have many layers, but in my case has only one. This is a file named manifest.json, its content is:

[
    {
        "Config": "mylayer.json",
        "Layers": ["mylayer.tar"]
    }
]

Together, the content of my directory is:

$ ls
manifest.json  mylayer.json  mylayer.tar

I package these three files with tar:

$ tar -c mylayer.tar manifest.json mylayer.json > ../image.tar

And in this way, I got a simple, correct docker image. I can load it into docker with the command:

$ docker load < ../image.tar
84ff92691f90: Loading layer [==================================================>]  10.24kB/10.24kB
Loaded image ID: sha256:50300b2f83fc25768e1cf832278d6c9026b4d75ed73c87bbc8f5e4f0c2701318

This command copies things from this image to the /var/lib/docker/image directory. It places them there in its strange format, which I don't understand, so don't expect that if you go there, you'll just find your copied image.tar file.

The docker load command didn't return any errors, which proves that this image is correct. The identifier that docker assigned to this image is displayed (i.e., the string 50300b2f83fc25768e1cf832278d6c9026b4d75ed73c87bbc8f5e4f0c2701318). Remember it, you will need it shortly.

Now we can try to run this image with the command:

$ docker run 50300b2f83fc25768e1cf832278d6c9026b4d75ed73c87bbc8f5e4f0c2701318
docker: Error response from daemon: No command specified.
See 'docker run --help'.

It didn't work, which isn't surprising. The image does not have a program specified in its configuration files to be launched at its startup, I also didn't provide this during startup, so docker tells me No command specified.

I don't really have anything to provide, because in my layer (in this empty tar) I don't have any executable files, but I can provide a non-existent path to at least see how docker doesn't find the program:

$ docker run 50300b2f83fc25768e1cf832278d6c9026b4d75ed73c87bbc8f5e4f0c2701318 /fiku/miku
WARNING: The requested image's platform (unknown) does not match the detected host platform (linux/amd64) and no specific platform was requested
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "/fiku/miku": stat /fiku/miku: no such file or directory: unknown.
ERRO[0000] error waiting for container: context canceled 

As you can see, docker couldn't find the fiku/miku program.

This is the end of the section on creating an empty image. The files discussed can be viewed here.

image with a simple binary

Now I'll create an image that can not only be loaded but also run. Its layer will not be empty this time, but it will have one executable file in it - a program that prints the message "Hello, world!".

I'll start by creating this program. I create a file named hello.c:

#include 

int main() {
   printf("Hello, world!\n");
   return 0;
}

I compile it statically (it's important that it's static - I don't want this program to need any libraries to run) with the command:

$ gcc -static hello.c -o hello

I check if the program runs:

$ ./hello 
Hello, world!

Good. Now I will create a tar, which will contain only this one file - my executable file hello (this tar will soon serve me as a layer in my image):

$ tar cf mylayer.tar hello

Now, I will be creating, loading, and running the image just as I did before. That is, first I check the checksum of the layer:

$ sha256sum mylayer.tar

The sum I got is 47ce307a2e16442b328bb821d69699007351ef6f26b278342c70cd799d272e1a. You might get a different one, if only because the date of this hello file will be different for you.

Then I write (in the same directory) two configuration files. First, the layer description file, named mylayer.json, with the following content:

{
    "id": "my_layer",
    "created": "2023-06-12T14:00:00Z",
    "container_config": {},
    "rootfs": {
        "type": "layers",
        "diff_ids": [
            "sha256:47ce307a2e16442b328bb821d69699007351ef6f26b278342c70cd799d272e1a"
        ]
    }
}

You see, in this file I used the checksum that I checked a moment ago, right?

Then I create a configuration file describing the whole image - which could have multiple layers, but in my case, it only has one. This is a file named manifest.json, its content is:

[
    {
        "Config": "mylayer.json",
        "Layers": ["mylayer.tar"]
    }
]

Together, the content of my directory is:

$ ls
hello  hello.c	manifest.json  mylayer.json  mylayer.tar

I package these three files - the layer and two configuration files - with tar:

$ tar -c mylayer.tar manifest.json mylayer.json > ../image.tar

And in this way, I got a Docker image. I can load it into Docker using the command:

$ docker load < ../image.tar
47ce307a2e16: Loading layer [==================================================>]  880.6kB/880.6kB
Loaded image ID: sha256:840877bd1e3faef46a6ea8699f5c0ef9cdf33fcae49a5e44274a25061841c50f

No errors came from the docker load command, which proves that this image is correct. An identifier was displayed, which Docker assigned to this image (the string 840877bd1e3faef46a6ea8699f5c0ef9cdf33fcae49a5e44274a25061841c50f). Remember it, you will need it shortly.

Now we can try to run this image with the command:

$ docker run 840877bd1e3faef46a6ea8699f5c0ef9cdf33fcae49a5e44274a25061841c50f
docker: Error response from daemon: No command specified.
See 'docker run --help'.

It didn't work, which is not surprising. The image does not have a program specified in its configuration files to be launched at its start, nor did I provide this when running it, so Docker tells me No command specified. So, I will specify that the /hello program (which, as we remember, is in our layer) should be run at startup:

$ docker run 840877bd1e3faef46a6ea8699f5c0ef9cdf33fcae49a5e44274a25061841c50f /hello
WARNING: The requested image's platform (unknown) does not match the detected host platform (linux/amd64) and no specific platform was requested
Hello, world!

It worked.

This is the end of the section on creating an image that can be run. If you would like to review the files discussed, they are here.