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:
- 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.
- 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.
- 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).
A rendering of the JSON supplied by http://localhost/json should look like
Of course gunicorn is meant to be behind a buffering proxy. We will talk about how to set that up easily next time.