The previous post in this series on Docker looked at starting up containers built from predefined images provided by Docker Hub. In this, the second in the series, I’ll look at creating customized images tailored to my specific requirements. I’ll also look at how my custom image can be pushed to Docker Hub for others to use.
To recap, this series covers:
- Running Containers: Installing Docker and starting containers;
- Building Images (this post);
- Disposable Containers: Using containers to run a short-lived job rather than a long-lived service;
- Composing an Environment Stack: Creating an environment composed of multiple linked containers.
Containers and Images
A Docker Container is the equivalent of a Virtual Machine (VM). It has a file system and a network connection. Services and jobs can be run from a container. Containers can be started, stopped, inspected and networked to other containers.
A Docker Image is a snapshot of a container. It defines the state of a container at a given time – usually the initial state. From this snapshot or image, a container can be started. Indeed, one image could be used to start up multiple containers.
The previous post in this series focused on running containers. This one focuses on defining images to create the containers.
There are two ways to create a new Docker image. The first is to take a snapshot of a running container using the docker commit command. The second is to create a Dockerfile which contains a list of instructions required to create the image. This is usually preferable to manually configuring a container and then taking a snapshot as it is repeatable and the Dockerfile is human readable. This post will cover only creation from a Dockerfile and not the docker commit command.
In the previous post, we created a MySQL database container by starting the official MySQL 5.6 image and then performing a few manual steps to initialize it for our application. If we create a custom image though, we can start the container already initialized and ready to go. When the database starts up for the first time, we want the application’s schema already created and a user called ‘spanners’ ready for the application to connect as.
The Dockerfile looks like this:
FROM mysql:5.6 # Copy the database initialize script: # Contents of /docker-entrypoint-initdb.d are run on mysqld startup ADD docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/ # Default values for passwords and database name. Can be overridden on docker run # ENV MYSQL_ROOT_PASSWORD=my-secret-pw # Not defaulted for security reasons! ENV MYSQL_DATABASE=spanners ENV MYSQL_USER=spanners ENV MYSQL_PASSWORD=password
The first line tells Docker that our image should be based on the official MySQL 5.6 image. Everything that follows will be built on top of that image.
The next line copies the contents of the local docker-entrypoint-initdb.d/ directory into the containers /docker-entrypoint-initdb.d/ directory. When the MySQL container starts it will look for this directory and run any scripts inside it. We can use this to initialize the schema for our application. Our schema creation script looks like this:
USE `spanners`; delimiter $$ CREATE TABLE `spanner` ( `id` int(11) NOT NULL auto_increment, `name` varchar(255) default NULL, `size` int(11) default NULL, `owner` varchar(255) default NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1$$
Finally, three environment variables are created in the container using the ENV keyword. These are used to have the MySQL container create a database schema, username and password. Again, the official MySQL image takes care of this for us. It is also possible to set the root password in this way but I’ve chosen not to.
In the previous post, we created a webserver image by starting the official Tomcat 7 image and then copying in some config files. Again, we can create a custom image so that the container starts up with everything it needs.
FROM tomcat:7-jre8 MAINTAINER Stuart 'Stevie' Leitch <[email protected]> # context.xml contains jndi connection to spanners-database ADD tomcat/context.xml $CATALINA_HOME/conf/ # tomcat-users.xml sets up user accounts for the Tomcat manager GUI # and script access for Maven deployments ADD tomcat/tomcat-users.xml $CATALINA_HOME/conf/ # MySQL driver jar ADD tomcat/mysql-connector-java-5.1.36-bin.jar $CATALINA_HOME/lib/ # Install spanners-mvc war from Maven repo RUN wget http://www.disasterarea.co.uk/maven/org/dontpanic/spanners-mvc/3.2/spanners-mvc-3.2.war -O /usr/local/tomcat/webapps/spanners-mvc.war
The first line here tells Docker that this image is based on the official Tomcat 7 (with JRE 8) image. The following three ADD commands add a context.xml file, tomcat-users.xml and the MySQL driver jar into the container’s Tomcat directory.
Finally, we want to RUN the wget command to download the application war and place it in Tomcat’s webapps directory. This means that when the container starts up Tomcat, the Spanners-MVC application will already be deployed.
Building and running the images
Dockerfiles must be built using the docker build command before the images can be run. The following commands build the database and webserver images:
sudo docker build -t spanners-database . sudo docker build -t spanners-webserver .
The -t switch tags the image with a name (spanners-database / spanners-webserver) and the dot at the end tells docker to build the Dockerfile in the current directory.
The two images are now ready to run:
sudo docker run --name spanners-database -e MYSQL_ROOT_PASSWORD=my-secret-pw -d spanners-database sudo docker run --name spanners-webserver --link spanners-database:spanners-database -p 8080:8080 -d spanners-webserver
The switches used are exactly the same as the ones we used to start the official MySQL / Tomcat images as described in part 1 of this series.
Publishing images to Docker Hub
The two images just built exist only on the machine that built them. The can be shared with other Docker users by publishing them to Docker Hub or some other Docker repo. One way to do this is to use the docker push command as described in the Docker Tutorial.
It’s also possible to automatically build and publish images whenever its definition is updated. This can be done by creating an Automated Build on Docker Hub. In this way, a Docker Hub (image) repository can be linked to a GitHub or Bitbucket (source code) repository. Whenever the source Dockerfile is updated in GitHub / Bitbucket, Docker Hub will build the updated image.
Running the published container images
The container images described in this tutorial are available from my Docker Hub account (hotblac) and have been tagged with a version number (3.2). They can be run directly from the Docker Hub automated build by referring to them by their full name:
sudo docker run --name spanners-database -e MYSQL_ROOT_PASSWORD=my-secret-pw -d hotblac/spanners-database:3.2 sudo docker run --name spanners-webserver --link spanners-database:spanners-database -p 8080:8080 -d hotblac/spanners-webserver:3.2
This starts to show the power of Docker. From any machine running Docker, it is possible to run the Spanners-MVC web application using just these two commands. It is not necessary to manually install a database or webserver on the host machine. It’s also not necessary to download and build the application source code or even the Docker image definitions. The whole application and its environment stack is available – already built – from Docker Hub. This means that the application can be started quickly and consistently on any developer workstation, on any physical server or on cloud based infrastructure.
In the next article in the series, we’ll look at using ‘disposable’ Docker containers to run a short lived jobs, specifically building and deploying the application from source code.