top of page
  • Writer's picturePradeep P

Docker – Building Custom Images

We will look at how to create our own images that can be run using docker.

We will use a file called a Dockerfile to instruct docker as to how the image is to be created. Please refer to the official documentation about the Dockerfile for a great deal of detailed info on the Dockerfile syntax, usage, etc.

The Dockerfile indicates what our image will contain, the required configuration, the default startup command, etc.

The Dockerfile will be used by the Docker server to create a Docker image.

The usual Dockerfile will contain the following:

  1. Specification for a base image

  2. Additional commands to be run to obtain specific programs, etc.

  3. The startup command

Syntax of the lines in the Dockerfile

The Dockerfile shall consist of a set of sequential instructions.

# Comment

INSTRUCTION arguments

The INSTRUCTION is case insensitive but it is written in caps by convention.

Docker instructions are run from the top to the bottom, sequentially.

Any line starting with a # is treated as a comment

A simple example

Let us take an example where in we use a base Alpine linux image, get python installed on to it and use the python terminal to perform a simple operation.

Create a Dockerfile

Create a new directory on your computer.

C:\Users\Pradeep>mkdir python-alpine

C:\Users\Pradeep>cd python-alpine

Create a new file called Dockerfile. Note that there is no extension and only the D is capitalized.

In this file, we shall:

# Import an existing image

# Add any new software needed

# Set the init command

The following is the Dockerfile to do this:

# Import an existing image

FROM alpine

# Add any new softwares needed

RUN apk add –no-cache python

RUN echo “print(2*3)” > testpythonfile.py

# Set the init command

CMD [ “python”, “./testpythonfile.py” ]

Let us break down what we are doing here

FROM alpine

This indicates that we need to use the Alpine base image

RUN apk add –no-cache python

The “RUN apk add –no-cache python” command is what we run on Alpine to download Python. Here, we add the “RUN” prefix to let Docker know that we need to run this command post the base image import. The text that follows the RUN instruction is not for Docker but for whatever came in from the base image. In our case, this text is for the Alpine linux, asking it to install python.

When this command runs, we will have the python installation occur.

RUN echo “print(2*3)” > testpythonfile.py

Here, we are just adding a simple python command into a new python file. This is what will be run by Python

CMD [ “python”, “./testpythonfile.py” ]

This tells Docker to run the python program on init and the second parameter specifies the parameter passed to the python.

Building the Dockerfile

In the command prompt, since we are already in the same folder as the Dockerfile, we can just say:

docker build .

This searches for the Dockerfile in the same directory and starts the image creation process:

C:\Users\Pradeep\python-alpine>docker build .

Sending build context to Docker daemon 2.048kB

Step 1/4 : FROM alpine

—> 5cb3aa00f899

Step 2/4 : RUN apk add –no-cache python

—> Running in efd18b6b532f

fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/APKINDEX.tar.gz

fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/x86_64/APKINDEX.tar.gz

(1/10) Installing libbz2 (1.0.6-r6)

(2/10) Installing expat (2.2.6-r0)

(3/10) Installing libffi (3.2.1-r6)

(4/10) Installing gdbm (1.13-r1)

(5/10) Installing ncurses-terminfo-base (6.1_p20190105-r0)

(6/10) Installing ncurses-terminfo (6.1_p20190105-r0)

(7/10) Installing ncurses-libs (6.1_p20190105-r0)

(8/10) Installing readline (7.0.003-r1)

(9/10) Installing sqlite-libs (3.26.0-r3)

(10/10) Installing python2 (2.7.15-r3)

Executing busybox-1.29.3-r10.trigger

OK: 53 MiB in 24 packages

Removing intermediate container efd18b6b532f

—> 7e30fd5ad435

Step 3/4 : RUN echo “print(2*3)” > testpythonfile.py

—> Running in 4989d6c00e87

Removing intermediate container 4989d6c00e87

—> 2763eaed365a

Step 4/4 : CMD [ “python”, “./testpythonfile.py” ]

—> Running in bec7c8610593

Removing intermediate container bec7c8610593

—> 16db70bec8a4

Successfully built 16db70bec8a4

SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have ‘-rwxr-xr-x’ permissions. It is recommended to double check and reset permissions for sensitive files and directories.

We can see that the image 16db70bec8a4 was created successfully.

Also note that each step created a temporary image and removed post the creation of the next image. Thus, at each step, a new image is created and the command provided with the RUN is set as the init command for this new temp image. The image is run and a snapshot of the container is taken. This is saved as a new image and the older temp image is removed. Thus, the new image created post the RUN apk add –no-cache python command will now have python installed as part of it.

In the above example:

Step 2/4 : RUN apk add –no-cache python

—> Running in efd18b6b532f

This created a new container efd18b6b532f and installed python onto it.

Removing intermediate container efd18b6b532f

—> 7e30fd5ad435

A new image (7e30fd5ad435) was created as a snapshot of the previous container and the container was removed.

Step 3/4 : RUN echo “print(2*3)” > testpythonfile.py

—> Running in 4989d6c00e87

Removing intermediate container 4989d6c00e87

—> 2763eaed365a

7e30fd5ad435 is used as a base image for the container 4989d6c00e87. This creates the testpythonfile.py, takes a snapshot 2763eaed365a and removes the container 4989d6c00e87.

Step 4/4 : CMD [ “python”, “./testpythonfile.py” ]

—> Running in bec7c8610593

Removing intermediate container bec7c8610593

—> 16db70bec8a4

Next, the 2763eaed365a image is used to create the container bec7c8610593. The specified command is set as the init command for this container and the image 16db70bec8a4 is created. Note that the CMD is not executed now. It is just set as the startup command for the image 16db70bec8a4.

The 16db70bec8a4 is now the final output of the docker build process (since there are no further steps in the Dockerfile)

Running the created image

Next, to run the image that we created, we just use the docker run command:

C:\Users\Pradeep\python-alpine>docker run -it 16db70bec8a4

6

What happened here is:

  1. The image was already created

  2. A container was created with this image

  3. python ./testpythonfile.py was run in this

  4. Python runs the print(2*3) command and prints out 6 as the output

The official documentation has a lot of detailed information about the INSTRUCTIONS supported inside the Dockerfile. I will add more info to here once I get through all of those.

Diving Deeper into the image creation

Let us dive a little deeper into what the Dockerfile is capable of.

First off, the FROM instruction enables us to use any image as the base image for our new image. Thus, if I can use my own custom images or the images created by someone else as a base image for my new image. This provides us with a lot of power. I need not work on getting the barebones of the Ubuntu OS on to my docker image since someone else has already put in that work. I can get started on what I need to do directly.

On another note, each Dockerfile can create multiple images. Thus, I can have a single Dockerfile creating out multiple images. So, if I need to create Docker images that run a functionality on Python2 and images that run the same functionality on Python3, I can do so with a single Dockerfile.

Cached builds

Assume the following is our Dockerfile:

# Import an existing image

FROM alpine

# Add any new softwares needed

RUN apk add –no-cache python

RUN echo “print(2*3)” > testpythonfile.py

# Set the init command

CMD [ “python”, “./testpythonfile.py” ]

We built this and it generated a set of intermediate image files before producing the final image file. These intermediate image files are cached. Thus, if we try to build the same Dockerfile again, the following happens:

C:\Users\Pradeep\python-alpine>docker build .

Sending build context to Docker daemon 2.048kB

Step 1/4 : FROM alpine

—> 5cb3aa00f899

Step 2/4 : RUN apk add –no-cache python

—> Using cache

—> 7e30fd5ad435

Step 3/4 : RUN echo “print(2*3)” > testpythonfile.py

—> Using cache

—> 2763eaed365a

Step 4/4 : CMD [ “python”, “./testpythonfile.py” ]

—> Using cache

—> 16db70bec8a4

Successfully built 16db70bec8a4

Docker realized that the required images are already present in the cache and thus did not have to recreate the images again. It just gave back the already created images.

If we try to add a step to the Dockerfile now:

# Import an existing image

FROM alpine

# Add any new softwares needed

RUN apk add –no-cache python

RUN echo “print(2*3)” > testpythonfile.py

RUN echo “print(1+10)” >> testpythonfile.py

# Set the init command

CMD [ “python”, “./testpythonfile.py” ]

If we build this now:

C:\Users\Pradeep\python-alpine>docker build .

Sending build context to Docker daemon 2.048kB

Step 1/5 : FROM alpine

—> 5cb3aa00f899

Step 2/5 : RUN apk add –no-cache python

—> Using cache

—> 7e30fd5ad435

Step 3/5 : RUN echo “print(2*3)” > testpythonfile.py

—> Using cache

—> 2763eaed365a

Step 4/5 : RUN echo “print(1+10)” >> testpythonfile.py

—> Running in bda5fde0d8a5

Removing intermediate container bda5fde0d8a5

—> 5904626773c0

Step 5/5 : CMD [ “python”, “./testpythonfile.py” ]

—> Running in 268d8a36125b

Removing intermediate container 268d8a36125b

—> 5cf03cbd19b6

Successfully built 5cf03cbd19b6

As you can see, everything before the Step 4/5 : RUN echo “print(1+10)” >> testpythonfile.py part of the Dockerfile was already built earlier. Hence, Docker just used the cached images till this point. Now that it encountered this new line, it got about building it and thus, resulted in a totally new image file at the end. If we run this now:

C:\Users\Pradeep\python-alpine>docker run 5cf03cbd19b6

6

11

Thus, the new image produced a new output as expected.

Tagging the image

It would be a pain to remember the image id whenever we want to use the same. We can simplify this by specifying a tag to the created image using the docker build command:

$ docker build -t pradeep87blore/python-alpine:latest .

Here, pradeep87blore is my docker ID. python-alpine will be my repository name. latest will be the tag (and also the version number)

C:\Users\Pradeep\python-alpine>docker build -t pradeep87blore/python-alpine:latest .

Sending build context to Docker daemon 2.048kB

Step 1/5 : FROM alpine

—> 5cb3aa00f899

Step 2/5 : RUN apk add –no-cache python

—> Using cache

—> 7e30fd5ad435

Step 3/5 : RUN echo “print(2*3)” > testpythonfile.py

—> Using cache

—> 2763eaed365a

Step 4/5 : RUN echo “print(1+10)” >> testpythonfile.py

—> Using cache

—> 5904626773c0

Step 5/5 : CMD [ “python”, “./testpythonfile.py” ]

—> Using cache

—> 5cf03cbd19b6

Successfully built 5cf03cbd19b6

Successfully tagged pradeep87blore/python-alpine:latest

Docker used the cached images to just add the tag here. Thus, the image ID didn’t change. Only, human readable name got attached to it.

To run this image:

C:\Users\Pradeep\python-alpine>docker run pradeep87blore/python-alpine

6

11

There is no need to specify the version since the latest one will be picked up.

Specifying a Dockerfile path

So far, we have assumed that we are running the docker build command from the same directory containing the Dockerfile. This need not be the case:

$ docker build -f /folder_path/Dockerfile .

This can be used to specify a custom directory containing the needed Dockerfile

Image from a container

Dockerfiles are not the only way for us to create new images. One other option is that we can directly use the docker client to do so.

We can use the docker commit command to create a snapshot of a container and turn that into a new image.

C:\Users\Pradeep>docker run -it alpine sh

/ # echo Hi > newfile.txt

/ # ls

bin etc lib mnt opt root sbin sys usr

dev home media newfile.txt proc run srv tmp var

Here, we created a container from the alpine base image and created a new file inside the filespace.

Note down the id for this container (from another cmd prompt):

C:\Users\Pradeep\python-alpine>docker ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

c52f5fe88a53 alpine “sh” 3 seconds ago Up 2 seconds serene_einstein

Now, commit this:

C:\Users\Pradeep>docker commit c52f5fe88a53 pradeep87blore/imgfromcontainer:latest

sha256:1f463a500dd2af2dfa53d929ca661a137efa8a4d39491033c973d492d8b0f531

Thus, a snapshot of the container was taken and a new image was created out of it. To verify if this is indeed what has happened, we can run the new image:

C:\Users\Pradeep>docker run -it pradeep87blore/imgfromcontainer sh

/ # ls

bin etc lib mnt opt root sbin sys usr

dev home media newfile.txt proc run srv tmp var

The newfile.txt was found in the newly created image as well.

It is recommended to make use of Dockerfiles to create images, though.

5 views

Recent Posts

See All

Comments


bottom of page