Docker is an open platform for building, shipping and running distributed applications. It gives programmers, development teams and operations engineers the common toolbox they need to take advantage of the distributed and networked nature of modern applications.


logo


Containerization provides several advantages like faster provisioning or isolated and easy to recreate environments. If you don’t know what containerization is, you can think of it as a simplified form of virtualization. Or better yet, you can look for a detailed description, which is not the purpose of this post.


For a while i have been interested on “Dockerizing” my Django apps to improve my deployment pipelines. While the documentation on docker is very good, it’s examples are too general for me, so i decided to dig a little on how to do it. On this post i intend to provide a walk through Dockerizing Django and exposing a port running uwsgi, which can be used for production unlike Django’s development server.


I will explain most of the steps, however i recommend reading docker’s tutorial on images. I assume you have Docker installed and running.


Ok, let’s get started.


On the root of our Django project (same folder as manage.py) we will have some structure:



  • requirements folder, which contains the Python requirements to be installed by pip.

  • requirements.apt, which contains the system requirements.

  • my_project.ini, which is used to hold uwsgi’s configurations.


The first step is to provide a Dockerfile in the project root, which tells docker how to build the image. My Dockerfile looks like this:


FROM ubuntu:14.04
RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV PYTHONBUFFERED 1

ADD ./requirements /requirements
ADD ./install_os_dependencies.sh /install_os_dependencies.sh
ADD ./requirements.apt /requirements.apt
ADD ./my_project.ini /my_project.ini

RUN ./install_os_dependencies.sh install

RUN pip3 install -r "requirements/requirements.txt"

RUN groupadd -r django && useradd -r -g django django
ADD . /app
RUN chown -R django /app
RUN chgrp -R django /app
WORKDIR /app

EXPOSE 8000

CMD sudo -u django uwsgi my_project.ini --master

This will build our image using as base dockerhub’s ubuntu. You can switch this by changing the first line, provided that you fetch the requirements for your other image:


FROM ubuntu:14.04

We need to generate the locales explicitly to avoid some issues with Python and Unicode. That may not be necessary with other images.


RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV PYTHONBUFFERED 1

Next we add the files required to build our image:


ADD ./requirements /requirements
ADD ./install_os_dependencies.sh /install_os_dependencies.sh
ADD ./requirements.apt /requirements.apt
ADD ./my_project.ini /my_project.ini

install_os_dependencies.sh is a bash script that will install the system requirements, which are listed in requirements.apt. It is started with:


RUN ./install_os_dependencies.sh install

You can find this script in PyDanny’s cookiecutter repository.


I am using Python3 for this project, so my requirements.apt has to reflect that:


##basic build dependencies of various Django apps for Ubuntu 14.04
#build-essential metapackage install: make, gcc, g++,
build-essential
#required to translate
gettext
python3-dev
python3-pip

##shared dependencies of:
##Pillow, pylibmc
zlib1g-dev

##Postgresql and psycopg2 dependencies
libpq-dev

##Pillow dependencies
libtiff4-dev
libjpeg8-dev
libfreetype6-dev
liblcms1-dev
libwebp-dev

##pylibmc
libmemcached-dev
libssl-dev

##django-extensions
graphviz-dev


##lxml
libxml2-dev
libxslt-dev

At this point our image has the basic system dependencies, let’s install our Python dependencies:


RUN pip3 install -r "requirements/requirements.txt"

The requirements.txt file should be in a format that pip can understand.

For this project i will install only Django and uwsgi, so that file is very short:


django==1.8.4
uWSGI==2.0.11.1

Now that the requirements are all installed, we will create a new user with lower permissions, which we will use to run uwsgi:


RUN groupadd -r django && useradd -r -g django django
ADD . /app
RUN chown -R django /app
RUN chgrp -R django /app
WORKDIR /app

WORKDIR is the preferred docker way to change directories (not something like RUN cd).


Ok, we got our app and all of it’s requirements installed. We need to expose a port to talk to the world (8000 in this case):


EXPOSE 8000

At this point we could just run our django’s development server and expose it, but that wouldn’t be useful for production. I will add uwsgi to run my Python application.

The next step is to define the configurations for uwsgi in my_project.ini:


[uwsgi]
http = :8000
chdir = /app/
env = DJANGO_SETTINGS_MODULE=config.settings
wsgi-file = config/wsgi.py
processes = 4
threads = 2

With the http directive we are telling it to serve http. You may want to use tcp sockets or unix sockets, which will require a change to that setting. For now i just want to access the server from my local browser, so http is enough.

chdir tells uwsgi where is the app.

We set an enviroment variable to select one of our settings. If you have instance specific settings, you can change them here (local, test, production, etc).


The last line in our Dockerfile defines which command will be run when we start our image:


CMD sudo -u django uwsgi my_project.ini --master

We run uwsgi as the user we created.


Ok, we are done building our Dockerfile. Time to check if we can build our image (don’t forget the .):


docker build -t django_app .

If everything works we should see something like:


Successfully built dd40ad889782

That is the id of our new image. We can run it now:


docker run -p 8000:8000 -ti dd40ad889782

We tell docker which ports do we want to forward (host:guest) with -p. -ti allow us to stop the container with ctrl+C. If we don’t care about this, that flag can be omitted.


Assuming that your Django application has no issues, you should be able to use it from http://localhost:8000.

I didn’t even mention databases because that is more related to Django setup than Docker setup. You can just use sqlite to get it to work.


If you want to inspect the internals of your new container, you can access it with:


docker exec -it weird_name bash

Where weird_name is the name assigned by docker to your container. If you want to check your running containers, you can run:


docker p

This should be enough to get you started. I kept most things at bare minimum to avoid unnecessary complexity.