Django Service with Gunicorn

PostgreSQL Mar 16, 2024

This will be a quick walkthrough of standing up basic Django with some fancy development tools like IntelliJ, mypy, and black. In addition to Django we will use DRF to implement a REST API and Gunicorn to expand on the basic Django runserver.

This guide acts as a precursor, setting the stage for future posts where this Django service will be utilized as a dependency of the SQS consumer we created in this guide https://honstain.com/asyncio-sqs-and-httpx/.

Why would you be interested in this write-up?

  • You're looking for some practical examples of setting Django with IntelliJ.
  • You're interested in Gunicorn configurations for Django services.
    • This will give us some basic tooling to consider performance in future blog posts.

What this post is not:

  • A replacement for the excellent Django tutorial https://docs.djangoproject.com/en/5.0/intro/. We assume you have gone through the tutorial or can wing it.
  • Security or CI/CD - we are primarily focused on getting a service setup for experimenting with development tools and eventually scaling/performance issue (subsequent posts).
💡
Source code for the django service discussed in this post: https://github.com/AnthonyHonstain/django-user-service

Overview of Key Dependencies

The dependencies most relevant to this blog:

Dependency Notes
gunicorn 21.2.0 This is the thing subsequent posts will look at the most. https://gunicorn.org/
djangorestframework 3.14.0 The only real logic we create for this service will be a DRF endpoint and a Django model. This version doesn't officially support Django 5 but I wanted to try it anyway. https://www.django-rest-framework.org/#
django 5.0.2 Giving the new version 5 a test drive. https://www.djangoproject.com/
psycopg2-binary 2.9.9 Needed for Postgres https://github.com/psycopg/psycopg2
postgres:16.2 The PostgreSQL docker image https://hub.docker.com/_/postgres
black 24.2.0 I have been getting a ton of value here, ymmv. I love that it consistently and with no effort on my part produces very readable and organized formatting. https://black.readthedocs.io/en/stable/
mypy 1.8.0 Static type checker - I still bump into odd things, but have found the juice worth the squeeze. https://mypy.readthedocs.io/en/stable/
poetry Using poetry in place of pip, this feels more natural for production systems.

Setup The Django Service

The idea here is to take the source code and get it running locally.

Get the Source Code

I have a public github repo you can pull or copy to get started.

GitHub - AnthonyHonstain/django-user-service: An example Django 5.0 service with DRF, postgres, gunicorn.
An example Django 5.0 service with DRF, postgres, gunicorn. - AnthonyHonstain/django-user-service
⚠️
This post does not contain enough information to take you through building a Django service from scratch. Please consider the Django tutorial if your looking for that sort of experience.

Get Mamba or Decide on an Alternative

Similar to the previous SQS consumer post we are going to assume you are using mamba.

Micromamba User Guide — documentation
# CD into the project directory
mamba create -n django-user-service -c conda-forge  python=3.12
mamba activate django-user-service

pyproject.toml and Poetry

We will be using poetry to manage the python dependencies for this service.

Poetry - Python dependency management and packaging made easy
Python dependency management and packaging made easy

This is the pyproject.toml for the repo https://github.com/AnthonyHonstain/django-user-service/blob/main/pyproject.toml

In the mamba environment you already have active:

pip install poetry
poetry install --no-root

Django and mypy

I set this Django service up using mypy, there was one thing I missed that you might also want to be aware of. Its not enough to just add mypy and django-stubs.

The mypy configuration file: https://github.com/AnthonyHonstain/django-user-service/blob/main/mypy.ini

Without the mypy configuration I ran into a number of mypy errors.

EXAMPLE ERRORS

❯ mypy .
usercore/models.py:5: error: Need type annotation for "name"  [var-annotated]
usercore/models.py:6: error: Need type annotation for "age"  [var-annotated]
user_service/settings.py:30: error: Need type annotation for "ALLOWED_HOSTS" (hint: "ALLOWED_HOSTS: List[<type>] = ...")  [var-annotated]
Found 3 errors in 2 files (checked 15 source files)

We have the django-stubs dependency set, but we need to help mypy along with that mypy.ini file.

References:

The mypy configuration file - mypy 1.9.0 documentation
Integrating mypy into a Django project | Ralph Minderhoud
GitHub - typeddjango/django-stubs: PEP-484 stubs for Django
PEP-484 stubs for Django. Contribute to typeddjango/django-stubs development by creating an account on GitHub.

IntelliJ and Django

I will provide some examples of configuring IntelliJ to work with Django, I didn't find it super intuitive and hope this could help others.

IntelliJ IDEA – the Leading Java and Kotlin IDE
IntelliJ IDEA is undoubtedly the top-choice IDE for software developers. It makes Java and Kotlin development a more productive and enjoyable experience.

You can skip this if you use an alternative editor.

Django and Python Black

I also found python Black to play nice with Django and have included an example screenshot of enabling the Black formatted automatically in IntelliJ. This automated the majority of the formatting activities.

Getting Django Running

Standing up the database

The core of this service is going to be a postgres database, which we will standup using docker compose.

docker-compose.yml

version: '3.8'

services:
  db:
    image: postgres:16.2
    volumes:
      - postgres_data_product:/var/lib/postgresql/data/
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    ports:
      - '5432:5432'
    # Logging for every query - can comment out the entire line to disable
    #     Reference: https://stackoverflow.com/a/58806511
    command: ["postgres", "-c", "log_statement=all"]

volumes:
  postgres_data_product:

Success looks like:

db-1 | 2024-02-27 15:02:00.353 UTC [1] LOG: database system is ready to accept connections

Connect IntelliJ to the Database

It should only require you to set the user and password from the docker-compose file.

You won't have much to look at until you run the migrations.

Run the Django Migrations

You can start running the manage.py commands to initialize your Django service.

python manage.py migrate

You can refresh the postgres schema in IntelliJ to view the results of the migration. Success looks like you now have a table called usercore_user.

This is also a reasonable time to create a superuser for local development.

python manage.py createsuperuser

Overview of the API

This service uses DRF https://www.django-rest-framework.org/#installation to serve as a basic REST API.

The model here is just a laughably basic user with three fields.

from django.db import models


class User(models.Model):
    name = models.CharField(max_length=200)
    age = models.IntegerField()

And we will make a basic DRF ModelViewSet for it.

import structlog

from rest_framework import serializers, viewsets

from .models import User

logger = structlog.get_logger(__name__)


# Serializers define the API representation.
class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ["id", "name", "age"]


# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    def list(self, request, *args, **kwargs):
        logger.info("UserViewSet list called", size=str(len(self.queryset)))
        return super(UserViewSet, self).list(request, *args, **kwargs)

    def create(self, request, *args, **kwargs):
        logger.info("Creating a new user", body=request.data)
        return super(UserViewSet, self).create(request, *args, **kwargs)

Running the service python manage.py runserver if you haven't already will let us exercise the endpoint using the DRF UI.

http://localhost:8000/usercore/users/

You also have the basic Django Admin UI http://localhost:8000/admin/

IntelliJ HTTP Client

I found the IntelliJ HTTP client to be a handy way to exercise simple endpoints, postman would also work here.

HTTP Client | IntelliJ IDEA
Explore the features of the HTTP Client plugin: compose and execute HTTP requests, view responses, configure proxy settings, certificates, and more.
### GET request list
GET http://localhost:8000/usercore/users/
Accept: application/json

### GET request single record
GET http://localhost:8000/usercore/users/40000/
Accept: application/json

### POST request create single record
POST http://localhost:8000/usercore/users/
Accept: application/json
Content-Type: application/json

{"name":"Anthony", "age":2}

Starting Gunicorn

The Django runserver is useful for local development, but we want to get a more capable web server more detailed investigation.

Since we should have already installed gunciorn with our python dependencies, we can start it using:

gunicorn --log-level debug --bind 0.0.0.0:8000 user_service.wsgi -w 1

This uses the wsgi module Django already created for us, and sets one default worker.

Shooting a few calls into the server should look something like this:

This service is also setup with structlog and emits JSON formatted logs to the logs folder in the project directory.

tail -f json.log | jq


{
  "request": "GET /usercore/users/",
  "user_agent": "Apache-HttpClient/4.5.14 (Java/17.0.10)",
  "event": "request_started",
  "ip": "127.0.0.1",
  "request_id": "f4449541-2eea-4337-ad63-fb24de0c0d35",
  "timestamp": "2024-03-15T15:06:48.697754Z",
  "logger": "django_structlog.middlewares.request",
  "level": "info"
}
{
  "size": "3",
  "event": "UserViewSet list called",
  "ip": "127.0.0.1",
  "request_id": "f4449541-2eea-4337-ad63-fb24de0c0d35",
  "timestamp": "2024-03-15T15:06:48.767477Z",
  "logger": "usercore.views",
  "level": "info"
}
{
  "code": 200,
  "request": "GET /usercore/users/",
  "event": "request_finished",
  "ip": "127.0.0.1",
  "user_id": null,
  "request_id": "f4449541-2eea-4337-ad63-fb24de0c0d35",
  "timestamp": "2024-03-15T15:06:48.770240Z",
  "logger": "django_structlog.middlewares.request",
  "level": "info"
}

Summary

At this stage you should now have a Django service with DRF that can serve a basic API via Gunicorn (and has logging, tests, and a real PostgreSQL database).

GitHub - AnthonyHonstain/django-user-service: An example Django 5.0 service with DRF, postgres, gunicorn.
An example Django 5.0 service with DRF, postgres, gunicorn. - AnthonyHonstain/django-user-service

In subsequent posts, we will explore more of having our SQS consumer, call this service.

Tags