← Blogue

Déployer Django avec Celery sur Hetzner

Railyard

12 mai 2026 · 8 min de lecture

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

Une application Django avec des tâches de fond, ce n’est pas un seul processus, c’est au moins trois : gunicorn qui sert les requêtes, un worker Celery qui vide la file, et Redis qui garde cette file entre les deux. La plupart des tutoriels s’arrêtent à runserver et laissent le worker, le courtier de messages et le certificat HTTPS comme exercice pour plus tard. Ce guide construit le tout sur un serveur Hetzner : gunicorn derrière Caddy, un worker Celery et un planificateur beat optionnel, Postgres et Redis, le tout comme services Docker Compose avec de vraies sauvegardes et une vraie procédure de mise à jour. Le raccourci à la fin pousse la même application avec un simple git push.

Ce guide utilise Postgres 16 et Redis 8.

Ce qu’il vous faut

  • Un serveur Hetzner Cloud. Un CPX31 (4 vCPU, 8 Go) fait tourner gunicorn, un worker Celery, Postgres et Redis ensemble sans échanger sur disque, pour 62,99 € par mois hors TVA. Un CX23 plus petit (2 vCPU, 4 Go) à 5,99 € par mois hors TVA fonctionne pour une application à faible trafic avec une file légère, c’est juste plus serré une fois que les tampons partagés de Postgres et les processus worker de Celery sont tous les deux en mémoire.
  • Un nom de domaine, pointé vers l’IP du serveur avant l’étape du HTTPS.
  • Un compte Hetzner Cloud et un jeton API (Security → API Tokens, Read & Write) si vous voulez scripter la création du serveur; la console Hetzner Cloud fonctionne tout aussi bien si vous préférez cliquer une fois.

Étape 1 : créer le serveur

Depuis la console Hetzner Cloud, créez un serveur sur Ubuntu 24.04, la taille choisie, et une région proche de vos utilisateurs. Si vous préférez le scripter :

curl -X POST "https://api.hetzner.cloud/v1/servers" \
  -H "Authorization: Bearer $HETZNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "django-app",
    "server_type": "cpx31",
    "image": "ubuntu-24.04",
    "location": "nbg1",
    "ssh_keys": ["your-ssh-key-name"]
  }'

Vous devriez recevoir une réponse JSON avec un server.id et une server.public_net.ipv4.ip. Connectez-vous en SSH avec cette IP une fois le démarrage terminé, généralement en moins d’une minute :

ssh root@YOUR_SERVER_IP

Étape 2 : installer Docker et pointer le DNS

curl -fsSL https://get.docker.com | sh
docker compose version

Vous devriez voir une chaîne de version Compose v2.x. Pendant que ça s’installe, créez l’enregistrement DNS A pour votre domaine et confirmez qu’il se résout :

dig +short app.example.com

Il doit retourner l’IP du serveur avant que Caddy puisse émettre un certificat plus tard, alors faites cette étape tôt et laissez-la se propager en arrière-plan.

Étape 3 : écrire l’image Django

Django ne fournit pas d’image officielle comme Postgres ou Redis, puisque les dépendances de chaque projet diffèrent, alors vous construisez la vôtre :

FROM python:3.12-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq-dev gcc \
    && rm -rf /var/lib/apt/lists/*

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

COPY . .

RUN python manage.py collectstatic --noinput

CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]

libpq-dev et gcc sont là pour psycopg2 (la version compilée, non binaire); retirez-les si votre requirements.txt épingle plutôt psycopg2-binary, qui embarque son propre libpq compilé. Exécuter collectstatic au moment du build fait en sorte qu’un fichier statique cassé n’atteint jamais le trafic de production, puisque le build échoue avant même que l’image soit poussée.

Étape 4 : écrire le fichier Docker Compose

Cinq services : l’application Django comme web, la même image encore une fois comme worker et beat avec des commandes différentes, Postgres, Redis, et Caddy devant.

services:
  web:
    build: .
    restart: unless-stopped
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 3
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app_net

  worker:
    build: .
    restart: unless-stopped
    command: celery -A myproject worker --loglevel=info --concurrency=2
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app_net

  beat:
    build: .
    restart: unless-stopped
    command: celery -A myproject beat --loglevel=info
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app_net

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: myproject
      POSTGRES_USER: myproject
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myproject"]
      interval: 5s
      start_period: 10s
      retries: 20
    networks:
      - app_net

  redis:
    image: redis:8-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data
    networks:
      - app_net

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

networks:
  app_net:

volumes:
  postgres_data:
  redis_data:
  caddy_data:
  caddy_config:

Retirez carrément le service beat si vous ne planifiez pas de tâches périodiques. worker et beat se construisent à partir du même Dockerfile exact que web, c’est le principe du modèle de processus 12-factor : un seul code source, trois rôles, lancés avec trois commandes différentes plutôt que trois dépôts à garder synchronisés.

Écrivez le .env à côté :

POSTGRES_PASSWORD=une-longue-chaine-aleatoire
DJANGO_SETTINGS_MODULE=myproject.settings
SECRET_KEY=une-autre-longue-chaine-aleatoire
DEBUG=False
ALLOWED_HOSTS=app.example.com
DATABASE_URL=postgres://myproject:une-longue-chaine-aleatoire@db:5432/myproject
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0

SECRET_KEY et le mot de passe Postgres devraient chacun venir de openssl rand -hex 32, pas de quelque chose tapé à la main.

Étape 5 : exécuter les migrations et démarrer la pile

docker compose up -d --build
docker compose run --rm web python manage.py migrate

Vous devriez voir la sortie des migrations de Django se terminer par une liste de migrations appliquées, pas une erreur sur une table manquante ou une connexion refusée vers db. Vérifiez ensuite que le worker a bien pris le courtier de messages :

docker compose logs worker --tail=20

Vous devriez voir une ligne comme celery@worker ready. confirmant qu’il s’est connecté à Redis et attend des tâches.

Étape 6 : écrire le Caddyfile

app.example.com {
	reverse_proxy web:8000

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

Caddy demande et renouvelle son propre certificat via Let’s Encrypt dès la première fois qu’il peut répondre sur le port 80 pour un domaine qui se résout déjà vers ce serveur, sans étape certbot séparée. Si vous servez les fichiers statiques directement depuis Caddy plutôt que via WhiteNoise, montez le même répertoire statique que le conteneur web écrit dans le service caddy à /srv/static.

Étape 7 : sauvegardes

Postgres contient les données qui comptent vraiment; sauvegardez-les avec pg_dump sur une tâche cron plutôt que de compter seulement sur le volume :

#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p backups
docker compose exec -T db pg_dump -U myproject myproject | gzip > "backups/django-db-$STAMP.sql.gz"
find backups -name '*.sql.gz' -mtime +14 -delete

Copiez backups/ hors du serveur selon le même horaire, vers du stockage objet ou une autre machine. Un dump qui ne quitte jamais le serveur où il a été pris ne protège pas contre une panne de ce serveur lui-même.

Étape 8 : mises à jour

Construisez votre propre nouvelle image, appliquez les migrations, puis recréez les conteneurs de l’application un à la fois :

docker compose build
docker compose run --rm web python manage.py migrate
docker compose up -d web worker beat

Exécuter la migration comme un run --rm à part avant de toucher aux conteneurs de longue durée fait en sorte qu’une migration échouée arrête le déploiement plutôt que de redémarrer partiellement web contre un schéma qu’il n’attend pas.

Le raccourci : faites-le avec Railyard

L’inspecteur de dépôt de Railyard lit requirements.txt et manage.py pour détecter Django lui-même, sans Dockerfile ni Procfile requis. Il règle la commande de release sur python manage.py migrate --no-input, démarre web avec gunicorn lié au port assigné, et exécute collectstatic automatiquement à l’étape de build. Une dépendance psycopg/psycopg2 provisionne Postgres et injecte DATABASE_URL; un client Redis (redis, django-redis, rq) fait la même chose pour REDIS_URL. Si Celery est dans vos dépendances, il est proposé comme processus worker de lui-même, et Celery Beat est ajouté aussi si django-celery-beat est présent ou que vos réglages définissent un planning beat, donc il n’y a pas de Procfile à écrire à la main pour l’un ou l’autre.

railyard create my-app https://github.com/me/my-app --addons postgres,redis

ou collez la même URL Git dans le tableau de bord une fois GitHub connecté. Mettez le worker à l’échelle indépendamment une fois qu’il tourne :

railyard scale my-app worker=2

Hetzner est l’un des fournisseurs sur lesquels Railyard peut créer un serveur directement depuis votre propre compte, facturé au prix de Hetzner sans majoration; voyez Ajouter un serveur. Référence complète du framework sur Django, et Déployer depuis un dépôt pour tout ce qui d’autre est lu depuis le dépôt. Voyez la page pratique sur Django sur Hetzner pour les détails de taille et de région, et Bases de données et sauvegardes pour savoir comment les sauvegardes Postgres programmées et la restauration à un instant précis fonctionnent une fois attachées.

Dépannage

Le port 80 ou 443 est déjà utilisé. Une installation de serveur web par défaut les accapare souvent en premier. sudo ss -tlnp | grep -E ':80|:443' vous montre le processus; arrêtez-le ou reconfigurez-le avant que Caddy ne démarre.

ALLOWED_HOSTS rejette chaque requête. Django retourne un 400 avec un message clair nommant l’hôte attendu, dans les logs que vous pouvez lire avec docker compose logs web. Ajoutez le domaine exact (et la variante www. si vous servez les deux) à ALLOWED_HOSTS dans .env, puis redémarrez le service web.

Le worker Celery démarre mais rien ne s’exécute jamais. C’est presque toujours CELERY_BROKER_URL qui pointe quelque part que le worker ne peut pas atteindre, souvent localhost resté du développement local plutôt que le nom du service Compose redis. Confirmez avec docker compose exec worker env | grep CELERY.

Le certificat n’est jamais émis. Vérifiez d’abord que dig +short app.example.com se résout vers le serveur; Caddy ne peut pas compléter un défi HTTP-01 pour un domaine qui ne pointe pas encore ici. Vérifiez aussi que votre pare-feu cloud autorise les ports 80 et 443 entrants, pas seulement ceux que vous testez activement.

Manque de mémoire sur un petit serveur. Postgres, Redis, gunicorn et un worker Celery s’additionnent vite sur une machine à 4 Go si le nombre de workers de gunicorn reste trop élevé. --workers 3 ici suppose une machine à 4 vCPU; divisez par deux sur un CX23, ou déplacez Postgres sur son propre serveur une fois que le trafic le justifie vraiment.

Foire aux questions

Ai-je besoin d’un worker et d’un processus beat? Seulement beat, si vous planifiez des tâches périodiques avec @shared_task et un CELERYBEAT_SCHEDULE ou django-celery-beat. Une application qui ne fait que déclencher des tâches à la demande (depuis une vue, disons) n’a besoin que du worker.

Puis-je faire tourner Celery et Django dans le même conteneur? Vous pouvez démarrer les deux processus dans un conteneur avec un superviseur de processus, mais un plantage de l’un fait alors tomber l’autre, et vous perdez la capacité de mettre le worker à l’échelle indépendamment du trafic web. Des conteneurs séparés à partir de la même image, comme ci-dessus, évitent les deux problèmes.

Pourquoi Caddy plutôt que nginx? Caddy demande et renouvelle les certificats Let’s Encrypt lui-même à partir d’une config de six lignes; nginx a besoin de certbot câblé séparément et d’une tâche cron pour le renouvellement. Les deux fonctionnent, Caddy demande juste moins d’entretien.

Et si mon application a besoin d’une version précise de Python? Changez la ligne FROM python:3.12-slim en conséquence; Django et Celery supportent tous les deux les versions de Python actuellement maintenues, vérifiez les contraintes de vos propres dépendances si l’une d’elles épingle une version plus ancienne.

Les deux chemins finissent avec les trois mêmes processus qui parlent au même Postgres et au même Redis, le fichier Compose si vous voulez voir chaque ligne, le push si vous préférez retourner au vrai code Django.

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.

Poussez Django directement depuis votre dépôt

Railyard lit requirements.txt, trouve Celery, et câble gunicorn, Postgres et le processus worker pour vous.

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