Docker Jumpstart by Andrew Odewahn

Creating your own Docker Image

Now that we've got the basics, let's make something a tiny bit more realistic: a Flask app. In this section we'll:

  • Start a basic container named "simple_flask" based on Ubuntu
  • Install the dependencies
  • Install the app itself (which is just the "Hello, World!" example, so it's nothing exciting)
  • Commit the container to a new image for our app
  • Start a new container based on our image
  • Access our app using a browser

Again, the point here is not to show the best way to set up an environment, but instead to illustrate the Docker commands and what they do. I'll code development environments in more detail later.

Start the "simple_flask" container

Our first step is to start a new container based on "ubuntu:latest." As we did in the previous example, we'll make it interactive using the "-it" options, but this time we'll give it our own name:

$ docker run -it --name="simple_flask" ubuntu:latest /bin/bash

The nice thing about using a name is that you can use it in other docker commands in place of the container id. For example, you can use this command to see the top process running on the container. (Also, it's worth noting that you need to run this on a terminal on your host, not in the container itself.)

$ docker top simple_flask
PID                 USER                COMMAND
969                 root                /bin/bash

Install the dependencies

Now that we've got the container running, we install the dependencies we need. First, since Flask is a Python micro framework, let's check out what version of Python we have:

root@2f5ada6523c4:/home# python --version
bash: python: command not found

"Wait a minute!" you might be asking yourself, "What gives? Shouldn't Python be installed on Ubuntu?" And, the answer is "Yes," on a "real" Ubuntu distribution it is. But, to save on size, the Docker base images are stripped down versions of the official images. This is important to keep this in mind as you build new containers: don't take it for granted that everything on the official distribution will be present on the Docker base image.

So, let's get a minimal python environment going. First, we'll update our apt repository:

root@2f5ada6523c4:/home# apt-get update

This will churn away for a while updating various packages. Once it's complete, install python and pip:

root@2f5ada6523c4:/home# apt-get install -y python python-pip wget

This churns even longer, but once it's done, you can install Flask:

root@2f5ada6523c4:/home# pip install Flask

Whew. That's a lot of stuff to install, so let's commit the image with a simple commit message:

$ docker commit -m "installed python and flask" simple_flask
c21c062b...

So, now if you check your "docker images" you'll see the new repository:

admins-air-5:~ odewahn$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
<none>              <none>              c21c062b086a        2 days ago          385.7 MB
ubuntu              latest              c4ff7513909d        7 days ago          225.4 MB

But, ugh, it doesn't have a name -- it just shows up as "<none>". We can use docker tag to give our image a nice name, like this:

$ docker tag c21c062b086a simple_flask
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
simple_flask        latest              c21c062b086a        2 days ago          385.7 MB
ubuntu              latest              c4ff7513909d        7 days ago          225.4 MB

Finally, taking a quick look at the history shows that we've loaded about 160MB worth of new stuff onto our image:

$ docker history simple_flask
IMAGE               CREATED             CREATED BY                                      SIZE
c21c062b086a        2 days ago          /bin/bash                                       160.3 MB
c4ff7513909d        7 days ago          /bin/sh -c #(nop) CMD [/bin/bash]               0 B
cc58e55aa5a5        7 days ago          /bin/sh -c apt-get update && apt-get dist-upg   32.67 MB
0ea0d582fd90        7 days ago          /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.895 kB
d92c3c92fa73        7 days ago          /bin/sh -c rm -rf /var/lib/apt/lists/*          0 B
9942dd43ff21        7 days ago          /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic   194.5 kB
1c9383292a8f        7 days ago          /bin/sh -c #(nop) ADD file:c1472c26527df28498   192.5 MB
511136ea3c5a        14 months ago                                                       0 B

Install our code

Let's get our code installed. First, fire up a new container:

$ docker run -it -p 5000:5000 simple_flask /bin/bash

As before, we'll use the "-it" options to make it interactive, but we're now adding a couple of new settings:

  • The "-p 5000:5000" option exposes port 5000 on the container and routes it to port 5000 on the host. If you're running Docker on a Mac, the confusing thing to remember is that from the Docker container's perspective, the virtual machine is the host, so you'll also need to expose port 5000 from the VM in order to see it on your "real" host OS. We'll cover this Inception-like step in the next section.
  • The "-w /home" option sets the working directory used when the container starts. Note that this must be an absolute path.

Once we're in, we'll grab our simple hello world file from this projects github repo:

# wget https://raw.githubusercontent.com/odewahn/docker-jumpstart/master/examples/hello.py
--2014-08-17 05:38:02--  https://raw.githubusercontent.com/odewahn/docker-jumpstart/master/examples/hello.py
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 199.27.78.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|199.27.78.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 149 [text/plain]
Saving to: 'hello.py'

100%[===========================================================================>] 149         --.-K/s   in 0s      

2014-08-17 05:38:02 (1.34 MB/s) - 'hello.py' saved [149/149]

root@2f5ada6523c4:/home# cat hello.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    app.run(host='0.0.0.0')

Finally, once you've got the file, start the app. You should see something like this:

root@2f5ada6523c4:/home# python hello.py 
 * Running on http://127.0.0.1:5000/

View the flask app on your host machine

We're ready for the big reveal. On a terminal on your host, try this command:

$ curl localhost:5000

If you're running this on Linux as your host OS, you should see "Hello World!" printed. If you're running it on boot2docker on a Mac or Windows, you most likely got this error:

curl: (7) Failed connect to localhost:5000; Connection refused

As I mentioned earlier, when you started the container and told it to expose port 5000, it exposed it on the virtual machine, not on your host. As described in the chapter on boot2docker, you need to use VBoxManage to open the port from the VM onto your host, like this:

$ VBoxManage controlvm boot2docker-vm natpf1 "flask-server,tcp,127.0.0.1,5000,,5000"

You should now be able to connect to the simple app, like this:

$ curl localhost:5000
Hello World!

Cleaning up

If you're really excited about your "Hello World!" Flask app, you should feel free to commit it. Otherwise, let's kill it. First, we need to figure out its ID, which we'll do using "docker ps":

$ docker ps -a
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS                      PORTS                    NAMES
2f5ada6523c4        simple_flask:latest   /bin/bash           3 days ago          Up 34 minutes               0.0.0.0:5000->5000/tcp   nostalgic_goodall                           simple_flask

We can kill our simple_flask app running as container "2f5ada6523c4" like this:

$ docker kill 2f5ada6523c4
2f5ada6523c4

To verify it's gone, lets run "docker ps" again, but this time add the "-a" option so that we see all containers:

$ docker ps -a
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS                      PORTS               NAMES
2f5ada6523c4        simple_flask:latest   /bin/bash           3 days ago          Exited (-1) 2 seconds ago                       nostalgic_goodall   
55ba5b67cb28        simple_flask:latest   /bin/bash           3 days ago          Exited (0) 40 minutes ago                       dreamy_mayer        
6029929a1e53        ubuntu:latest         /bin/bash           3 days ago          Exited (0) 13 hours ago                         naughty_leakey      
ef9bd2df07c6        ubuntu:latest         /bin/bash           4 days ago          Exited (0) 13 hours ago                         simple_flask

As you'll see, although the container has stopped running (i.e., its status has changed from Up to Exited), the container itself is still there. In fact, unless you use the "-rm" option when you start a container, it will always leave this remnant image behind. And, if left unchecked, after a while you'll consume your entire disk with stopped containers.

Why is it like this, you might ask? The answer lies in Docker's need to get a clean files state for a commit. In a container is running, it can mean that there are open files or processes that could interfere with the ability to save the state of the filesystem. So, rather than destroy the container automatically, Docker saves it to enable you to get a nice, clean commit image. So, killing the image is really the same as just pausing it.

So, to get rid of this ghost container, you need to use "docker rm":

$ docker rm 2f5ada6523c4
2f5ada6523c4

But, what about all those other stopped containers hanging around? Never fear! This great tip from Jim Hoskins in Remove Untagged Images From Docker shows a quick way to remove stopped containers in bulk:

$ docker rm $(docker ps -aq)