Building Custom Images in Docker

Thousands of images are available to pull from Docker Hub that can be used as containers with a simple run command, but it is almost certain none of them fits exactly what you want to have running in your production environment . Most of the time, you will find yourself running containers, tweaking them, installing binaries and libraries and doing a bunch of things before finally getting those containers ready to be deployed in your own specific environment.

Once you have your containers fully updated and running, you might want to re-use them for future deployments or share them with others. Long story short, you will want to create your own custom image to be able run ready-to-use containers from it.

In Docker, there two ways to create a custom image, a basic one where you commit the container instance as an image, and a much more powerful and useful one where you create an image using Dockerfile.

We will explore in this post both methods to create Docker custom images.

Creating a Custom Image from a Container


Lets’ get a running base ubuntu container.

$ docker container run -it ubuntu bash
root@0af4a3a142be:/#

Now, Let’s pretend that we will need nano and elinks to be installed before deploying this container in production. For the moment, nothing is installed there!

root@0af4a3a142be:/# nano
bash: nano: command not found
root@0af4a3a142be:/# elinks
bash: elinks: command not found

We will update the system, then install those packages on the running container.

root@0af4a3a142be:/# apt-get update
Get:1 http://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
Hit:2 http://archive.ubuntu.com/ubuntu bionic InRelease
Get:3 http://archive.ubuntu.com/ubuntu bionic-updates InRelease [88.7 kB]
Get:4 http://archive.ubuntu.com/ubuntu bionic-backports InRelease [74.6 kB]
Get:5 http://archive.ubuntu.com/ubuntu bionic-updates/main amd64 Packages [422 kB]
Get:6 http://archive.ubuntu.com/ubuntu bionic-updates/universe amd64 Packages [251 kB]
Fetched 919 kB in 2s (559 kB/s)
Reading package lists... Done
root@0af4a3a142be:/# apt-get install -y nano elinks python
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  elinks-data file krb5-locales libexpat1 libfsplib0 libgdbm-compat4 libgdbm5 libgpm2 libgssapi-krb5-2 libidn11 libk5crypto3 libkeyutils1
  ...

Done! we can now exit the container.

root@0af4a3a142be:/# exit

Let’s get the the container name or ID as it is needed to while creating the image.

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
0af4a3a142be        ubuntu              "bash"              20 hours ago        Exited (0) 3 seconds ago                       clever_ptolemy

To create an image based on this container, we use the commit directive with the following command.

$ docker container commit CONTAINER_ID/NAME IMAGE_NAME

The above command will create an image called custom-image from our container using its ID.

$ docker commit 0af4a3a142be custom-image
sha256:f221db1a5ec0f401ffc84086f501851ab397f669fb6b17d782c4223c03eb3152

The image is now created and visible in the images list locally.

$ docker image ls
REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
custom-image              latest              f221db1a5ec0        26 seconds ago      206MB
ubuntu                    latest              cd6d8154f1e1        2 weeks ago         84.1MB
nginx                     latest              06144b287844        2 weeks ago         109MB

Consequently, we have a ready-to-use image with nano, elinks and python installed. As such, If you run now a container from the custom-image will have already those three packages installed.

$ docker container run -it custom-image bash

root@64258a1ee3b6:/# python --version
Python 2.7.15rc1
root@64258a1ee3b6:/#
root@64258a1ee3b6:/# elinks --version
ELinks 0.12pre6

Features:
Standard, IPv6, gzip, bzip2, UTF-8, Periodic Saving, Viewer (Search
History, Timer, Marks), Cascading Style Sheets, Protocol
(Authentication, File, CGI, Finger, FSP, FTP, HTTP, URI rewrite, User
protocols), SSL (GnuTLS), MIME (Option system, Mailcap, Mimetypes
files), LED indicators, Bookmarks, Cookies, Form History, Global
History, Scripting (Lua, Perl), Goto URL History
root@64258a1ee3b6:/#
root@64258a1ee3b6:/# nano --version
 GNU nano, version 2.9.3
 (C) 1999-2011, 2013-2018 Free Software Foundation, Inc.
 (C) 2014-2018 the contributors to nano
 Email: nano@nano-editor.org    Web: https://nano-editor.org/
 Compiled options: --disable-libmagic --disable-wrapping-as-root --enable-utf8
root@64258a1ee3b6:/#

Creating a Custom Image using Dockerfile


This method consist of using a file called Dockerfile. A Dockerfile is a text document that contains all the instructions for building the image. This is much handy and easier compared to the previous method, especially if your image gets bigger, because the Dockerfile will include the commands needed for building the image from scratch, by pulling it from Docker Hub, tweaking it then saving it.

One the other hand, if you want to rebuild your image, let’s say because you want to install a new version of nano or elinks, you will not have to run the container again, upgrade the installed version, then commit that container again (which something you have to do if using the first method). You will only have to recompose the image using the instructions in the Dockerfile.

So building images using Dockerfile is the preferred method in a very active and dynamic environment where images tend to change very often.

To use a Dockerfile, you will need create it. As mentioned before, it is nothing but a text file that lists all the instructions that make your image. Some instructions are mandatory and some others are optional.

A very basic Dockerfile with only two instructions looks like this:

$ cat Dockerfile
FROM ubuntu
RUN apt-get update

The first line contains the FROM instruction, which pulls an ubuntu image. Note that A valid Dockerfile must always starts with a FROM instruction. The image can be any valid image.

The second instruction is self-explanatory. This one runs a system update on the ubuntu container.

So to summarize it up, this simple Dockerfile pulls the latest ubuntu image from Docker Hub makes sure the system is updated after that. But we want more. Won’t we? Like getting the packages needed available on our image.

To go ahead and install nano, elinks or may be python in the image, our Dockerfile will have the following instructions:

$ cat Dockerfile
FROM ubuntu
RUN apt-get update
RUN apt-get install -y nano elinks python

The -y argument is very important here. Because we don’t have any way to interact with our Dockerfile while it is building the configuration, we need to confirm the package installation beforehand.

Once we have the Dockerfile with all the instruction, we’ll need to use it as source to build the image using the docker build command.

$ docker build .

Most commonly, the Dockerfile is located in your working directory and the (.) is used to confirm this to Docker. However, the file can be located anywhere in your file system other than your working directory. If this is the case, you use the -f flag with docker build.

$ docker build -f /path/to/a/Dockerfile .

Note that the path to the Dockerfile can also be a URL to GitHub

Docker will go through the steps listed in the Dockerfile and build the intended image. The output of the above will be similar to this.

$ docker build .

Sending build context to Docker daemon  6.651MB
Step 1/3 : FROM ubuntu
 ---> cd6d8154f1e1
Step 2/3 : RUN apt-get update
 ---> Running in 1058e479533e
Get:1 http://archive.ubuntu.com/ubuntu bionic InRelease [242 kB]
Get:2 http://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
Get:3 http://archive.ubuntu.com/ubuntu bionic-updates InRelease [88.7 kB]
Get:4 http://archive.ubuntu.com/ubuntu bionic-backports InRelease [74.6 kB]
Get:5 http://security.ubuntu.com/ubuntu bionic-security/universe Sources [19.2 kB]
....
<Output partially truncated for the sake of clarity>
....
Setting up perl (5.26.1-6ubuntu0.2) ...
Setting up elinks (0.12~pre6-13) ...
Processing triggers for libc-bin (2.27-3ubuntu1) ...
Removing intermediate container df5a1eba066d
 ---> 197efdf8429a
Successfully built 197efdf8429a

The image is now built and can be found in the images list, but does not have a name.

$ docker images
REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
<none>                    <none>              197efdf8429a        3 hours ago         206MB
custom-image              latest              f221db1a5ec0        6 hours ago         206MB
ubuntu                    latest              cd6d8154f1e1        2 weeks ago         84.1MB
nginx                     latest              06144b287844        2 weeks ago         109MB

We might optionally tag it with a proper name.

$ docker tag 197efdf8429a custom-image-bis
$ docker images
REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
custom-image-bis          latest              197efdf8429a        3 hours ago         206MB
custom-image              latest              f221db1a5ec0        6 hours ago         206MB
ubuntu                    latest              cd6d8154f1e1        2 weeks ago         84.1MB
nginx                     latest              06144b287844        2 weeks ago         109MB

Any container pulled from this image will have the needed packages.

$ docker run -it custom-image-bis bash

root@b928caff60f5:/# python --version
Python 2.7.15rc1
root@b928caff60f5:/# elinks --version
ELinks 0.12pre6


Features:
Standard, IPv6, gzip, bzip2, UTF-8, Periodic Saving, Viewer (Search
History, Timer, Marks), Cascading Style Sheets, Protocol
(Authentication, File, CGI, Finger, FSP, FTP, HTTP, URI rewrite, User
protocols), SSL (GnuTLS), MIME (Option system, Mailcap, Mimetypes
files), LED indicators, Bookmarks, Cookies, Form History, Global
History, Scripting (Lua, Perl), Goto URL History
root@b928caff60f5:/#
root@b928caff60f5:/# nano --version
 GNU nano, version 2.9.3
 (C) 1999-2011, 2013-2018 Free Software Foundation, Inc.
 (C) 2014-2018 the contributors to nano
 Email: nano@nano-editor.org    Web: https://nano-editor.org/
 Compiled options: --disable-libmagic --disable-wrapping-as-root --enable-utf8
root@b928caff60f5:/#

A More Interesting Example: Running a Node.js Application


Let’s admit it! Building a flat Linux image with nano and elinks installed is not that exciting. So let’s get a more interesting example. This time we will run a node.js application that will we will run on top of a node.js image.

The node.js application consists of a single file called app.js with the contents shown in the following listing.

$ cat app.js

const http = require('http');
const os = require('os');

console.log("Kubia server starting...");

var handler = function(request, response) {
  console.log("Received request from " + request.connection.remoteAddress);
  response.writeHead(200);
  response.end("You've hit " + os.hostname() + "\n");
};

var www = http.createServer(handler);
www.listen(8080);

Mainly, this code starts up an HTTP server on port 8080. The server responds with an HTTP response status code 200 OK and the text “You’ve hit <hostname>” to every request.

os.hostname() will display the container’s name and not the Docker hostname, as the web page will be displayed by the container itself.

To run this application, there is no need to install node.js on the host. We’ll user a Dockerfile to package the app into a container image and enable it to be run anywhere without having to download or install anything (except Docker, which does need to be installed on the machine you want to run the image on).

$ cat Dockerfile

FROM node:latest
ADD app.js /app.js
ENTRYPOINT ["node", "app.js"]

This Dockerfile will pull the latest node.js image from Docker Hub, copy local host app.js to the / directory (the ADD directive is used for this) of the image, then run the node.js script with the node command.

Let’s build the image and tag it as node-custom.

$ docker build -t node-custom .

Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM node:7
7: Pulling from library/node
ad74af05f5a2: Pull complete
2b032b8bbe8b: Pull complete
a9a5b35f6ead: Pull complete
3245b5a1c52c: Pull complete
afa075743392: Pull complete
9fb9f21641cd: Pull complete
3f40ad2666bc: Pull complete
49c0ed396b49: Pull complete
Digest: sha256:af5c2c6ac8bc3fa372ac031ef60c45a285eeba7bce9ee9ed66dad3a01e29ab8d
Status: Downloaded newer image for node:7
 ---> d9aed20b68a4
Step 2/3 : ADD app.js /app.js
 ---> 75e408d6a778
Step 3/3 : ENTRYPOINT ["node", "app.js"]
 ---> Running in 64538517b5be
Removing intermediate container 64538517b5be
 ---> 50b17327f4a4
Successfully built 50b17327f4a4
Successfully tagged node-custom:latest

When the build process completes, you have a new node-custom stored locally.

$ docker images
REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
node-custom               latest              50b17327f4a4        53 seconds ago      660MB
...

You can now use your image to run the node.js application named node-app and listening on port 8080 (both on host and on the container) with the following command:

$ docker run --name node-app -p 8080:8080 -d node-custom
bde38aaa62b462e6edb82df803ad1d4efd59545878f3c1109330f82fd6e1ee6c

You now have a running application in your Docker host from a customized node.js image containing your code, that display the container’s name.

Launch a browser on your Docker host and type http://localhost:8080 to see the running application in action.

I hope this post helps to get the usefulness of creating your own customized images for future uses, and to understand the different use cases for doing so. Thanks for reading!

Leave a Comment

Your email address will not be published. Required fields are marked *