Building container images
As this book attempts to troubleshoot Docker, wouldn't it prove beneficial to reduce our chances for errors that we would have to troubleshoot in the first place? Fortunately for us, the Docker community (and the open source community at large) provides a healthy registry of base (or root) images that dramatically reduce errors and provide more repeatable processes. Searching the Docker Registry, we can find official and automated build statuses for a broad and growing array of container images. The Docker official repositories (https://docs.docker.com/docker-hub/official_repos/) are carefully organized collections of images supported by Docker Inc.-automated repositories that allow you to validate source and content of a particular image also exist.
A major thrust and theme of this chapter will be in basic Docker fundamentals; while they may seem trivial to the experienced container user, following some best practices and levels of standardization will serve us well in avoiding trouble spots in addition to enhancing our abilities to troubleshoot.
Official images from the Docker Registry
Standardization is a major component for repeatable processes. As such, wherever and whenever possible, one should opt for a standard base image as provided in the Docker Hub for the variant Linux distributions (for example, CentOS, Debian, Fedora, RHEL, Ubuntu, and others) or for specific use cases (for example, WordPress applications). Such base images are derived from their respective Linux platform images, and are built specifically for use in containers. Further, standardized base images are well maintained and updated frequently to address security advisories and critical bug fixes.
These base images are built, validated, and supported by Docker Inc. and are easily recognized by their single word names (for example, centos
). Additionally, user members of the Docker community also provide and maintain prebuilt images to address certain use cases. Such user images are denoted with the prefix of the Docker Hub username that created them, suffixed with the image name (for example, tutum/centos
).
To our great advantage, these standard base images remain ready and are publicly available on the Docker Registry; images can be searched for and retrieved simply using the docker search
and docker pull
Terminal commands. These will download any image(s) that are not already located on the Docker host. The Docker Registry has become increasingly powerful in providing official base images for which one can use directly, or at least as a readily available starting point toward addressing the needs of your container building.
Note
While this book assumes your familiarity with Docker Hub/Registry and GitHub/Bitbucket, we will dedicate initial coverage of these as your first line of reference for efficient image building for containers. You can visit the official registry of Docker images at https://registry.hub.docker.com/.
The Docker Registry can be searched from your Docker Hub account or directly from the Terminal, as follows:
$ sudo docker search centos
Flags can be applied to your search criteria to filter images for star ratings, automated builds, and many more. To use the official centos
image from the registry, from a Terminal:
$ sudo docker pull centos
: This will download thecentos
image to your host machine.$ sudo docker run centos
: This will first look for this image localized on your host and, if not found, it will download the image to host. The run parameters for the image will have been defined in its Dockerfile.
User repositories
Further, as we have seen, we are not limited merely to the repositories of official Docker images. Indeed, a wealth of community users (both as inpiduals and from corporate enterprises) have prepared images constructed to meet certain needs. As an example, an ubuntu
image is created to run the joomla
content management system within a container running on Apache, MySql, and PHP.
Here, we have a user repository with just such an image (namespace/repository name
):
Note
Try it out: Practice an image pull
and run
from the Docker Registry from the Terminal.
$ sudo docker pull cloudconsulted/joomla
pulls our base image for a container and $ sudo docker run -d -p 80:80 cloudconsulted/joomla
runs our container image and maps port 80
of the host to port 80
of the container.
Point your browser to http://localhost
and you will have the build page for a new Joomla website!
Building our own base images
There may be occasion, however, when we need to create custom images to suit our own development and deployment environment. If your use case dictates using a nonstandardized base image, you will need to roll your own image. As with any approach, appropriate planning beforehand is necessary. Before building an image, you should spend adequate time to fully understand the use case your container is meant to address. There isn't much need for a container that cannot run the intended application. Other considerations may include whether the library or binary you are including in the image is reusable, and many more. Once you feel you are done, review your needs and requirements once more and filter out parts that are unnecessary; we do not want to bloat our containers for no good reason.
Using the Docker Registry, you may find automated builds. These builds are pulled from repositories at GitHub/Bitbucket and can, therefore, be forked and modified to your own specifications. Your newly forked repository can then in turn be synced to the Docker Registry with your new image, which can then be pulled and run as needed for your containers.
Note
Try it out: Pull the ubuntu minimal image from the following repository and drop it to your Dockerfile directory to create your own image:
$ sudo docker pull cloudconsulted/ubuntu-dockerbase
$ mkdir dockerbuilder
$ cd dockerbuilder
Open an editor (vi/vim or nano) and create a new Dockerfile:
$ sudo nano Dockerfile
We will delve into creating good Dockerfiles later as we talk about layered and automated image building. For now, we just want to create our own new base image, only symbolically going through the procedure and location for creating a Dockerfile. For the sake of simplicity, here we are just calling the base image from which we want to build our new image:
FROM cloudconsulted/ubuntu-dockerbase:latest
Save and close this Dockerfile. We now build our new image locally:
$ sudo docker build -t mynew-ubuntu
Let's check to ensure our new image is listed:
$ sudo docker images
Note our IMAGE ID for mynew-ubuntu, as we will need it shortly:
Create a new public/private repository under your Docker Hub username. I'm adding the new repository here under <namespace><reponame>
as cloudconsulted/mynew-ubuntu
:
Next, return to the Terminal so that we can tag our new image to push to the new Docker Hub repository under our <namespace>
:
$ sudo docker tag 1d4bf9f2c9c0 cloudconsulted/mynew-ubuntu:latest
Ensure that our new image is correctly tagged for <namespace><repository>
in our images list:
$ sudo docker images
Also, we will find our newly created image labeled for pushing it to our Docker Hub repository.
Now, let's push the image up to our Docker Hub repository:
$ sudo docker push cloudconsulted/mynew-ubuntu
Then, check the Hub for our new image:
There are essentially two approaches to building your own Docker images:
- Manually constructing layers interactively via bash shell to install necessary applications
- Automating through a Dockerfile that builds the images with all necessary applications
Building images using the scratch repository
Going about building your own container images for Docker is highly dependent on which Linux distribution you intend to package. With such variance, and with the prevalence and growing registry of images already available to us via the Docker Registry, we won't spend much time on such a manual approach.
Here again, we can look in the Docker Registry to provide us with a minimal image to use. A scratch
repository has been created from an empty TAR file that can be utilized simply via docker pull
. As before, make your Dockerfile according to your parameters, and you have your new image, from scratch.
This process can be even further simplified by making use of available tools, such as supermin (Fedora systems) or debootstrap (Debian systems). Using such tools, the build process for an Ubuntu base image, for example, can be as simple as follows:
$ sudo debootstrap raring raring > /dev/null $ sudo tar -c raring -c . | docker import - raring a29c15f1bf7a $ sudo docker run raring cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=14.04 DISTRIB_CODENAME=raring DISTRIB_DESCRIPTION="Ubuntu 14.04"
Building layered images
A core concept and feature of Docker is layered images. One of the most important features of Docker is image layering and the management of image content. A layered approach for container images is very efficient, as you can reference the contents in the image, identifying the layer in a layered image. This is very powerful when building multiple images, using the Docker Registry to push and pull images.
[Image Copyright © Docker, Inc.]
Building layered images using Dockerfiles
Layered images are primarily built using the Dockerfile. In essence, a Dockerfile is a script that automatically builds our containers from a source (base or root) image in the order you need them executed by the Docker daemon, step by step, layer upon layer. These are successive commands (instructions) and arguments enlisted within the file that execute a proscribed set of actions on a base image, with each command constituting a new layer, in order to build a new one. This not only facilitates the organization of our image building but greatly enhances deployments from beginning to end through its simplification. The scripts within a Dockerfile can be presented to the Docker daemon in a range of ways to build new images for our containers.
Dockerfile construction
The first command of a Dockerfile is typically the FROM
command. FROM
specifies the base image to be pulled. This base image can be located in the public Docker registry (https://www.docker.com/) within a private registry or even a localized Docker image from the host.
Additional layers in a Docker image are populated as per the directives defined in the Dockerfile. Dockerfiles have very handy directives. Every new directive defined in the Dockerfile constitutes a layer in a layered image. With a RUN
directive, we can specify a command to be run, with the result of the command as an additional layer in the image.
Tip
It is highly advised to logically group the operations performed in an image and keep the number of layers to a minimum. For example, while trying to install the dependencies for your application, one can install all the dependencies in one RUN
directive rather than using N number of directives per dependency.
We will inspect more closely, the aspects of Dockerfiles for automation in a later section, Automated image building. For now, we need to make certain that we grasp the concept and construction of the Dockerfile itself. Let's look specifically at a simple list of commands that can be employed. As we have seen before, our Dockerfile should be created in a working directory containing our existing code (and/or other dependencies, scripts, and others).
Tip
CAUTION: Avoid use of the root [/
] directory as root of your source repository. The docker build
command makes use of the directory containing your Dockerfile as the build context (including all of its subdirectories). The build context will be sent to the Docker daemon before building the image, which means if you use /
as the source repository, the entire contents of your hard drive will get sent to the daemon (and thus to the machine running the daemon). In most cases, it is best to put each Dockerfile in an empty directory. Then, only add the files needed for building the Dockerfile to the directory. To increase the build's performance, a .dockerignore
file can be added to the context directory to properly exclude files and directories.
Dockerfile commands and syntax
While simplistic, the order and syntax of our Dockerfile commands are extremely important. Proper attention to details and best practice here will not only help ensure successful automated deployments, but also serve to help in any troubleshooting efforts.
Let's delineate some basic commands and illustrate them directly with a working Dockerfile; our joomla
image from before is a good example of a basic layered image build from a Dockerfile.
Note
Our sample joomla base image is located in the public Docker index via
cloudconsulted/joomla
.
FROM
A proper Dockerfile begins with defining an image FROM
, from which the build process starts. This instruction specifies the base image to be used. It should be the first instruction in Dockerfile, and it is a must for building an image via Dockerfile. You can specify the local image, an image present at the Docker public registry, or image at a private registry.
Common Constructs
FROM <image> FROM <image>:<tag> FROM <image>@<digest>
<tag>
and <digest>
are optional; if you do not specify them, it defaults to latest
.
Example Dockerfile from our Joomla Image
Here, we define the base image to be used for the container:
# Image for container base FROM ubuntu
MAINTAINER
This line designates the Author of the built image. This is an optional instruction in Dockerfile; however, one should specify this instruction with the name and/or e-mail address of the author. MAINTAINER
details can be placed anywhere you prefer in your Dockerfile, so long as it is always post your FROM
command, as they do not constitute any execution but rather a value of a definition (that is, just some additional information).
Common Constructs
MAINTAINER <name><email>
Example Dockerfile from our Joomla Image
Here, we define the author for this container and image:
# Add name of image author MAINTAINER John Wooten <jwooten@cloudconsulted.com>
ENV
This instruction sets the environment variable in Dockerfile. An environment variable set can be used in subsequent instructions.
Common Constructs
ENV <key> <value>
The preceding code sets one environment variable <key>
with <value>
.
ENV <key1>=<value1> <key2>=<value2>
The preceding instruction sets two environment variables. Use the =
sign between key and value of an environment variable and separate two environment key-values with space to define multiple environment variables:
ENV key1="env value with space"
Use quotes for value having spaces for environment variable.
The following are the points to remember about ENV
instructions:
- Use single instruction to define multiple environment variables
- Environment variables are available when you create container from image
- One can review the environment variable from image using
docker inspect <image>
- Values of environment variables can be changed at runtime by passing the
--env <key>=<value>
option to thedocker run
command
Example Dockerfile from our Joomla Image
Here, we set the environment variables for Joomla and the Docker image running without an interactive Terminal:
# Set the environment variables ENV DEBIAN_FRONTEND noninteractive ENV JOOMLA_VERSION 3.4.1
RUN
This instruction allows you to run commands and yield a layer. The output of the RUN
instruction will be a layer built for image under process. Command passed to the RUN
instruction runs on the layers built before this instruction; one needs to take care of the orders.
Common Constructs
RUN <command>
The <command>
is executed in a shell -/bin/sh -c
shell form.
RUN ["executable", "parameter1", "parameter2"]
In this particular form, you specify the executable
and parameters
in executable form. Ensure that you pass the absolute path of the executable in the command. This is useful for cases where the base image does not have /bin/sh
. You can specify an executable, which could be your only executable in a base image and build the layers on top using it.
This is also useful if you do not want to use the /bin/sh
shell. Consider this:
RUN ["/bin/bash", "-c", "echo True!"] RUN <command1>;<command2>
Actually, this is a special form of example, where you specify multiple commands separated by ;
. The RUN
instruction executes such commands together and builds a single layer for all of the commands specified.
Example Dockerfile from our Joomla Image
Here, we update the package manager and install required dependencies:
# Update package manager and install required dependencies RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ mysql-server \ apache2 \ php5 \ php5-imap \ php5-mcrypt \ php5-gd \ php5-curl \ php5-apcu \ php5-mysqlnd \ supervisor
Note that we have purposefully written so that new packages are to be added as their own apt-get install lines, following the initial install commands.
This is done so that, should we ever need to add or remove a package, we can do so without requiring to re-install all other packages within our Dockerfile. Obviously, this provides considerable savings in build time, should the need arise.
Note
Docker Cache: Docker will first check against the host's image cache for any matching layers from previous builds. If found, the given build step within the Dockerfile will be skipped to utilize the previous layer, from cache. As such, it is best practice to enlist each of the Dockerfile's apt-get -y install
commands on their own.
As we've discussed, the RUN
command in a Dockerfile will execute any given command under the context and filesystem of the Docker container, and produce a new image layer with any resulting file system changes. We first run apt-get update
to ensure that the repositories and the PPAs of the packages are updated. Then, in separate calls, we instruct the package manager to install MySQL, Apache, PHP, and Supervisor. The -y
flag skips interactive confirmation.
With all of our necessary dependencies installed to run our service, we ought to tidy up a bit to give us a cleaner Docker image:
# Clean up any files used by apt-get RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
ADD
This information is used to copy files and directories from the local filesystem or files from a remote URL into the image. The source and destination must be specified in ADD
instructions.
Common Constructs
ADD <source_file> <destination_directory>
Here the path of <source_file>
is relative to the build context. Also, the path of <destination_directory>
could either be absolute or relative to the WORKDIR
:
ADD <file1> <file2> <file3> <destination_directory>
Multiple files, for example, <file1>
, <file2>
, and <file3>
, are copied into <destination_directory>
. Note that paths of these source files should be relative to the build context, as follows:
ADD <source_directory> <destination_directory>
Contents of the <source_directory>
are copied into <destination_directory>
along with the filesystem metadata; the directory itself is not copied:
ADD text_* /text_files
All the files starting with text_
in the build context directory are copied in the /text_files
directory in the container image:
ADD ["filename with space",..., "<dest>"]
Filename with a space can be specified in quotes; one needs to use a JSON array to specify the ADD instruction in this case.
The following are the points to remember about ADD
instructions:
- All new files and directories that are copied into the container image have UID and GID as
0
- In cases where the source file is a remote URL, the destination file will have a permission of
600
- All the local files referenced in the source of the
ADD
instruction should be in the build context directory or in its subdirectories - If the local source file is a supported tar archive then it is unpacked as a directory
- If multiple source files are specified, the destination must be a directory and end with a trailing slash,
/
- If a destination does not exist, it will be created along with all the parent directories in the path, if required
Example Dockerfile from our Joomla Image
Here, we download joomla
into the Apache web root:
# Download joomla and put it default apache web root ADD https://github.com/joomla/joomla-cms/releases/download/$JOOMLA_VERSION/Joomla_$JOOMLA_VERSION-Stable-Full_Package.tar.gz /tmp/joomla/ RUN tar -zxvf /tmp/joomla/Joomla_$JOOMLA_VERSION-Stable-Full_Package.tar.gz -C /tmp/joomla/ RUN rm -rf /var/www/html/* RUN cp -r /tmp/joomla/* /var/www/html/ # Put default htaccess in place RUN mv /var/www/html/htaccess.txt /var/www/html/.htaccess RUN chown -R www-data:www-data /var/www # Expose HTTP and MySQL EXPOSE 80 3306
COPY
The COPY
command specifies that a file, located at the input path, should be copied from the same directory as the Dockerfile to the output path inside the container.
CMD
The CMD
instruction has three forms-a shell form, as default parameters to ENTRYPOINT
and the preferred executable form. The main purpose of a CMD
is to provide defaults for an executing container. These defaults can either include or omit an executable, the latter of which must specify an ENTRYPOINT
instruction as well. If the user specifies arguments to Docker run
, then they will override the default specified in CMD
. If you would like your container to run the same executable every time, then you should consider using ENTRYPOINT
in combination with CMD
.
The following are the points to remember:
- Do not to confuse
CMD
withRUN
-RUN
will actually execute the command and commit the result, whereasCMD
does not execute commands during a build, but instead specifies the intended command for the image - A Dockerfile can only execute one
CMD
; if you enlist more than one, only the lastCMD
will be executed
Example Dockerfile from our Joomla Image
Here, we set up Apache for it to start:
# Use supervisord to start apache / mysql COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf CMD ["/usr/bin/supervisord", "-n"]
The following is the content of our completed Joomla Dockerfile:
FROM ubuntu MAINTAINER John Wooten <jwooten@cloudconsulted.com> ENV DEBIAN_FRONTEND noninteractive ENV JOOMLA_VERSION 3.4.1 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ mysql-server \ apache2 \ php5 \ php5-imap \ php5-mcrypt \ php5-gd \ php5-curl \ php5-apcu \ php5-mysqlnd \ supervisor # Clean up any files used by apt-get RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Download joomla and put it default apache web root ADD https://github.com/joomla/joomla-cms/releases/download/$JOOMLA_VERSION/Joomla_$JOOMLA_VERSION-Stable-Full_Package.tar.gz /tmp/joomla/ RUN tar -zxvf /tmp/joomla/Joomla_$JOOMLA_VERSION-Stable-Full_Package.tar.gz -C /tmp/joomla/ RUN rm -rf /var/www/html/* RUN cp -r /tmp/joomla/* /var/www/html/ # Put default htaccess in place RUN mv /var/www/html/htaccess.txt /var/www/html/.htaccess RUN chown -R www-data:www-data /var/www # Expose HTTP and MySQL EXPOSE 80 3306 # Use supervisord to start apache / mysql COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf CMD ["/usr/bin/supervisord", "-n"]
Other common Dockerfile commands are as follows: ENTRYPOINT
An ENTRYPOINT
allows you to configure a container that will run as an executable. From Docker's documentation, we will use the provided example; the following will start nginx
with its default content, listening on port 80
:
docker run -i -t --rm -p 80:80 nginx
Command-line arguments to docker run <image>
will be appended after all elements in an executable form ENTRYPOINT
, and will override all elements specified using CMD
. This allows arguments to be passed to the entry point, that is, docker run <image> -d
will pass the -d
argument to the entry point. You can override the ENTRYPOINT
instruction using the docker run --entrypoint
flag.
LABEL
This instruction specifies the metadata for the image. This image metadata can later be inspected using the docker inspect <image>
command. The idea here is to add information about the image in image metadata for easy retrieval. In order to get the metadata from the image, one does not need to create a container from the image (or mount the image on local filesystem), Docker associates metdata data with every Docker image, and it has a predefined structure for it; using LABEL
, one can add additional associated metadata describing the image.
The label for the image is a key-value pair. Following are examples of using LABEL
in a Dockerfile:
LABEL <key>=<value> <key>=<value> <key>=<value>
This instruction will add three labels to the image. Also, note that it will create one new layer as all the labels are added in a single LABEL
instruction:
LABEL "key"="value with spaces"
Use quotes in labels if the label value has spaces:
LABEL LongDescription="This label value extends over new \ line."
If the value of the label is long, use backslash to extend the label value to a new line.
LABEL key1=value1 LABEL key2=value2
Multiple labels for an image can be defined by separating them by End Of Line (EOL). Note that, in this case, there will be two image layers created for two different LABEL
instructions.
Notes about LABEL
instructions:
- Labels are collated together as described in Dockerfile and those from the base image specified in the
FROM
instruction - If
key
in labels are repeated, later one will override the earlier defined key's value. - Try specifying all the labels in a single
LABEL
instruction to produce an efficient image, thus avoiding unnecessary image layer count - To view the labels for a built image, use the
docker inspect <image>
command
WORKDIR
This instruction is used to set the working directory for subsequent RUN
, ADD
, COPY
, CMD
, and ENTRYPOINT
instructions in Dockerfile.
Define a work directory in Dockerfile, all subsequent relative paths referenced inside the container will be relative to the specified work directory.
The following are examples of using the WORKDIR
instruction:
WORKDIR /opt/myapp
The preceding instruction specifies /opt/myapp
as the working directory for subsequent instructions, as follows:
WORKDIR /opt/ WORKDIR myapp RUN pwd
The preceding instruction defines the work directory twice. Note that the second WORKDIR
will be relative to the first WORKDIR
. The result of the pwd
command will be /opt/myapp
:
ENV SOURCEDIR /opt/src WORKDIR $SOURCEDIR/myapp
Work directory can resolve the environment variables defined earlier. In this example, the WORKDIR
instruction can evaluate the SOURCEDIR
environment variable and the resultant working directory will be /opt/src/myapp
.
USER
This sets the user for running any subsequent RUN
, CMD
, and ENTRYPOINT
instructions. This also sets the user when a container is created and run from the image.
The following instruction sets the user myappuser
for the image and container:
USER myappuser
Notes about USER
instructions:
- One can override the user using
--user=name|uid[:<group|gid>]
in thedocker run
command for container