Django PostgreSQL Database Connection Pooling with PgBouncer
March 5, 2023
Introduction
In this post, we will learn how to use PostgreSQL database connection pooling with PgBouncer for Django applications.
What is database connection pooling?
Database connection pooling is a technique that allows an application to reuse database connections instead of creating a new connection for each request. This reduces the cost of opening and closing connections to the database server by maintaining a pool of connections that can be reused. This improves the performance of the database and reduces the number of connections to the database server.
What is PgBouncer?
PgBouncer is a lightweight connection pooler for PostgreSQL. It can sit between the application and the database server and manage the connections to the database server.
Why use PgBouncer?
- Low memory requirements (2 kB per connection by default).
- This is because PgBouncer does not need to see full packets at once.
- It is not tied to one backend server.
- The destination databases can reside on different hosts.
Note: This is copied from the PgBouncer Features Page.
Prerequisites
To follow this tutorial, you need to have the following installed:
- Docker
- Docker Compose
We are going to use Docker and Docker Compose to run Django, PostgreSQL and PgBouncer.
Create a New Django Project
First, we need to create a project directory. We will call it try-dj-pgbouncer.
Then, we need to create a requirements.txt file inside the project directory that will contain the dependencies for our Django project.
Add the following dependencies to the requirements.txt file:
- Specify the version of
Djangothat we want to use. - Specify the version of
psycopg2that we want to use. This is the PostgreSQL database adapter for Python. - Specify the version of
dj-database-urlthat we want to use. This is a Django utility that allows us to configure the database using a URL.
Now we can add the Dockerfile for our Django project.
Add the following content to the Dockerfile:
FROM python:3.10-slim-bullseye
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Set the working directory
WORKDIR /app
RUN apt-get update \
&& apt-get install \
-y \
--no-install-recommends \
--no-install-suggests \
# Required for psycopg2
gcc \
g++ \
libpq-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Copy requirements.txt to the container
COPY ./requirements.txt .
# Install Python dependencies
RUN pip install -r requirements.txt
# Copy application code to the container
COPY . .
Let's build the Docker image.
Now, we can create a Django project using Docker.
This command will create a Django project inside the current directory.
We can also create a Django app called core using Docker.
This command will create a Django app called core inside the try-dj-pgbouncer directory.
We need to add the core app to the INSTALLED_APPS list in the try_dj_pgbouncer/settings.py file.
Setup Docker Compose for Django and PostgreSQL
We need to create a docker-compose.yaml file at the root of the project (try-dj-pgbouncer/) that will contain the docker-compose configuration
for our Django, PostgreSQL and PgBouncer services.
Add the following content to the docker-compose.yaml file:
version: '3.9'
services:
web: # (1)!
build:
context: . # (2)!
volumes:
- .:/app # (3)!
env_file:
- ./.env # (4)!
ports:
- 8000:8000 # (5)!
depends_on:
- db # (6)!
command: > # (7)!
bash -c "while !</dev/tcp/db/5432; do sleep 1; done;
python manage.py runserver 0.0.0.0:8000"
db:
image: postgres:13.10-alpine # (8)!
environment:
- POSTGRES_PASSWORD=postgres # (9)!
volumes:
- postgres_data:/var/lib/postgresql/data/ # (10)!
healthcheck: # (11)!
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data: # (12)!
- The
webservice is the Django Application. - The
buildcontext is the current directory where theDockerfileis located. - This will mount the current directory to the
/appdirectory inside the container. - This will load the environment variables from the
.envfile. - This will expose the port
8000of the container to the host machine's port8000. - This will make sure that the
dbservice is started before thewebservice. - This
commandwill wait for thedbservice to be ready before starting the Django server. - The
dbservice will use thepostgres:13.10-alpineimage. - The
POSTGRES_PASSWORDenvironment variable will set the Database password topostgres. - The
volumeswill mount thepostgres_datavolume to the/var/lib/postgresql/data/directory inside the container. This will ensure that the database data is persisted between container restarts. - This will check if the database is ready to accept connections.
- This will create a volume called
postgres_datathat will be used to persist the database data.
We also need to create a .env file at the root of the project (try-dj-pgbouncer/) that will contain the environment variables for the docker-compose configuration.
Add the following content to the .env file:
How to get DATABASE_URL ?
The format of the DATABASE_URL is as follows:
DATABASE_URL=<ENGINE>://<USER>:<PASSWORD>@<HOST>:<PORT>/<NAME>
Where:
ENGINE: The database engine. For PostgreSQL, it ispostgres.USER: The database user.PASSWORD: Password for the database user.HOST: The database host. As we are using docker-compose, we can use the service name (e.g:db) as the host.PORT: The port on which the database is accepting connections. For PostgreSQL, it is5432.NAME: Name of the database.
Configure Django Settings to use PostgreSQL
We need to configure the Django settings to use PostgreSQL as the database engine.
We will use the dj-database-url package to configure the database using a DATABASE_URL we defined in the .env file.
Add the following content to the try_dj_pgbouncer/settings.py file:
# ...
import dj_database_url # (1)!
# ...
DATABASES = {
'default': dj_database_url.config() # (2)!
}
- This will import the
dj_database_urlpackage. - Here we are using the
dj_database_url.config()function to configure thedeafultDATABASESsetting for Django. This function will read theDATABASE_URLenvironment variable and configure theDATABASESsetting accordingly.
This will configure the DATABASES setting to use PostgreSQL as the database engine.
Add Django Model and View
We need to add Django models and views to test the database connection pooling.
First, we need to update core/models.py file to add the Post model:
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
Next, we need to update core/admin.py file to register the Post model:
Then, we need to update core/views.py file to add the PostistView view:
from django.views.generic import ListView
from .models import Post
class PostistView(ListView):
model = Post
template_name = 'core/post_list.html'
Now, we need to create core/urls.py file to add the PostistView view:
Add the following content to the core/urls.py file:
from django.urls import path
from .views import PostistView
urlpatterns = [
path('', PostistView.as_view(), name='post_list'),
]
We also need to update try_dj_pgbouncer/urls.py file to include the core.urls:
from django.contrib import admin
from django.urls import path, include # (1)!
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')), # (2)!
]
- This will import the
includefunction from thedjango.urlsmodule. - This will include the
core.urlsmodule.
Finally, we need to create a template file core/templates/core/post_list.html to render the list of posts:
Add the following content to the core/templates/core/post_list.html file:
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Post List</title>
<body>
{% block content %}
<h1>Post List</h1>
<ul>
{% for post in object_list %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
{% endblock %}
</body>
</html>
Now, we can run Django migrations to create the Post table in the database:
docker-compose run --rm web python manage.py makemigrations # (1)!
docker-compose run --rm web python manage.py migrate # (2)!
- This will create the migration files for the
Postmodel. - This will run the migrations to create the
Posttable in the database.
We also create an admin user to access the Django admin.
Now, we can run the Django development server.
The Django development server will be available at: http://localhost:8000/
We can also visit Django Admin at http://localhost:8000/admin/ and login with the admin user we created. Then we can create some posts from the Django admin.
After creating some posts, we can visit the post list page at http://localhost:8000/ and see the list of posts.
Note
Here we are using PostgreSQL as the database engine for Django. In the next section, we will see how to use PgBouncer to provide database connection pooling for Django applications.
Final Project Structure Should Look Like This:
try-dj-pgbouncer
├── core
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── core
│ │ └── post_list.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── docker-compose.yaml
├── Dockerfile
├── manage.py
├── requirements.txt
└── try_dj_pgbouncer
├── asgi.py
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
Add and Configure PgBouncer
We need to add PgBouncer service to our docker-compose.yaml file to provide database connection pooling for Django applications.
Add the following content to the docker-compose.yaml file:
version: '3.9'
services:
web:
build:
context: .
volumes:
- .:/app
env_file:
- ./.env
ports:
- 8000:8000
depends_on:
- db
- pgbouncer
command: >
bash -c "while !</dev/tcp/db/5432; do sleep 1; done;
python manage.py runserver 0.0.0.0:8000"
db:
image: postgres:13.10-alpine
environment:
- POSTGRES_PASSWORD=postgres
volumes:
- postgres_data:/var/lib/postgresql/data/
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
pgbouncer: # (1)!
image: edoburu/pgbouncer:1.18.0 # (2)!
env_file:
- ./.env # (3)!
environment:
DATABASE_URL: "${POSTGRES_DATABASE_URL}" # (4)!
depends_on:
- db # (5)!
volumes:
postgres_data:
- This will add a new service named
pgbouncer. - We will use the
edoburu/pgbouncer:1.18.0Docker image. - This will load the environment variables from the
.envfile. - This will set the
DATABASE_URLenvironment variable to thePOSTGRES_DATABASE_URLenvironment variable which will be set in the.envfile. - This will add a dependency on the
dbservice. This will ensure that thedbservice will be started before thepgbouncerservice.
Why Use edoburu/pgbouncer Docker Image ?
The official pgbouncer/pgbouncer Docker image was last updated 2 years ago (November 2020).
But edoburu/pgbouncer is updated regularly and is the most popular (over 10+ Million downloads) PgBouncer Docker image on Docker Hub.
You can also use bitnami/pgbouncer Docker image as an alternative which is also updated regularly. (Some of the configuration options are a bit different)
Now, we need to update the .env file.
DATABASE_URL=postgres://postgres:postgres@pgbouncer:5432/postgres # (1)!
POSTGRES_DATABASE_URL=postgres://postgres:postgres@db:5432/postgres # (2)!
POOL_MODE=transaction # (3)!
MAX_DB_CONNECTIONS=100 # (4)!
DEFAULT_POOL_SIZE=40 # (5)!
- This will set the
DATABASE_URLenvironment variable to use thepgbouncerservice instead of thedbservice. - This will set the
POSTGRES_DATABASE_URLenvironment variable to use thedbservice.pgbouncerservice will use this environment variable to connect to thedbservice. - This will set the
POOL_MODEtotransaction. You can also set this tosessionorstatementpooling mode. You can find more information about the different pooling modes in the official PgBouncer documentation. - Do not allow more than this many server connections per database (regardless of user). This considers the PgBouncer database that the client has connected to, not the PostgreSQL database of the outgoing connection.
- How many server connections to allow per user/database pair.
There are many other configuration options available in the official PgBouncer documentation. You can use environment variables to set these configuration options.
The name of the environment variable should be the same as the name of the configuration option but capitalized. (e.g: pool_mode -> POOL_MODE, max_db_connections -> MAX_DB_CONNECTIONS)
Note
You can also use the pgbouncer.ini file to set the configuration options by attaching a volume (e.g: pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro).
You can find more information about pgbouncer.ini in the official PgBouncer documentation.
We also need to update Django settings file.
# ...
import dj_database_url
# ...
DATABASES = {
'default': dj_database_url.config()
}
DATABASES['default']['DISABLE_SERVER_SIDE_CURSORS'] = True
Why are we setting DISABLE_SERVER_SIDE_CURSORS to True ?
Using a connection pooler in transaction pooling mode (e.g. PgBouncer) requires disabling server-side cursors for that connection.
Server-side cursors are local to a connection and remain open at the end of a transaction when AUTOCOMMIT is True.
A subsequent transaction may attempt to fetch more results from a server-side cursor. In transaction pooling mode,
there’s no guarantee that subsequent transactions will use the same connection. If a different connection is used,
an error is raised when the transaction references the server-side cursor,
because server-side cursors are only accessible in the connection in which they were created.
One solution is to disable server-side cursors for a connection in DATABASES by setting DISABLE_SERVER_SIDE_CURSORS to True.
Now, we can run the development server again.
We can visit the post list page at http://localhost:8000/ and see the list of posts. Now Django server is using the pgbouncer service to connect to the db service.
Top Level Architecture Diagram
flowchart LR
A[Browser] <--> B(Django)
B <--> C(PgBouncer)
C <--> id1[(PostgreSQL)]
References
- Django Transaction pooling and server-side cursors
- PgBouncer
- Edoburu PgBouncer Docker Hub
- Bitnami PgBouncer Docker Hub
- Official PgBouncer Docker Hub
Conclusion
In this post, we learned how to use PostgreSQL database connection pooling with PgBouncer for Django applications. In production environments, PgBouncer can be really useful to reduce the number of connections to the database server and provide significant performance improvements. You can run PgBouncer as a separate service alongside you Django application and PostgreSQL database server. If your servers are hosted in AWS you can also check out AWS RDS Proxy which is a managed service that provides connection pooling for RDS databases.