Quickstart for a Django project with Docker

In this post we will see how to set up quickly a Django project with Docker, so that it will be less painful to set up a CI pipeline on any environment of your choice (AWS, DigitalOcean, etc.).

This is something that should be done as soon as possible when bootstrapping your project – stop doing things that require a set of pre-installed packages only available on your machine. The earlier you do this, the less painful it will be – yourself included!

1. Clone and/or Create a Repository

You will need a repository for your project, right? So, either clone one already existing or create a new one.

You can clone the repo I created for this tutorial so that we can refer to the same code:

$ git clone https://github.com/markon/django-rest-quickstart

2. Create environment with Pipenv

Pipenv is the recommended Python packaging tool.

Install it:

$ pip install pipenv

and create a new environment in your repo:

$ pipenv --three # if you want to use python 3

and verify that everything went fine:

Pipfile found at {...}/tutorials/django-rest-quickstart/Pipfile. 
Considering this to be the project home.

If you open the file, you will find the following content:

$ cat Pipfile
[source]
url = "https://pypi.python.org/simple"
verify_ssl = true

Essentially, a brand new project! However, we know we want to use Django, so, let’s add some dependencies:

[packages]
psycopg2 = ">=2.7.3.1"
Django = ">=1.11,<2.0"

[requires]
python_version = "3.6"

For this tutorial we don’t care right now about the specific Django version. Whatever is recent enough should be good – however, you should not do that on production! Many things can be updated between two versions, and having such a file checked in implies that whatever recent version will be taken – don’t use version ranges, unless you know what you are doing.

As we want to have deterministic builds, we could use Pipenv lock, that generates a file containing the specific versions we want to use.

$ pipenv lock
Locking [dev-packages] dependencies...
Locking [packages] dependencies...
Updated Pipfile.lock!

This way, we can easily “freeze” the dependencies to a specific version, so that everyone else will install exactly the same dependencies as we do – no more problems like “hey, I am using version 1.7.0.1 while you use 1.7.0.4“. One version for all of us!

3. Create a Dockerfile

Before starting a Django project, we really want to make sure not to skip this important step – creating a Dockerfile:

FROM python:3.6-alpine

ENV PYTHONUNBUFFERED 1

RUN apk add --repository http://dl-cdn.alpinelinux.org/alpine/v.3.6/main --no-cache postgresql-dev
RUN apk add --repository http://dl-cdn.alpinelinux.org/alpine/v.3.6/main --no-cache gcc
RUN apk add --repository http://dl-cdn.alpinelinux.org/alpine/v.3.6/main --no-cache python3-dev
RUN apk add --repository http://dl-cdn.alpinelinux.org/alpine/v.3.6/main --no-cache musl-dev

RUN mkdir /code
WORKDIR /code
ADD . /code/

RUN pip3 install pipenv
RUN pipenv --three
RUN pipenv install --deploy --system

EXPOSE 8000

CMD ["python", "manage.py", "runserver"]

To summarize what it does:

  • Use python:3.6-alpine
  • Install PostgreSQL (we will need it for our project)
  • Create a new directory /code inside the container and the current directory to it
  • Install Pipenv for Python 3
  • Expose port 8000 for our Django web service
  • Run manage.py runserver by default

4. Create a docker-compose.yaml file

Following the official documentation from Docker, let’s create now a docker-compose file:

version: '3'

services:
 db:
   image: postgres
 web:
   build: .
   command: python3 manage.py runserver 0.0.0.0:8000
   volumes:
    - .:/code
   ports:
     - "8000:8000"
   depends_on:
   - db

We mount the current directory as /code, so that we don’t have to build a new Docker image every time we change the code. However, be careful: if you update your dependencies (e.g., a little change in Pipfile), your Docker image needs to be updated, because they come pre-installed.

5. Create a Django project

Now we are finally able to create a Django project! Go to the root of your project

 docker-compose run web django-admin.py startproject djangorestapi .

You should now see a djangorestapi folder and a manage.py in your project.

6. Start coding your app

Finally, we can add some code to create some features. An example I would like you to refer to is based on the previous post – RBAC with django-rest-framework and django-guardian.

The full code is visible in the repository, as it’s a bit out of scope for this tutorial.

7. Test locally

Normally, you would want to run migrations first.

docker-compose run web python manage.py migrate

then you will be able to run

docker-compose up

and wait until your service is up and running. Then you can play with your app available on http://localhost:8000/api/profiles. Please follow the README file to see how to run the examples.

Note: sometimes you may get a ConnectionRefused error, because PostgreSQL is slower than Django to start up. In this case, you could just re-run the command twice.

 

RBAC + ACLs with django-rest-framework and django-guardian

Recently, I have been working on a personal project developed in Django. It was the first time I used django-rest-framework, and I got to say: it’s impressive how easy it has become to develop something with it.

Usually, RBAC is something not trivial to implement with the current technologies – lots of times you end up writing custom code here and there. Also, I see more and more often developers assigning roles to users and having a call to an ideal “has_role(whatever_role_here)” method to authorize a user. However, roles and their assignments can change over time: what needs to be checked is the particular permission needed to perform that action, not the role. A little hint here: try to go for a fine-grained, not a coarse-grained permission check, because you never know: an AdminRole may be allowed today to be able to view your billings, but tomorrow you’ll want to limit that action to an AccountingAdminRole – and this implies code changes!

Unfortunately, RBAC doesn’t allow to set specific permissions for individual objects, and sometimes you really need that – for example, you don’t want to allow any user to edit other users’ information. However, django-rest-framework and django-guardian, which is really the missing Django permission/authorization tool thanks to all the extremely useful shortcuts it provides, are two excellent tools that help you use RBAC effectively and overcome this limitation, so that you can extend the “role-delegation” behavior with custom ACLs (to have per-object permissions). In fact, by using the permissions API together with django-guardian, it’s really easy to implement multiple use cases to authorize your users. This way, you can benefit from RBAC, by assigning user/roles/permissions while at the same time you can use ACLs to assign individual permissions. Finally, although this is not really related to RBAC, there is support for third-party packages to do authentication, like JWT, or OAuth2.

In the following paragraphs we will set up authentication, authorization and filters for the entire project – meaning, for all our API endpoints/models exposed via django-rest-framework. The procedure to set custom authorization/filtering for only a few specific classes is described in the official documentation, and it’s really not difficult to set up.

1. Set Authentication type

For simplicity, we will assume we are using Basic/SessionAuthentication. Let’s start by writing down the following:


REST_FRAMEWORK = {
  'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework.authentication.BasicAuthentication',
    'rest_framework.authentication.SessionAuthentication',
  )
}

also, we need to add ‘guardian‘ and ‘rest_framework‘ to the list of INSTALLED_APPS.

After that, we can focus on the authorization part (django-rest-framework calls it permissions).

2. Set Authorization classes

One of the best features django-rest-framework provides is the so called “per-object permissions on models” implemented via the DjangoObjectPermissions class. For example, let’s suppose that User A creates a new Post 123, while User B creates a new Post 456, we want these two users to be able to perform actions only on the the Post they have created – we don’t want User A to mess up with Post 456.

By using DjangoObjectPermissions, we can easily map who can do what on which object. However, as the documentation says, you will need to install django-guardian.

Normally, in addition to the already existing operations that Django supports out of the box, add, change and delete, you’ll probably want to add some limitations on who can view specific objects – this works so good with the concept of filters!

You will need to add the following code somewhere in your project:


from rest_framework import permissions

class CustomObjectPermissions(permissions.DjangoObjectPermissions):
  """
  Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
  """
  perms_map = {
    'GET': ['%(app_label)s.view_%(model_name)s'],
    'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
    'HEAD': ['%(app_label)s.view_%(model_name)s'],
    'POST': ['%(app_label)s.add_%(model_name)s'],
    'PUT': ['%(app_label)s.change_%(model_name)s'],
    'PATCH': ['%(app_label)s.change_%(model_name)s'],
    'DELETE': ['%(app_label)s.delete_%(model_name)s'],
  }

At this point, we have to set this class in the REST_FRAMEWORK map we have declared above:

REST_FRAMEWORK = {
  ...,
  'DEFAULT_PERMISSION_CLASSES': (
    'module_containing.CustomObjectPermissions',
  ),
}

and have the following to enable django-guardian backend:

AUTHENTICATION_BACKEND = (
    'django.contrib.auth.backends.ModelBackend', # default
    'guardian.backends.ObjectPermissionBackend',
)

So far, we have added some configuration to tell django-rest-framework that we would like to have the possibility to use permissions on individual objects. However, we have not specified yet that we want to limit the objects a logged-in user is allowed to view.

3. Add filtering for ‘view’ operations

django-rest-framework recommends to use DjangoObjectsPermissionFilter. In order to do so, we need to add one more class to the REST_FRAMEWORK map:

REST_FRAMEWORK = {
  ...,
  ...,
  'DEFAULT_FILTER_BACKENDS': (
    'rest_framework.filters.DjangoObjectPermissionsFilter',
  ),
}

4. Add ‘view’ permissions to your models

As already mentioned, Django doesn’t come with a view permission on models. Therefore, we will have to add this manually for each model, like the following code shows:

class UserProfile(models.Model):
  user = models.OneToOneField(settings.AUTH_USER_MODEL,
                              on_delete=models.CASCADE,
                              related_name='profile')

  class Meta:
    permissions = (
      ('view_userprofile', 'View UserProfile'),
    )

Once we create a User and we give it a UserProfile, the logged-in user will only be able to retrieve his own UserProfile object. Also, don’t forget to create a migration for this and for the models used by django-guardian.

5. Create views and serializers

At the end, of course, we should not forget to create the serializers and views:

class UserProfileViewSet(viewsets.ModelViewSet):
  serializer_class = UserProfileSerializer
  queryset = UserProfile.objects.all()

I leave the serializer implementation up to you. 🙂

Note: a fully functioning sample can be found here: https://github.com/markon/django-rest-quickstart.

Further Readings