From ee2d3f8b95af45e00df1142d2cc52e43b86da810 Mon Sep 17 00:00:00 2001 From: AyoubHmadouch Date: Tue, 9 Jun 2026 08:43:18 +0000 Subject: [PATCH 1/2] update .env --- .env_sample | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.env_sample b/.env_sample index 9c00eafe7..9d56ad03d 100644 --- a/.env_sample +++ b/.env_sample @@ -7,6 +7,15 @@ DB_NAME=postgres DB_USERNAME=postgres DB_PASSWORD=postgres DB_PORT=5432 +DB_CONN_MAX_AGE=0 +DB_CONN_HEALTH_CHECKS=True + +# Gunicorn tuning +GUNICORN_WORKERS=4 +GUNICORN_MAX_REQUESTS=2000 +GUNICORN_MAX_REQUESTS_JITTER=200 +GUNICORN_TIMEOUT=120 +GUNICORN_KEEPALIVE=5 DJANGO_SETTINGS_MODULE=settings.develop ALLOWED_HOSTS=localhost,example.com From 39fb3bafc25e4c5d149e47e21096f8776e6c0d2a Mon Sep 17 00:00:00 2001 From: AyoubHmd Date: Tue, 9 Jun 2026 12:58:43 +0200 Subject: [PATCH 2/2] Add PgBouncer service for Postgres connection pooling and update Django DB connection settings --- docker-compose.yml | 29 +++++++++++++++++++++++++++++ src/gunicorn_run.py | 7 ++++++- src/settings/base.py | 4 ++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index f5b32b753..ae8e397cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,6 +136,35 @@ services: max-size: "20m" max-file: "5" + #---------------------------------------------------------------------------------------------------- + # pgbouncer for connection pooling to Postgres, configured to use SCRAM-SHA-256 authentication and ignore the + #---------------------------------------------------------------------------------------------------- + pgbouncer: + image: edoburu/pgbouncer:latest + container_name: pgbouncer + env_file: .env + environment: + - DB_USER=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=${DB_NAME} + - POOL_MODE=transaction + - MAX_CLIENT_CONN=2000 + - DEFAULT_POOL_SIZE=80 + - RESERVE_POOL_SIZE=20 + - AUTH_TYPE=scram-sha-256 + - IGNORE_STARTUP_PARAMETERS=extra_float_digits + depends_on: + - db + restart: unless-stopped + ports: + - 6432:5432 + logging: + options: + max-size: "20m" + max-file: "5" + #---------------------------------------------------------------------------------------------------- # Rabbitmq & Flower monitoring tool #---------------------------------------------------------------------------------------------------- diff --git a/src/gunicorn_run.py b/src/gunicorn_run.py index 32d972a13..d37e85a5d 100644 --- a/src/gunicorn_run.py +++ b/src/gunicorn_run.py @@ -72,7 +72,12 @@ def load(self): "errorlog": "-", "worker_class": "uvicorn.workers.UvicornWorker", "logger_class": StubbedGunicornLogger, - "capture_output": 'true' + "capture_output": 'true', + # Recycle workers periodically to release DB connections and avoid leaks + "max_requests": int(os.environ.get("GUNICORN_MAX_REQUESTS", "2000")), + "max_requests_jitter": int(os.environ.get("GUNICORN_MAX_REQUESTS_JITTER", "200")), + "timeout": int(os.environ.get("GUNICORN_TIMEOUT", "120")), + "keepalive": int(os.environ.get("GUNICORN_KEEPALIVE", "5")) } StandaloneApplication(app, options).run() diff --git a/src/settings/base.py b/src/settings/base.py index 47c76ad6b..fcc5ccf73 100644 --- a/src/settings/base.py +++ b/src/settings/base.py @@ -212,6 +212,10 @@ } } +# With ASGI workers and PgBouncer, keep Django DB connections short-lived. +DATABASES['default']['CONN_MAX_AGE'] = int(os.environ.get('DB_CONN_MAX_AGE', '0')) +DATABASES['default']['CONN_HEALTH_CHECKS'] = os.environ.get('DB_CONN_HEALTH_CHECKS', 'True').lower() == 'true' + # TODO: Pull this, leaving in case django-oauth-toolkit problems # # ============================================================================= # # SSL