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.
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:
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:
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):
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:
This should be enough to get you started. I kept most things at bare minimum to avoid unnecessary complexity.