Creating a custom Docker image
The Docker community has Docker images for most popular software applications. These include, for example, images for web servers (Apache, Nginx, and so on), enterprise application platforms (JBoss EAP, Tomcat), images with programming languages (Perl, PHP, Python), and so on.
In most cases, you do not need to build your own Docker images to run standard software. But if you have a business need that requires having a custom application, you probably need to create your own Docker image.
There are a number of ways to create a new docker image:
- Commit: Creating a Docker image from a running container. Docker allows you to convert a working container to a Docker image using the
docker commit
command. This means that image layers will be stored as a separate docker image. This approach is the easiest way to create a new image. - Import/Export: This is similar to the first one but uses another Docker command. Running container layers will be saved to a filesystem using docker export and then the image will be recreated using docker import. We do not recommend this method for creating a new image since the first one is simpler.
- Dockerfile: Building a Docker image using a Dockerfile. Dockerfile is a plain text file that contains a number of steps sometimes called instructions. These instructions can run a particular command inside a container or copy files to a container. A user can initiate a build process using Dockerfile and the Docker daemon will run all instructions in the Dockerfile in a temporary container. Then this container is converted to a docker image. This is the most common way to create a new docker image. Building custom docker images from Dockerfile will be described in details in a later chapter.
- From scratch: Building a base Docker image. In the two previous methods, Docker images are created using Docker images, and these docker images were created from a base Docker image. You cannot modify this base image unless you create one yourself. If you want to know what is inside your image, you might want to create a base image instead. There are two ways to do so:
- Create a base image layer using the
tar
command. - Use special Dockerfile instructions (from scratch). Both methods will be described in later chapters.
- Create a base image layer using the
Customizing images using docker commit
The general recommendation is that all Docker images should be built from a Dockerfile to create clean and proper image layers without unwanted temporary and log files, despite the fact that some vendors deliver their Docker images without an available Dockerfile . If there is a need to modify that existing image, you can use the standard docker commit
functionality to convert an existing container to a new image.
As an example, we will try to modify our existing httpd container and make an image from it.
First, we need to get the httpd image:
$ docker pull httpd
Using default tag: latest
Trying to pull repository docker.io/library/httpd ...
latest: Pulling from docker.io/library/httpd
...
output truncated for brevity
...
Digest: sha256:6e61d60e4142ea44e8e69b22f1e739d89e1dc8a2764182d7eecc83a5bb31181e
Next, we need a container to be running. That container will be used as a template for a future image
$ docker run -d --name httpd httpd
c725209cf0f89612dba981c9bed1f42ac3281f59e5489d41487938aed1e47641
Now we can connect to the container and modify its layers. As an example, we will update index.html
:
$ docker exec -it httpd /bin/sh # echo "This is a custom image" > htdocs/index.html # exit
Let's see the changes we made using the docker diff
command. This command shows you all files that were modified from the original image. The output looks like this:
$ docker diff httpd
C /usr
C /usr/local
C /usr/local/apache2
C /usr/local/apache2/htdocs
C /usr/local/apache2/htdocs/index.html
...
output truncated for brevity
...
The following table shows the file states of the docker diff
command:
Symbol | Description |
A | A file or directory was added |
D | A file or directory was deleted |
C | A file or directory was changed |
In our case, docker diff httpd
command shows that index.html
was changed.
Create a new image from the running container:
$ docker commit httpd custom_image
sha256:ffd3a523f9848776d65de8302253de9dc78e4948a792569ee46fad5c099312f6
Verify that the new image has been created:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
custom_image latest ffd3a523f984 3 seconds ago 177.4 MB
docker.io/httpd latest 01154c38b473 2 weeks ago 177.4 MB
The final step is to verify that the image works properly:
$ docker run -d --name custom_httpd -p 80:8080 custom_image
78fc5731d62e5a6377a7de152c0ba25d350603e6d97fa26967e06a82c8257e71
$ curl localhost:8080
This is a custom image
Using Dockerfile build
Usually, those who use Docker containers expect to have a high-level of automation, and the docker commit
command is difficult to automate. Luckily, Docker can build images automatically by reading instructions from a special file usually called a Dockerfile. A Dockerfile is a text document that contains all the commands a user can call on the command line to assemble an image. Using docker build, users can create an automated build that executes several command-line instructions in succession. On CentOS 7, you can learn a lot more using the Dockerfile built-in documentation page man Dockerfile
.
A Dockerfile has a number of instructions that help Docker to build an image according to your requirements. Here is a Dockerfile example, which allows us to achieve the same result as in the previous section:
$ cat Dockerfile
FROM httpd
RUN echo "This is a custom image" > /usr/local/apache2/htdocs/index.html
Once this Dockerfile is created, we can build a custom image using the docker build
command:
$ docker build -t custom_image2 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM httpd
---> 01154c38b473
Step 2 : RUN echo "This is a custom image" > /usr/local/apache2/htdocs/index.html
---> Using cache
---> 6b9be8efcb3a
Successfully built 6b9be8efcb3a
Note
Please note that the .
at the end of the first line is important as it specifies the working directory. Alternatively, you can use ./
or even $(pwd)
. So the full commands are going to be: docker build -t custom_image2 .
ordocker build -t custom_image2 ./
ordocker build -t custom_image2 $(pwd)
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
custom_image2 latest 6b9be8efcb3a 2 minutes ago 177.4 MB
custom_image latest ffd3a523f984 19 minutes ago 177.4 MB
docker.io/httpd latest 01154c38b473 2 weeks ago 177.4 MB
Using Docker history
We can check the history of image modifications using docker history
:
$ docker history custom_image2
IMAGE CREATED CREATED BY SIZE COMMENT
6b9be8efcb3a 21 hours ago /bin/sh -c echo "This is a custom image" > /u 23 B
01154c38b473 2 weeks ago /bin/sh -c #(nop) CMD ["httpd-foreground"] 0 B
...
output truncated for brevity
...
Note that a new layer, 6b9be8efcb3a
, is added. This is where we change the content of the index.html
file in comparison to the original httpd
image.
Dockerfile instructions
Some Dockerfile instructions are shown in the table:
Instruction | Description and examples |
| It sets the base image used in the build process. Examples: FROM httpd FROM httpd:2.2 |
| The RUN instruction executes any commands in a new layer on top of the current image and commits the results. Examples: RUN |
| This is the same as the last one but in Docker format. |
| The COPY instruction copies new files from Examples: COPY |
| An ENTRYPOINT helps you configure a container that can be run as an executable. When you specify an ENTRYPOINT, the whole container runs as if it were only that executable. Examples: ENTRYPOINT In most cases the default value of ENTRYPOINT is |
| This instruction informs a Docker daemon that an application will be listening on this port at runtime. This is not very useful when working with standalone Docker containers because port publishing is performed via the |
| Provides arguments to an Example:
|
When the docker build
command is run, Docker reads the provided Dockerfile from top to bottom, creating a separate layer for every instruction and placing it in the internal cache. If an instruction from Dockerfile is updated, it invalidates the respective caching layer and every subsequent one, forcing Docker to rebuild them when the docker build command is run again. Therefore, it's more effective to place the most malleable instructions at the end of Dockerfile, so that the number of invalidated layers is minimized and cache usage is maximized. For example, suppose we have a Dockerfile with the following contents:
$ cat Dockerfile
FROM centos:latest
RUN yum -y update
RUN yum -y install nginx, mariadb, php5, php5-mysql
RUN yum -y install httpd
CMD ["nginx", "-g", "daemon off;"]
In the example, if you choose to use MySQL instead of MariaDB, the layer created by the second RUN command, as well as the third one, will be invalidated, which for complex images means a noticeably longer build process.
Consider the following example. Docker includes images for minimal OSes. These base images can be used to build custom images on top of them. In the example, we will be using a CentOS 7 base image to create a web server container from scratch:
- First, we need to create a
project
directory:
$ mkdir custom_project; cd custom_project
Then, we create a Dockerfile with the following content:
$ cat Dockerfile
FROM centos:7
RUN yum install httpd -y
COPY index.html /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/httpd","-D","FOREGROUND"]
- Create the
index.html
file:
$ echo "A new cool image" > index.html
- Build the image using
docker build
:
$ docker build -t new_httpd_image .
Sending build context to Docker daemon 3.072 kB
...
output truncated for brevity
...
Successfully built 4f2f77cd3026
- Finally, we can check that the new image exists and has all the required image layers:
$ docker history new_httpd_image
IMAGE CREATED CREATED BY SIZE COMMENT
4f2f77cd3026 20 hours ago /bin/sh -c #(nop) ENTRYPOINT ["/usr/sbin/htt 0 B
8f6eaacaae3c 20 hours ago /bin/sh -c #(nop) COPY file:318d7f73d4297ec33 17 B
e19d80cc688a 20 hours ago /bin/sh -c yum install httpd -y 129 MB
...
output truncated for brevity
...
Note
The top three layers are the instructions we added in the Dockerfile.