June 14, 2015

Linking Docker containers - A POC using Django and PostgreSQL

As I mentioned in my earlier post, I was working on writing a blog about linking docker containers running Django and PostgreSQL.

Docker container linking is not a new thing. It’s been used widely by a lot of people out there. However, when I wanted to link a Django and PostgreSQL container, I could only find posts that explained the linking using some existing Djanngo code.

What I was actually looking for was the modifications to be made to settings.py, way to correctly run a postgres container, and then link them correctly. Since I couldn’t find such a post, I wrote one myself in the hope of it being useful for someone who is same situation as I was.

In case you don’t want to walkthrough the code and directly want to test Docker container linking with Django and PostgreSQL by pulling images from Docker Hub, click here.

This post doesn’t have any boilerplate code and walks you through steps right from creating a Django project to creating the containers and then linking them in the end.

Software versions:

  • Ubuntu 15.04 (64-bit)
  • Docker 1.6.2
  • Django 1.8.2
  • Django Rest Framework 3.1.3
  • psycopg2 2.6

Docker images:

First we will create a Django project and see if things work well on the local system. Follow below steps to create a Django project and an app:

$ mkcd link_containers
$ virtualenv venv
$ source venv/bin/activate
$ pip install django djangorestframework psycopg2
$ django-admin startproject docker_links
$ cd docker_links
$ python manage.py startapp api

Modify docker_links/settings.py to have below information:

INSTALLED_APPS = (
    ..
    'rest_framework',
    'api',
)

and

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'docker_links',
        'USER': 'postgres',
        'PASSWORD': 'postgres123',
        'HOST': '127.0.0.1',
        'PORT': '5432'
    }
}

Make sure that DATABASES dictionary has settings that reflect configuration on your system!

To ensure that requests to api are redirected to api app, modify docker_links/urls.py:

from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^api/', include('api.urls')),
]

Let api/models.py have very basic code like below:

from django.db import models

class User(models.Model):

    name = models.CharField(max_length=255)
    class Meta:
        db_table = "users"

And apply migrations:

$ python manage.py makemigrations
$ python manage.py migrate

Next, have below code in api/serializers.py:

from rest_framework.serializers import ModelSerializer
from api.models import User

class UserSerializer(ModelSerializer):

    class Meta:
        fields = ('id', 'name',)
        model = User

api/urls.py:

from django.conf.urls import url, patterns

urlpatterns = patterns(
    'api.views',

    url(r'^users/(?P<pk>[0-9]+)',
        'user_detail', name='user_detail'),

    url(r'^users/new/$', 'user_create', name='user_create'),
)

and api/views.py:

from api.models import User
from api.serializers import UserSerializer

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status

@api_view(['GET'])
def user_detail(request, pk):
    try:
        user = User.objects.get(id=pk)
    except User.DoesNotExist:
        return Response({"error": "User with id %s does not exist" % pk})
    else:
        serializer = UserSerializer(user)
        return Response(serializer.data, status=status.HTTP_200_OK)

@api_view(['POST'])
def user_create(request):
    try:
        serializer = UserSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
    except:
        return Response({"error": "Invalid data"},
                        status=status.HTTP_400_BAD_REQUEST)
    else:
        return Response(serializer.data, status=status.HTTP_201_CREATED)

That is it. You can now test POST and GET requests by running the Django dev server:

$ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
June 13, 2015 - 15:09:46
Django version 1.8.2, using settings 'docker_links.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

I prefer using Postman REST client to test REST APIs.

If everything works well, make sure to have a requirements.txt file created using below command:

$ pip freeze > requirements.txt

requirements.txt could be in the same directory as manage.py.

Now that we have everything working on the host which has Django and PostgreSQL configured on it, we will move towards dockerizing the setup and link the containers running Django and PostgreSQL.

Create a Dockerfile like below:

FROM ubuntu:latest

RUN apt-get update && apt-get upgrade
RUN apt-get -y install python-pip
RUN apt-get install python-dev postgresql-server-dev-all -y

RUN mkdir /code && cd /code
WORKDIR /code
ADD docker_links /code

RUN pip install -r requirements.txt

and build an image out of it:

$ sudo docker build -t django-postgres-link .

Alternatively, you can also download image created using above Dockerfile from Docker Hub.

Going through Dockerfile line by line, python-pip package is required to install the project dependencies from earlier created requirements.txt file. python-dev and postgresql-server-dev-all packages are required to make sure that psycopg2 has its dependencies preinstalled. Then we copy the code from host system to the container and install dependencies from requirements.txt.

Quick setup

Start the PostgreSQL container:

$  sudo docker run --name db -p 5432:5432 -e POSTGRES_PASSWORD=postgres123 -d postgres

Create a database called docker_links in the postgres container:

$ psql -h <container-ip-address> -U postgres

Above command should work without specifying host with -h switch. I need to figure it out.

If you’re going to pull image from Docker Hub,

$ sudo docker pull dharmit/django-postgresql-link

Create a throwaway container to apply migrations from our Django app:

$ sudo docker run --rm --link db:db django-postgres-link python manage.py migrate

And finally create a container that would serve our REST APIs:

$ sudo docker run -d --name web -p 80:8000 --link db:db django-postgres-link python manage.py runserver 0.0.0.0:8000

You can now test API on localhost using the Postman REST client.

For any suggestions on improving above workflow, please ping me on twitter.