So we’ve done quite a bit with docker up to this point. If you’ve missed the earlier posts, take a look at them here…
Getting Started with Docker
Docker essentials – Images and Containers
Docker essentials – More work with images
So I’d like to take us to the next step and talk about how to use docker files. As we do that, we’ll also get our first exposure to how docker handles networking. So let’s jump right in!
We saw earlier that when working with images that the primary method for modifying images was to commit your container changes to an image. This works, but it’s a bit clunky since you’re essentially starting a docker container, making changes, exiting out of it, and then committing the changes. What if we could just run a script that would build the image for us? Enter docker files!
Docker has the ability to build an image based on a set of instructions referred to as a docker file. Using the docker run command, we can rather easily build a custom image and then spin up containers based upon the image. Docker files use a fairly simple syntax where you specify a command followed by an argument. Any line can be prefaced with a ‘#‘ making anything after that a comment. Let’s take a look at some common commands you’ll use in docker files. I’ll list each command with a comment afterwards showing an example of its usage…
Note: Docker suggests that all commands are specified in uppercase but it is not required. Interestingly enough, you’ll see later on that the docker file’s name itself (Dockerfile) is case sensitive.
FROM – From specifies the base image you’ll use to build your new image.
MAINTAINER – Lets you specify the author of the image.
MAINTAINER Jon Langemak - firstname.lastname@example.org
RUN – Lets you run a command in the container. After the run completes, the build process will commit the image to the imagestack. This is important to remember and we’ll see an example of this when we run a test later. There are two options for running this command. One where you pass a simple command to docker (this is what I’ll be using) and the other referred to as exec syntax where you run the command and don’t require /bin/sh on the image to use it.
RUN yum update –y
CMD – Specifies the command that should be run when a container is built off an image. Recall that when you create a container, you need to tell it what process to run. CMD lets you specify this in the image creation so it doesn’t need to be specified at container runtime. You can only have one CMD per docker file much like you can only have one command ran when you launch a container.
EXPOSE – Expose tells the container which ports it should expose to the host the container is running on. For instance, in our example we’ll have two expose statements since our container will host both Apache and a SSH daemon. When we build the container, we tell docker to read the expose commands and only expose those ports.
EXPOSE 80 EXPOSE 22
ADD – Copies a file from the docker system into the image. This is useful for copying configuration files as well as any other files you may want to be on the host for other purposes. You specify the local location as well as where you want the file on the docker image. Pretty simple.
ADD index.html /var/www/html/index.html
So that’s just a taste of the available commands and examples of them but it’s all we need for our example at this point. So let’s build an example docker file so we can see how powerful this is.
#Documentation stuff(s) FROM centos:latestMAINTAINER Jon Langemak - email@example.com #Install the EPEL repo RUN yum update -y RUN yum install http://mirror.compevo.com/epel/6/i386/epel-release-6-8.noarch.rpm –y RUN yum install httpd openssh-server supervisor –y #Download the supervisord config ADD supervisord.conf /etc/supervisord.conf #Download the apache index page ADD index.html /var/www/html/index.html #Configure SSH RUN ssh-keygen -b 1024 -t rsa -f /etc/ssh/ssh_host_key RUN ssh-keygen -b 1024 -t rsa -f /etc/ssh/ssh_host_rsa_key RUN ssh-keygen -b 1024 -t dsa -f /etc/ssh/ssh_host_dsa_key RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config RUN sed -ri 's/#UsePAM no/UsePAM no/g' /etc/ssh/sshd_config #Set root password RUN echo root:docker | chpasswd #Expose SSH and HTTP ports EXPOSE 22 EXPOSE 80 #Command to run supervisord in foreground CMD ["/usr/bin/supervisord"]
So at this point, we should be familiar with the commands I’m using. The instructions following each command might not make sense if you aren’t a Linux person (trust me, I’m barely a Linux person). I broke the config down into a couple of sections so lets walk through each and see what I’ve done…
Here I’m telling the docker file which image to use as a base image. If you attempt to build the image without having the specified base image already on the host, docker will download it for you. Secondly, I’m putting a brief comment in about who created and owns the image.
Install the EPEL repo
I need the EPEL repo for some of the stuff I want to install. Namely Supervisor. Supervisor (process is named supervisord) is a process control system written for Linux. Essentially, it’s what allows us to run and maintain multiple processes in the docker container.
Download the Supervisor config
Recall that when you launch a container, you can only list one process. Since we want to run more than on in this case, we need to install Supervisor into the image, provide the config file, and list the supervisord process as the one process we want to load in the container. Supervisor is not a part of docker, but it certainly seems to be a good fit. Check them out here. The config file we’re downloading looks like this…
[supervisord] nodaemon=true [program:sshd] command=/usr/sbin/sshd –D [program:httpd] command=/usr/sbin/httpd -D FOREGROUND
So this is pretty straight forward. First we tell the supervisord process to not run as a daemon since it needs to run in the foreground of the container. If it doesn’t, then the container will run the process once and then quit because it completed running it. Next we specify the apps we want to run and also indicate that we want them run in the foreground for the same reason.
Download the apache index page
Since I’m once again building this image to impress my friend Bob, I want to have a super cool index page that apache uses when I spin up the image, I’ll load it in during image creation.
Since this is a brand new host, we need to do all of the normal stuff to setup SSH. I’m hoping that some of this (besides the key creation) can get added into the base image at some point to make this easier.
Set the root password
I set the root password so I can login. I’m sure there are better and more secure ways to do this but I do it this way here just to prove that SSH works.
Expose SSH and HTTP ports
These are the ports I want to expose to my end host. If I list them here in the docker file I can just pass the ‘–P’ flag to docker during build and it will automatically use these ports rather than having me specify them. The downside is that I cant specify a host port with the expose command, only the destination port. More on this later.
Command to run supervisord in foreground
As we already discussed, I need to do this to keep the container running with multiple processes.
So now that we have a file, let’s talk about the build process in docker. To build the image, I use the docker build command. Let’s give it a try and see what happens…
So the first thing I do is show you that I made a folder called webapp. In this folder are three things. The docker file, the index.html I want to copy to the image, and the supervisord configuration I want to copy to the image. If the two files I wanted to copy were in a different location than the docker file, I would have to specify their full path in the docker file. The next thing I do is kick off the actual build of the image. Note that I specify the image name with the –t flag and then specify a ‘.’ at the end of the command to signify the location of the docker file is local. Then the build starts. The output is huge so this is only the first part of the output. The tail end of the output should include a success message…
So it built successfully! One thing I want to point out here is that if you look at the output, you can see that the build process is building intermediate containers for each step of the docker file. And while it doesn’t call it out entirely, it’s also creating an image for each step of the process. These image names can be seen on the next line after the ‘Running in <container name>’ log. If we look at our image tree now we see…
Wohah! That’s a lot of images! You can see after the base image of centos:latest that we have 15 user images! Looking back our docker file we can see that we had 15 executable lines without the last CMD command. So docker creates a image for each line in the docker file with the exception of the CMD line. You can see that if you had an extensively long docker file how this might get out of hand. Hopefully at some point, there will be a way to squash the user images together while maintaining the base image.
So now that we have an image, let’s run it as a container and see what we get…
Notice how we passed 3 flags to the container. The name is obvious, the ‘–d’ flag tells docker to run the container as a daemon, and the ‘–P’ flag means to expose the ports from the docker file to random ports on the host. We can see these ports by looking at the running containers…
Looking at the container, we can see that docker has mapped port 22 on the container to port 49159 on the physical host. Additionally, port 80 on the container has been mapped to port 49160 on the physical host. So let’s test this out and see how it works!
I’ll connect via SSH by passing the port in my connect command…
ssh –l root -p 49159 docker
Then we authenticate with the password of ‘docker’ as set in our docker file and we’re good to go…
Notice how the hostname of the host is the same as the container ID. Now, let’s see if our apache web server came up as we expected…
There it is, you can now see Bob’s awesome home page! Note that if you were to stop the supervisord process through the SSH connection, the container would quit.
Now, let’s send this docker file over to Bob and see what happens. We’ll zip up the entire webapp folder and send it over to him…
Now that Bob has it, he can unpack all of the files into his own ‘webapp’ directory on his docker host…
Now Bob can try and build the same image that I did…
Note that Bob didn’t have the base centos container so the first step is to download it. After that, the rest of the script runs and we end in a successful build…
Now Bob can build a container around his image in the same manner that I did. Once running, he can check the container to see what ports were mapped to the container services…
And let’s check the webpage quick..
So as we can see, the docker file is a much more portable option then sending entire image stacks when sharing information. Docker files appear to be the best way to build images at this point but I’ll say again that I think being able to squash user images together would be very beneficial. Creating a new image every time a command in the file is run can get huge pretty quick.