Lecture : 13 minutes · Niveau : intermédiaire-avancé · Mise à jour : avril 2026
Faire tourner Django en local est trivial. Le faire tourner en production fiable, performant, sécurisé et déployable sans downtime demande des choix précis. Ce guide rassemble les pratiques opérationnelles éprouvées en 2026 pour des PME qui veulent livrer du Django sérieux.
Voir aussi → Django pour PME : guide backend Python.
Sommaire
- Choix d’hébergement Django en 2026
- Gunicorn : serveur WSGI standard
- Uvicorn pour ASGI/async
- Whitenoise pour les static files
- Reverse proxy : Caddy ou Nginx
- Docker et conteneurisation
- Settings production sécurisés
- Base de données et Redis
- Celery pour le travail asynchrone
- Monitoring et logs
- Déploiement zéro downtime
- FAQ
1. Choix d’hébergement Django en 2026
Plusieurs options selon contexte PME.
PaaS (Render, Railway, Fly.io) : déploiement git push, scaling automatique, base de données managée. Simplicité maximale. Adapté aux MVPs et PME jusqu’à un certain volume.
VPS auto-géré (Hetzner, OVH, Scaleway, hébergeurs ouest-africains) : contrôle total, prix maîtrisé, gestion technique à assumer. Compétence sysadmin nécessaire.
Heroku : pionnier du PaaS Django, mais moins compétitif sur le prix qu’avant. Tier gratuit supprimé en 2022.
AWS Elastic Beanstalk / GCP Cloud Run / Azure App Service : robustes, intégrés à l’écosystème cloud, plus complexes que PaaS, plus flexibles.
Kubernetes : pour des organisations matures avec plusieurs services et équipes. Sur-dimensionné pour la majorité des PME.
Recommandation par profil
- MVP / startup : Render ou Railway. Configuration en quelques minutes.
- PME établie : VPS Hetzner avec stack Docker, ou Render si l’équipe veut zéro infra à gérer
- Croissance : Migration progressive vers AWS/GCP ou Kubernetes si vraiment nécessaire
2. Gunicorn : serveur WSGI standard
Django est généralement servi via Gunicorn (Green Unicorn) en production WSGI.
uv add gunicorn
gunicorn monsite.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 4 \
--worker-class sync \
--timeout 60 \
--access-logfile - \
--error-logfile -
Configuration
--workers: nombre de processus. Règle empirique :2 × nombre de cœurs CPU + 1--worker-class:sync(défaut),geventpour I/O concurrent,uvicorn.workers.UvicornWorkerpour ASGI--timeout: tue un worker bloqué après N secondes (défaut 30, augmenter si traitements longs)--max-requests: recycle un worker après N requêtes (atténue les fuites mémoire)
Fichier de config Python
Pour des configs plus complexes, créer gunicorn.conf.py :
import multiprocessing
bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
timeout = 60
keepalive = 5
max_requests = 1000
max_requests_jitter = 100
preload_app = True # économise mémoire en chargeant l'app une fois avant fork
accesslog = "-"
errorlog = "-"
loglevel = "info"
gunicorn -c gunicorn.conf.py monsite.wsgi:application
preload_app=True charge l’app dans le processus master avant de forker les workers. Économise de la mémoire (chaque worker partage la même image initiale).
3. Uvicorn pour ASGI/async
Pour des applications avec vues async ou Django Channels (WebSockets) :
uv add uvicorn
uvicorn monsite.asgi:application \
--host 0.0.0.0 --port 8000 \
--workers 4
Ou en combo Gunicorn + Uvicorn workers :
gunicorn monsite.asgi:application \
-k uvicorn.workers.UvicornWorker \
--workers 4 \
--bind 0.0.0.0:8000
Cette approche bénéficie de la robustesse de Gunicorn (supervision worker, max-requests) avec la performance ASGI d’Uvicorn.
4. Whitenoise pour les static files
Django ne sert pas les fichiers statiques en production par défaut. Solution simple : Whitenoise.
uv add whitenoise
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # juste après SecurityMiddleware
# ...
]
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
CompressedManifestStaticFilesStorage :
– Compresse les fichiers statiques (gzip/brotli)
– Ajoute un hash dans le nom de fichier pour cache long terme
– Permet de mettre des cache headers agressifs (1 an)
Build des assets
python manage.py collectstatic --noinput
À intégrer au pipeline de déploiement.
Pour un trafic important
Whitenoise est suffisant jusqu’à un trafic significatif. Pour des sites avec millions de pages vues : déléguer les statics à un CDN (Cloudflare, BunnyCDN, AWS CloudFront) en frontend.
5. Reverse proxy : Caddy ou Nginx
Django/Gunicorn ne devraient jamais être directement exposés sur Internet. Reverse proxy en frontal pour HTTPS, compression, rate limiting basique.
Caddy : le plus simple
django.exemple.com {
reverse_proxy localhost:8000
encode gzip zstd
log {
output file /var/log/caddy/django.log
}
}
HTTPS automatique avec Let’s Encrypt, configuration minimaliste. Choix excellent pour démarrer.
Nginx : plus flexible
upstream django {
server unix:/run/gunicorn.sock fail_timeout=0;
}
server {
listen 443 ssl http2;
server_name django.exemple.com;
ssl_certificate /etc/letsencrypt/live/django.exemple.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/django.exemple.com/privkey.pem;
client_max_body_size 50M;
location /static/ {
alias /var/www/app/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /var/www/app/media/;
}
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Plus de configuration mais plus flexible. Conventionnel sur stack Linux.
6. Docker et conteneurisation
FROM python:3.12-slim AS base
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
# Dépendances système pour psycopg, Pillow, etc.
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev libjpeg-dev zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# Installer uv
RUN pip install --no-cache-dir uv
# Dépendances Python
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
# Copier l'app
COPY . .
# Collecter les statics
RUN uv run python manage.py collectstatic --noinput
# User non-root
RUN useradd --create-home --shell /bin/bash appuser \
&& chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["uv", "run", "gunicorn", "monsite.wsgi:application", \
"--bind", "0.0.0.0:8000", "--workers", "4"]
Compose pour orchestration
services:
app:
image: ghcr.io/ma-pme/django-app:latest
restart: unless-stopped
environment:
DATABASE_URL: postgres://app:${DB_PASS}@db:5432/app
DJANGO_SETTINGS_MODULE: monsite.settings.production
depends_on:
db: { condition: service_healthy }
redis: { condition: service_started }
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]
interval: 30s
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASS}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
redis:
image: redis:7-alpine
restart: unless-stopped
celery:
image: ghcr.io/ma-pme/django-app:latest
restart: unless-stopped
command: uv run celery -A monsite worker -l info
depends_on:
- db
- redis
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports: ["80:80", "443:443"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
volumes:
db-data:
caddy-data:
Voir Docker en production pour PME.
7. Settings production sécurisés
# settings/production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
# HTTPS strict
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000 # 1 an
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Headers de sécurité
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'same-origin'
# Cookies
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
# Database via env
DATABASES = {'default': env.db('DATABASE_URL')}
DATABASES['default']['CONN_MAX_AGE'] = 60 # connection pooling
# Redis pour cache et sessions
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': env('REDIS_URL'),
}
}
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# Email
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = env('EMAIL_HOST')
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
# Logging structuré
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'json': {
'()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'json',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
}
Vérifier la sécurité
python manage.py check --deploy
Liste les configurations de sécurité manquantes ou faibles.
8. Base de données et Redis
PostgreSQL en production
# Configuration recommandée
# postgresql.conf
shared_buffers = 25% de la RAM
effective_cache_size = 75% de la RAM
work_mem = 16MB
maintenance_work_mem = 256MB
max_connections = 100 # selon nombre de workers Gunicorn × Django
Connection pooling
Django ouvre une connexion DB par requête par défaut. Sur volume important : épuisement des connexions.
Solutions :
– CONN_MAX_AGE : Django garde la connexion ouverte N secondes (défaut 0). Mettre 60-300 en prod.
– PgBouncer : pooler externe, plus performant pour gros volumes
– Cloud DB managées : intègrent souvent un pooler
Redis pour cache et sessions
Indispensable au-delà de quelques utilisateurs concurrents :
– Cache applicatif
– Sessions (au lieu de DB pour vitesse)
– Queue Celery
– Channel layer Django Channels
9. Celery pour le travail asynchrone
Pour les tâches longues (envoi mail, génération PDF, appels API tiers, traitement images) : Celery.
uv add celery redis
# monsite/celery.py
from celery import Celery
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'monsite.settings.production')
app = Celery('monsite')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
# settings.py
CELERY_BROKER_URL = env('REDIS_URL')
CELERY_RESULT_BACKEND = env('REDIS_URL')
CELERY_TASK_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TIMEZONE = 'Africa/Dakar'
# clients/tasks.py
from celery import shared_task
@shared_task
def envoyer_email_bienvenue(client_id):
client = Client.objects.get(pk=client_id)
send_email(client.email, 'Bienvenue', '...')
# Usage
envoyer_email_bienvenue.delay(client.id)
Lancer un worker :
celery -A monsite worker -l info
Pour le scheduling type cron : celery beat.
10. Monitoring et logs
Sentry pour les erreurs
uv add sentry-sdk
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=env('SENTRY_DSN'),
integrations=[DjangoIntegration()],
traces_sample_rate=0.1, # 10% des transactions pour APM
send_default_pii=False,
)
Capture toutes les exceptions non gérées avec contexte. Tier gratuit suffit pour démarrer.
Métriques Prometheus
uv add django-prometheus
INSTALLED_APPS += ['django_prometheus']
MIDDLEWARE = [
'django_prometheus.middleware.PrometheusBeforeMiddleware',
# ...
'django_prometheus.middleware.PrometheusAfterMiddleware',
]
Endpoint /metrics exposé. Scrape via Prometheus + dashboard Grafana.
Health checks
# urls.py
def health_check(request):
return JsonResponse({'status': 'ok'})
def ready_check(request):
try:
from django.db import connection
connection.cursor().execute('SELECT 1')
return JsonResponse({'status': 'ready'})
except Exception as e:
return JsonResponse({'status': 'error', 'detail': str(e)}, status=503)
urlpatterns += [
path('health/', health_check),
path('ready/', ready_check),
]
Distinction live/ready essentielle pour orchestrateurs (Kubernetes, Docker Swarm).
11. Déploiement zéro downtime
Avec PaaS (Render, Railway, Fly.io)
Géré automatiquement. Push to main → build → health check du nouveau container → bascule du trafic → arrêt de l’ancien.
Avec Docker Compose
docker compose pull
docker compose up -d
Compose remplace les conteneurs un par un. Pour zero-downtime réel : duplication temporaire d’instance + reverse proxy qui bascule.
Migrations DB en production
Toujours tester sur staging d’abord. Migrations bloquantes (cf Django ORM et migrations) doivent être décomposées :
- Déploiement A : ajout colonne nullable + code qui peut écrire dedans
- Backfill des données existantes via job Celery
- Déploiement B : code qui lit la nouvelle colonne
- Migration : ajout contrainte NOT NULL
- Déploiement C : drop ancienne colonne
Ce pattern évite tout downtime même sur grosses tables.
Workflow CI/CD type
Pipeline GitHub Actions (GitHub Actions tutoriel) :
- Tests :
pytest+python manage.py check --deploy - Build : Docker image taggée avec SHA
- Push image vers GHCR/Docker Hub
- Migration test sur instance staging
- Déploiement automatique sur staging
- Déploiement manuel approuvé sur production
12. FAQ
Render, Railway ou VPS auto-géré ?
Render/Railway pour démarrer simplement, sans compétence sysadmin. VPS auto-géré pour le contrôle total et l’optimisation budget. La bascule est possible plus tard avec Docker.
Combien de workers Gunicorn ?
Règle de départ : 2 × cpu + 1. Ajuster selon RAM disponible (chaque worker consomme ~100-300 Mo selon l’app) et observer en charge réelle. Pour I/O-bound : envisager gevent ou uvicorn workers pour plus de concurrence.
Async Django vaut-il vraiment la peine ?
Pour des apps majoritairement DB-bound : non, le sync suffit largement. Pour des apps avec beaucoup d’appels HTTP externes ou WebSockets : oui, async libère les workers pendant les attentes I/O.
Comment sécuriser le admin Django en production ?
URL non standard (/superadmin/ au lieu de /admin/), restreindre l’accès par IP via Nginx/middleware, exiger 2FA via django-otp, surveiller les tentatives de connexion via Sentry.
Static files via Whitenoise ou CDN ?
Whitenoise suffit pour la majorité des cas. CDN (Cloudflare, BunnyCDN) pertinent quand : trafic international important, médias volumineux, optimisation des Core Web Vitals au max.
Comment gérer les secrets en production ?
Variables d’environnement chargées au démarrage. Stockage : .env chmod 600, ou gestionnaire de secrets cloud (AWS Secrets Manager, GCP Secret Manager, Vault). Jamais en dur dans le code.
PgBouncer obligatoire ?
Pas pour démarrer. Avec moins de 100 connexions simultanées et CONN_MAX_AGE=300, Django seul tient. Au-delà, PgBouncer en transaction-pooling devient utile, surtout sur cloud DB managées avec limites de connexions.
Articles liés (cluster Django)
- 👉 Django pour PME : guide backend Python (pillar)
- 👉 Django REST Framework en pratique
- 👉 Django ORM et migrations
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.