ITSkillsCenter
Blog

Django déploiement production : guide complet 2026

13 دقائق للقراءة

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

  1. Choix d’hébergement Django en 2026
  2. Gunicorn : serveur WSGI standard
  3. Uvicorn pour ASGI/async
  4. Whitenoise pour les static files
  5. Reverse proxy : Caddy ou Nginx
  6. Docker et conteneurisation
  7. Settings production sécurisés
  8. Base de données et Redis
  9. Celery pour le travail asynchrone
  10. Monitoring et logs
  11. Déploiement zéro downtime
  12. 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), gevent pour I/O concurrent, uvicorn.workers.UvicornWorker pour 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 :

  1. Déploiement A : ajout colonne nullable + code qui peut écrire dedans
  2. Backfill des données existantes via job Celery
  3. Déploiement B : code qui lit la nouvelle colonne
  4. Migration : ajout contrainte NOT NULL
  5. 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) :

  1. Tests : pytest + python manage.py check --deploy
  2. Build : Docker image taggée avec SHA
  3. Push image vers GHCR/Docker Hub
  4. Migration test sur instance staging
  5. Déploiement automatique sur staging
  6. 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)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité