Django development environment with Docker — A step by step guide

Fernando Aramendi
devartis
Published in
6 min readJul 20, 2017

--

About Docker

Docker is the software development and devops tool everyone wants to use for their projects. It’s a platform that allows you to easily create isolated environments (aka Containers) that can run specific pieces of software. In other words, it provides a lightweight mechanism to “virtualize” environments in a host computer.

This is a very powerful tool for development since it simplifies the process of configuring and setting up environments or switching between completely different projects in the same development box. But, most importantly, it allows developers to consistently reproduce production environments and avoid problems due to configuration differences.

Particularly in Django there are some common errors that can arise with the default settings configuration and their differences with a typical production deployment. When using SQLite as a development database, which lacks DDL constraints, you can encounter some migrations failing in production even if you were able to run them perfectly on your local environment. Another common source of problems can be the difference between serving static files using the runserver command and the existence of a Web server with a reverse proxy in production. Also, if you use Memcached in production you might not be able to reproduce invalidation errors, and so on.

The advisable thing to do would be to configure your development box exactly as the production environment, but switching between projects wouldn’t be that easy and you don’t want to have all the services running while doing other tasks. That’s where virtualization becomes handy.

Using Docker for development

As an example we are going to go through the process of creating a Django application from scratch as setting up two of the most common external services you would use in production; MySQL and NGINX.

Create a Django project

First we create an empty virtual environment and initialize our Django project. I’m using virtualenv wrapper for simplicity, you can read more about it here.

$ mkvirtualenv djangodocker --python=/usr/bin/python3.6
$ pip install django
$ django-admin startproject djangodocker

MySQL

In order to use MySQL instead of SQLite we are going to edit the default settings.

$ vim djangodocker/settings.py# replace your default DATABASES section for this one
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangodocker_db',
'USER': 'root',
'PASSWORD': 'root',
'HOST': '127.0.0.1', # Or an IP Address that your DB is hosted on
'PORT': '3306',
}
}

Add the Python driver and attempt to run migrations.

$ pip install mysqlclient
$ python manage.py migrate
django.db.utils.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1' (111)")

Naturally, the connection to MySQL fails since we aren’t running any server locally. Instead of installing it in your OS as you normally would, we are going to run a Container with a MySQL installation inside it. Luckily Docker has a public hub which hosts lots of Container images with the most popular services. The one that includes a MySQL installation is called mysql and we are going to start it with the command docker run. We are also passing the -p parameter to expose the Port 3306 outside the container, and also a database name with a root password.

Note: If you need to install Docker for the first time you should check the official docs or any guide online depending your OS.

$ docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=djangodocker_db mysql
$ mysql -h127.0.0.1 -proot -uroot
mysql> show DATABASES;
$ python manage.py migrate

You can now check that your migrations run and your database is correctly initialized. But what happens if you run the Container again?

$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1d841feb5f40 mysql "docker-entrypoint.s…" 7 seconds ago Up 6 seconds 0.0.0.0:3306->3306/tcp dreaming_devartis
$ docker stop dreaming_devartis
$ docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=djangodocker_db mysql
$ python manage.py showmigrations

You’ll see that migrations are lost and you need to execute them again. This is because run command creates and starts a Container in the same action, so every time you use run you are actually creating a new Container from scratch. In order to reuse the same image with persisted data, first you need to find out the image name and then you can use the start command.

$ docker container ls --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
61f76cdccfd3 mysql "docker-entrypoint..." 7 minutes ago Exited (0) 4 seconds ago 0.0.0.0:3306->3306/tcp dreaming_devartis
$ docker start dreaming_devartis

NGINX

Similarly, we are going to follow the same process to launch a Container with an NGINX server running. The image we will be using is called nginx and you are encouraged to check the documentation in the Hub page.

The configuration file we are going to use for our basic application will look like this.

upstream django_server {
server 127.0.0.1:8001 fail_timeout=0;
}
server {
listen 80;
client_max_body_size 4G;
server_name localhost;
keepalive_timeout 5;
location /static/ {
root /usr/share/nginx/djangodocker/;
expires 30d;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://django_server;
break;
}
}
}

In order to use this file from the container we are going to create it on our project directory and map it as a Data Volume. Data Volumes are a way to share files between the host and the Container context. That way you can destroy and create new Containers that use those files while keeping them intact.

$ vim nginx.conf
$ docker run -p 80:80 -v /home/fara/code/djangodocker/nginx.conf:/etc/nginx/conf.d/default.conf:ro -d nginx

You can now run your Django application locally making sure you expose the development server to external requests and on the port 8001.

$ python manage.py runserver 0.0.0.0:8001

But when you browse to http://localhost/ you’ll see a Bad Request response, something in the reverse proxy configuration is failing. That’s because the Docker Container localhost IP refers to the Container itself and not to the outside host which is running the Python process. In order to make the configuration work we’ll need to find the IP address of our host from inside the network of the container. To do that we are going to run an interactive shell on the NGINX Container and use the route command to get the host IP.

$ docker container exec -it gossiping_globber /bin/bash
# apt update
# apt install net-tools
# route | awk ‘/^default/ { print $2 }’

Now update the nginx.conf file to use that IP address in the upstream_server section instead of the 127.0.0.1, restart the container and it should be working.

There’s one more thing to complete the configuration. If you browse to the /admin section you’ll see that static files are not working. That’s because NGINX is expecting to serve them from an inexistent location in the Container. We need to map it also with a Data Volume.

$ python manage.py collectstatic
$ docker run -v /home/fara/code/djangodocker/nginx.conf:/etc/nginx/conf.d/default.conf:ro -v /home/fernando/djangodocker/static/:/usr/share/nginx/djangodocker/static/ -p 80:80 nginx

Docker Compose

There’s an easier way to run both containers every time we are working on the project and that’s with the Docker Compose tool.
To use it we just need to create a docker-compose.yml file on our project directory with all the parameter we’ve been using to run our Containers. The file should look like this.

version: '2'
services:
db:
image: mysql
environment:
MYSQL_DATABASE: djangodocker_db
MYSQL_ROOT_PASSWORD: root
ports:
— "3306:3306"
nginx:
image: nginx
volumes:
— ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
— ./static:/usr/share/nginx/djangodocker/static
ports:
— "80:80"

You can now launch everything with the following command.

$ docker-compose up

Note: since this will be creating new Images you will have to run an interactive shell on the NGINX instance and check for the host IP to fix the nginx.conf file the first time you run the Container.

Next steps

You might have noticed by now that launching new images is very easy, and that the Hub already has lots of useful configurations to use on your projects. I encourage you to start playing with them and creating your own compose configurations; as you familiarize yourself with it you can start looking into the deployment possibilities.

Visit us!

--

--