← Blogue

Déployer Django sur DigitalOcean

Railyard

1 mai 2026 · 9 min de lecture

Suivez ça sur votre propre serveur. Commencer gratuitement, sans carte

Vous avez une application Django qui fonctionne sur votre ordinateur, et un compte DigitalOcean. Ce qui manque, c’est tout l’entre-deux : un serveur qui reste debout, une base de données Postgres au lieu de SQLite, un gestionnaire de processus pour que gunicorn redémarre après un plantage, des fichiers statiques servis correctement, et un certificat pour que le site réponde sur https:// plutôt que sur une simple adresse IP. Rien de tout ça n’est difficile en soi, mais il est facile d’avoir une pièce presque correcte et de passer une soirée sur ALLOWED_HOSTS.

Ce guide construit l’ensemble avec Docker Compose : un conteneur pour Postgres, un pour votre application Django derrière gunicorn, et un pour Caddy qui gère le HTTPS et les fichiers statiques. À la fin, vous aurez un droplet qui sert votre application sur votre propre domaine, avec une façon reproductible de déployer de nouvelles versions et de sauvegarder la base de données. Ensuite on regarde le raccourci : pousser le même dépôt vers Railyard et le laisser lire requirements.txt à votre place.

Ce qu’il vous faut

  • Un droplet DigitalOcean. Le forfait Basic à 1 vCPU / 2 Go de RAM fait tourner Postgres, gunicorn et Caddy ensemble sans problème, et coûte actuellement 12 $US/mois ; le forfait 1 Go (6 $US/mois) convient pour de très petites applications mais laisse peu de marge une fois Postgres ajouté.
  • Un nom de domaine, avec accès pour ajouter un enregistrement A.
  • Un dépôt GitHub (ou tout autre hébergeur Git) contenant votre projet Django.
  • Un compte DigitalOcean avec la facturation configurée, et une clé SSH ajoutée à votre compte.

Étape 1 : créer le droplet

Depuis le tableau de bord DigitalOcean, créez un nouveau droplet : choisissez l’image Ubuntu 24.04 LTS, le forfait Basic 2 Go, une région proche de vos utilisateurs, et votre clé SSH. Une fois qu’il tourne, notez son adresse IP publique et pointez l’enregistrement A de votre domaine vers celle-ci :

myproject.example.com.  A  203.0.113.10

La propagation DNS peut prendre de quelques minutes à quelques heures. Vous pouvez continuer à configurer le serveur en attendant ; Caddy a seulement besoin que l’enregistrement DNS soit correct au moment où il tente d’émettre un certificat.

Connectez-vous en SSH en tant que root et installez Docker :

ssh root@203.0.113.10
curl -fsSL https://get.docker.com | sh
apt-get install -y docker-compose-plugin

Vérifiez que ça a fonctionné :

docker compose version

Vous devriez voir une chaîne de version pour le plugin Compose, et non une erreur « command not found ».

Étape 2 : préparer l’application Django pour la production

Les valeurs par défaut de Django supposent un développeur qui lance manage.py runserver en local, pas un serveur. Quelques réglages doivent changer pour la production. Dans settings.py :

import os

DEBUG = False
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["POSTGRES_DB"],
        "USER": os.environ["POSTGRES_USER"],
        "PASSWORD": os.environ["POSTGRES_PASSWORD"],
        "HOST": "db",
        "PORT": "5432",
    }
}

STATIC_URL = "/static/"
STATIC_ROOT = "/app/staticfiles"

Fixez les versions des paquets dont vous avez besoin dans requirements.txt. Ce guide utilise Django 6.1, gunicorn 26.2 comme serveur WSGI, et psycopg2-binary 2.9.13 pour parler à Postgres :

django==6.1.2
gunicorn==26.2.0
psycopg2-binary==2.9.13

Si vous utilisez déjà django-environ ou dj-database-url pour lire DATABASE_URL comme une seule chaîne, ça fonctionne aussi ; les variables d’environnement ci-dessus sont juste la version la plus simple.

Étape 3 : écrire le Dockerfile

FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
RUN python manage.py collectstatic --noinput

CMD ["sh", "-c", "python manage.py migrate --no-input && gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 3"]

Remplacez myproject.wsgi par le vrai nom de votre paquet de projet. Lancer migrate avant que gunicorn démarre fait que chaque déploiement applique automatiquement les migrations en attente ; pour une configuration à un seul serveur comme celle-ci, c’est plus simple qu’une étape de release séparée. Trois workers gunicorn est un bon point de départ pour un droplet de 2 Go ; surveillez la mémoire sous charge et ajustez.

Étape 4 : écrire docker-compose.yml

services:
  db:
    image: postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_DB: myproject
      POSTGRES_USER: myproject
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myproject"]
      interval: 5s
      timeout: 5s
      retries: 5

  web:
    build: .
    restart: unless-stopped
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - static_files:/app/staticfiles
    expose:
      - "8000"

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - static_files:/srv/static
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - web

volumes:
  postgres_data:
  static_files:
  caddy_data:
  caddy_config:

Seul Caddy publie des ports vers l’extérieur ; web et db restent accessibles uniquement depuis les conteneurs du même réseau Compose, ce qui fait une chose de moins exposée à Internet par accident.

Étape 5 : écrire le Caddyfile

myproject.example.com {
	encode gzip

	handle_path /static/* {
		root * /srv/static
		file_server
	}

	reverse_proxy web:8000
}

Caddy demande et renouvelle automatiquement un certificat Let’s Encrypt pour myproject.example.com dès son premier démarrage, du moment que le DNS du domaine pointe déjà vers ce serveur et que les ports 80 et 443 l’atteignent. Les fichiers statiques sous /static/ sont servis directement par Caddy depuis le volume partagé ; tout le reste est redirigé vers gunicorn.

Étape 6 : configurer vos variables d’environnement et secrets

Créez un fichier .env à côté de docker-compose.yml, et gardez-le hors du contrôle de version :

POSTGRES_DB=myproject
POSTGRES_USER=myproject
POSTGRES_PASSWORD=un-mot-de-passe-long-et-aleatoire
DJANGO_ALLOWED_HOSTS=myproject.example.com
SECRET_KEY=une-autre-valeur-longue-et-aleatoire

Générez ces deux valeurs aléatoires avec openssl rand -base64 32 plutôt que de taper quelque chose de mémorable ; cette base de données et cette instance Django n’ont besoin que d’être imprévisibles, pas mémorisables.

Étape 7 : ouvrir le pare-feu et déployer

ufw allow 22
ufw allow 80
ufw allow 443
ufw enable

Puis, depuis le répertoire contenant votre docker-compose.yml :

docker compose up -d --build
docker compose logs -f caddy

Vous devriez voir Caddy signaler qu’il a obtenu un certificat pour votre domaine, puis docker compose logs -f web devrait montrer gunicorn démarrer et les migrations s’appliquer. Visitez https://myproject.example.com dans un navigateur : vous devriez voir votre application en HTTPS avec un certificat valide, pas un avertissement du navigateur.

Sauvegardes et mises à jour

Sauvegardez Postgres avec un dump planifié, écrit en dehors du conteneur pour qu’un volume perdu n’emporte pas la sauvegarde avec lui :

docker compose exec -T db pg_dump -U myproject myproject > backup-$(date +%F).sql

Mettez ça dans une tâche cron et copiez le résultat ailleurs que sur le droplet (DigitalOcean Spaces, S3, ou simplement scp vers une autre machine). Pour mettre à jour l’application, récupérez votre dernier code, reconstruisez, et relancez les conteneurs ; l’étape migrate dans le CMD du Dockerfile s’exécute de nouveau automatiquement :

git pull
docker compose up -d --build web

Le raccourci : avec Railyard

Tout ce qui précède (le Dockerfile, gunicorn, l’étape de migration, DATABASE_URL, le proxy Caddy et le certificat) est ce que Railyard configure automatiquement quand vous le pointez vers le même dépôt. D’après ses propres règles de détection : Django est reconnu à partir de requirements.txt, pyproject.toml, ou d’un manage.py qui importe Django ; l’étape de release exécute python manage.py migrate --no-input avant que la nouvelle version ne reçoive du trafic ; le processus web tourne sur gunicorn, lié au port que Railyard attribue ; et une dépendance psycopg/psycopg2 provisionne automatiquement une base Postgres, avec DATABASE_URL injecté comme variable d’environnement, sans Dockerfile ni fichier Compose requis. collectstatic s’exécute aussi automatiquement pendant le build, donc WhiteNoise ou une configuration STATIC_ROOT standard fonctionne sans que vous écriviez cette étape vous-même. Voir la documentation Django pour les règles exactes, et Bases de données et sauvegardes pour savoir comment l’instance Postgres gérée est sauvegardée et restaurée.

Pour déployer de cette façon : créez un droplet ou un serveur depuis Railyard (DigitalOcean est l’un des fournisseurs pris en charge), puis collez l’URL Git de votre dépôt, ou ajoutez ceci à votre README pour un déploiement en un clic :

[![Deploy to Railyard](https://app.railyard.run/deploy/badge)](https://app.railyard.run/deploy?repo=https://github.com/you/your-django-app)

Le chemin manuel ci-dessus reste celui qu’il faut connaître. C’est ce que Railyard fait sous le capot, et le comprendre veut dire que vous ne serez jamais bloqué si un changement n’est pas encore exposé dans le tableau de bord.

Dépannage

Caddy n’obtient jamais de certificat. Vérifiez que l’enregistrement A du domaine pointe bien vers le droplet (dig myproject.example.com) et que les ports 80 et 443 sont ouverts à la fois dans le pare-feu du droplet et dans le pare-feu cloud de DigitalOcean si vous en avez ajouté un. Let’s Encrypt valide d’abord en HTTP simple, donc le port 80 doit être accessible même si le site final n’est qu’en HTTPS.

« Bad Request (400) » de Django. C’est presque toujours ALLOWED_HOSTS qui ne correspond pas au domaine visité. Vérifiez que DJANGO_ALLOWED_HOSTS dans .env correspond exactement, redéployez avec docker compose up -d web, et consultez docker compose logs web.

Les fichiers statiques renvoient 404. collectstatic doit s’exécuter avant que Caddy puisse servir quoi que ce soit depuis /static/, et le volume static_files doit être monté à la fois dans web et caddy aux chemins attendus par chacun. Si vous avez changé STATIC_ROOT, mettez à jour le Dockerfile et les chemins de volume dans docker-compose.yml.

Le conteneur Postgres ne démarre pas, ou les données semblent effacées après un redémarrage. Vérifiez docker compose logs db pour des erreurs de permission sur /var/lib/postgresql/data ; ça veut généralement dire que le volume nommé a été supprimé puis recréé (par exemple par docker compose down -v, qui supprime les volumes). Ne lancez jamais cette option à moins de vouloir vraiment effacer les données.

Le port 80 ou 443 est déjà utilisé. Un autre serveur web (souvent un nginx installé par une image DigitalOcean en un clic) y est déjà lié. Arrêtez-le et désactivez-le : systemctl stop nginx && systemctl disable nginx.

Les workers gunicorn se font tuer, ou le droplet ralentit sous charge. Sur un droplet de 1 Go, Postgres, trois workers gunicorn et Caddy peuvent se disputer la mémoire. Passez à deux workers, ajoutez un fichier swap comme filet de sécurité, ou passez à 2 Go si l’application a un vrai trafic.

FAQ

Ai-je besoin de Docker, ou puis-je faire ça avec systemd et un virtualenv ? Docker Compose garde Postgres, l’application et Caddy isolés et faciles à déplacer entre serveurs, mais un virtualenv avec gunicorn lancé par systemd et un Postgres installé au niveau du système fonctionnent de la même façon ; la commande gunicorn et la configuration Caddy ci-dessus ne changent pas dans un cas comme dans l’autre.

Puis-je utiliser SQLite au lieu de Postgres ? Pour un seul droplet avec peu d’écritures, oui, même si vous perdez la possibilité de faire tourner plus d’un conteneur web plus tard, et les sauvegardes deviennent la copie d’un seul fichier plutôt qu’un dump. Postgres ne coûte rien de plus ici puisque c’est simplement un autre conteneur.

Et Celery ou les tâches en arrière-plan ? Ajoutez un service worker à docker-compose.yml qui lance celery -A myproject worker --loglevel=info sur la même image et le même fichier .env ; il a besoin de Redis ou RabbitMQ comme broker, un autre conteneur, de la même façon que Postgres.

Ai-je besoin d’un droplet plus gros si j’ajoute d’autres applications plus tard ? Seulement une fois que vous manquez réellement de mémoire ou de CPU. Un seul droplet peut faire tourner plusieurs applications derrière la même instance Caddy, sur des sous-domaines différents, chacune avec son propre certificat, exactement comme pour une seule.

Une application Django sur un droplet n’a rien de fragile une fois les pièces correctement assemblées, et aucune d’entre elles n’est spécifique à Django : le même conteneur Postgres, le même proxy Caddy et la même tâche cron de sauvegarde fonctionnent aussi pour la prochaine application que vous mettrez sur ce serveur.

Pourquoi les gens choisissent Railyard pour ça

  • Votre propre compte infonuagique. Déployez sur DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou n'importe quel serveur Linux par SSH, facturé directement par votre fournisseur, sans majoration.
  • Un prix fixe par équipe. L'offre gratuite est gratuite pour toujours : un serveur plus un serveur de base de données, trois applications, sans carte. Pro coûte 19 $ par mois pour jusqu'à cinq serveurs et un nombre illimité d'applications. Team coûte 49 $ par mois avec des serveurs illimités. Aucuns frais par application, par déploiement, par siège ou par gigaoctet.
  • Des gabarits en un clic. Déployez des applications open source, comme des bases de données, des outils d'analyse et des outils de développement, sur votre propre serveur en un clic.
  • Des bases de données gérées. Postgres, MySQL et Redis, avec des sauvegardes planifiées, vérifiées par restauration, et la récupération à un point dans le temps.
  • Déploiement par push. Déployez depuis GitHub, GitLab ou la CLI, avec HTTPS automatique et des mises en production sans interruption.

Évitez la configuration du droplet

Collez votre dépôt Django dans Railyard : gunicorn, les migrations et Postgres sont configurés pour vous.

Offre gratuite, sans carte, et 14 jours de Pro pour chaque nouvelle équipe. Prix fixe par équipe.