This is part 3 of a series of articles on Docker. Please feel free to read part 1 and part 2.

It's been a while since I last wrote about Docker. Part of that is because I took a vacation and drove back and forth across the country. My phone managed to keep a record of the trip, though it appears that it didn't update between Gary, IN and Laramie, WY on the way back.

The other thing that I wanted to do was get more familiar with using Docker and Bottle together before I started writing about it. Over the last few months I have developed a few patterns and feel I can better express my practices.

What is this Bottle of which you speak?

Bottle is a python micro web-framework. It is similar to Flask, however I like it because it focuses very strongly on staying out of your way as much as possible. I like the minimalist nature of the framework, as well as the ease with which one can get up and running quickly. If you're not a fan of Bottle, most of what I say applies equally to Flask or other small python web frameworks.

If you want to learn more about Bottle, I suggest your read the tutorial. You will be up and running in an afternoon.

I find Bottle extremely useful for writing application API layers in microservices architectures, however there is support for templates, so Bottle can run a full-fledged web application front-end server.

Making a base image

I found it useful to start by making a base image for by Bottle projects. This base is very simple, and is available in the docker registry. To use the base image you can use the command

docker pull devries/bottle

or you can build the image with the following Dockerfile:

FROM ubuntu:14.04
MAINTAINER Christopher De Vries <devries@idolstarastronomer.com>

RUN apt-get update && apt-get install -y python-pip python-dev && apt-get clean

RUN pip install bottle
RUN pip install gunicorn

This simple recipe uses an Ubuntu 14.04 LTS as a starting point, then installs python's pip package installer, along with the python development libraries. The development libraries are important for building and packages that include portions written in C which must be compiled against the current python version. I then use pip to install bottle and the gunicorn WSGI server which is the WSGI server I tend to use.

A couple of tips about your base image:

  1. Do use a version tag with your base image's "FROM" directive. I want to build off of Ubuntu 14.04, not necessarily the latest Ubuntu image.
  2. Install the components which are common to your stack. If you have a couple different substacks, you may want to build a couple different base images, or even a hierarchy of base images.
  3. Do not include any "Hello World" application. This will only have to be removed or overwritten by your actual application image. If you want to write a demo application, then make another image that uses the base image.

A Hello World application

Now that we have created a base image, time to build the "Hello World" application image off of this Base image. The code for this example is available from bitbucket. There are a couple of files I wont touch on here, but they will be discussed in a further installment of this blog.

First, let's write a simple bottle application. The file main_app.py is shown below. It is a web service with two endpoints. The root endpoint has a simple hello world page (which uses the index.tpl template file stored under "views" in the repository) which provides the virtual IP address of the docker container.

The /json path returns a JSON encoded value including the headers in the request, the environment of the server, and the response headers.

#!/usr/bin/env python
import bottle
import subprocess
import os

p1 = subprocess.Popen(['ip','addr','show','eth0'],stdout=subprocess.PIPE)
p2 = subprocess.Popen(['sed','-rn',r's/\s*inet\s(([0-9]{1,3}\.){3}[0-9]{1,3}).*/\1/p'],stdin=p1.stdout,stdout=subprocess.PIPE)
p1.stdout.close()
ip_addr = p2.communicate()[0].strip()
p1.wait()

app = bottle.app()

@bottle.route('/')
def root_index():
    return bottle.template('index',ip_addr = ip_addr)

@bottle.route('/json')
def json_reply():
    heads = bottle.request.headers
    bottle.response.content_type = 'application/json'

    response = {'headers':dict(heads),
            'environment':dict(os.environ),
            'response':dict(bottle.response.headers)}
    return response

if __name__=='__main__':
    bottle.debug(True)
    bottle.run(app=app,host='localhost',port=8080)

The views/index.tpl file contains a template for the root page. This uses the included simple template language.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello World</title>
</head>
<body>
<h1>Hello World!</h1>
<h2>From Docker and Bottle</h2>
<p>This service is running in a docker container with a virtual IP address of {{ip_addr}}.</p>
</body>
</html>

The Dockerfile included in the repository and shown below adds the gevent asynchronous worker, exposes port 8080, adds the application to the /app directory of the image, and sets up a gunicorn process to run on port 8080 by default, logging in debug mode to the console so it will be visible using the docker logs command.

FROM devries/bottle
MAINTAINER Christopher De Vries <devries@idolstarastronomer.com>

RUN pip install gevent

EXPOSE 8080

ADD . /app
WORKDIR /app

CMD ["gunicorn","-b","0.0.0.0:8080","-w","3","-k","gevent","--log-file","-","--log-level","debug","--access-logfile","-","main_app:app"]

As you can see the above file starts with a FROM command which imports the base image we created earlier. Since this image is in the docker registry, it is will be automatically downloaded if it is not already available locally.

You can build the hello world image using the command below. It is traditional to use your own username before the /.

docker build -t devries/hellobottle .

Then you can run it with the command below. In my case I have chosen to map port 8080 of the container (where the service is running) to port 80 on the local host.

docker run -p 80:8080 -d devries/hellobottle

If you go to http://localhost/ you should see in your browser something like the image below (Note that if the docker container is not running locally, you will have to use the hostname of the system on which the container is running).

hello world message

A rendering of the JSON supplied by http://localhost/json should look like

json response

Of course gunicorn is meant to be behind a buffering proxy. We will talk about how to set that up easily next time.