← Blogue

Déployer FastAPI sur Hetzner

Railyard

6 juillet 2026 · 7 min de lecture

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

uvicorn main:app --reload est la façon dont une application FastAPI tourne sur un ordinateur portable ; ce dont elle a besoin en production, c’est d’un vrai gestionnaire de processus, d’une base de données qui survit à un redémarrage, de migrations qui s’exécutent avant que le nouveau code soit en ligne, et du HTTPS devant. Rien de tout ça n’est spécifique à FastAPI exactement, mais les détails diffèrent assez d’un déploiement Flask ou Django, un serveur ASGI plutôt que WSGI, Alembic plutôt que les migrations intégrées de Django, pour qu’un Dockerfile Python générique copié-collé laisse habituellement quelque chose à moitié configuré. Ce guide construit la vraie chose dans Docker sur un serveur Hetzner Cloud, avec Caddy gérant les certificats, et termine avec une application FastAPI qui tourne sur votre propre domaine, que vous saurez mettre à jour et faire avancer.

Ce qu’il vous faut

  • Un serveur Hetzner Cloud. Un CX23 (2 vCPU, 4 Go de mémoire) à 5,99 € par mois avant taxes fait tourner confortablement une seule application FastAPI avec sa base de données Postgres à côté ; passez à un CX33 (4 vCPU, 8 Go, 8,99 € par mois) si l’application fait aussi du vrai travail de fond.
  • Un domaine, avec le DNS déjà pointé vers le serveur avant de demander le HTTPS.
  • Un compte Hetzner Cloud et un jeton API (Security → API Tokens, Read & Write) si vous voulez provisionner le serveur via l’API plutôt que la console.
  • Une application FastAPI dans un dépôt Git. Ce guide utilise FastAPI 0.161.1, qui requiert Python 3.10 ou plus récent ; le Dockerfile ci-dessous fixe Python 3.13.
  • Docker et le plugin Compose, soit localement pour tester le build d’abord, soit directement sur le serveur.

Étape 1 : créer le serveur

Dans la Hetzner Cloud Console, créez un projet si nécessaire, puis Add Server : choisissez Ubuntu 24.04, le type de vCPU partagée CX23, un emplacement (Nuremberg, Falkenstein et Helsinki restent tous dans l’UE), ajoutez votre clé SSH, et créez-le.

ssh root@your-server-ip

Vous devriez arriver à un shell root sans invite de mot de passe.

Étape 2 : installer Docker

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

Étape 3 : écrire le Dockerfile

FROM python:3.13-slim

WORKDIR /app

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

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

COPY . .

EXPOSE 8000
CMD ["fastapi", "run", "app/main.py", "--port", "8000"]

fastapi run est la commande de production que la documentation officielle de FastAPI recommande ; elle fait tourner Uvicorn en dessous sans commande séparée à retenir. libpq-dev et build-essential sont là pour psycopg, qui a besoin de compiler contre la bibliothèque client de Postgres ; retirez-les si votre requirements.txt utilise plutôt psycopg[binary] ou asyncpg, puisque les deux fournissent des wheels précompilées.

Un requirements.txt minimal pour ce guide :

fastapi[standard]>=0.161
psycopg[binary]>=3.2
sqlalchemy>=2.0
alembic>=1.13

Construisez-le une fois pour confirmer qu’il compile proprement :

docker build -t my-fastapi-app .

Étape 4 : écrire le fichier Compose

services:
  app:
    build: .
    restart: unless-stopped
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    expose:
      - "8000"

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

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - app

volumes:
  db-data:
  caddy-data:
  caddy-config:

Votre fichier .env sur le serveur :

DB_NAME=app
DB_USER=app
DB_PASSWORD=change-this
DATABASE_URL=postgresql+psycopg://app:change-this@db:5432/app

Validez avant de démarrer :

docker compose config

Étape 5 : écrire le Caddyfile

api.example.com {
    reverse_proxy app:8000
}

Étape 6 : premier démarrage et migrations

docker compose up -d
docker compose exec app alembic upgrade head
docker compose ps

Vous devriez voir app, db et caddy tous running, et https://api.example.com devrait répondre avec un certificat valide. La documentation interactive de FastAPI vit sur /docs par défaut ; la visiter devrait montrer les vrais points de terminaison de votre API.

Étape 7 : déployer de nouvelles versions

git pull
docker compose build app
docker compose up -d --no-deps app
docker compose exec app alembic upgrade head

Reconstruire seulement app laisse db et caddy intacts, donc le certificat et la base de données restent tous deux en marche pendant le déploiement. Exécuter la migration après que le nouveau conteneur est en marche, plutôt que pendant le build, fait qu’un échec de migration apparaît comme une erreur de commande claire plutôt qu’une image à moitié déployée.

Sauvegardes

Postgres dans le conteneur db est le seul élément d’état dans cette pile qui n’est pas reproductible depuis Git. Planifiez un dump quotidien hors du serveur :

docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz

Copiez ce fichier vers du stockage objet ou une autre machine via une tâche cron, et restaurez-le périodiquement dans une base de données jetable pour confirmer qu’il est réellement utilisable avant d’en avoir vraiment besoin.

Le raccourci : faites-le avec Railyard

Déployer FastAPI sur Hetzner exécute la même recette à partir d’une URL de dépôt, sans écrire le Dockerfile, le fichier Compose ou le Caddyfile vous-même. Connectez un serveur avec un jeton de la Hetzner Cloud Console, ou amenez-en un que vous avez déjà provisionné. Railyard détecte FastAPI à partir de requirements.txt ou pyproject.toml, sert main:app sur $PORT avec uvicorn (pointé automatiquement vers votre propre module s’il se trouve ailleurs), et reconnaît psycopg, asyncpg ou SQLAlchemy comme dépendance pour provisionner Postgres avec DATABASE_URL injectée. Définissez alembic upgrade head comme commande de release sous Settings → Deploy pour migrer à chaque mise en production, comme à l’étape 6 ci-dessus mais exécuté automatiquement avant que chaque nouvelle version reçoive du trafic. N’importe quel processus d’un Procfile devient un worker que vous pouvez mettre à l’échelle indépendamment du processus web. Voyez la documentation déployer depuis un dépôt pour les règles exactes de détection.

Dépannage

  • connection refused au premier démarrage : app a démarré avant que Postgres ait fini de s’initialiser. depends_on avec une condition de healthcheck (comme à l’étape 4) devrait empêcher ça, mais si vous retirez le healthcheck, ajoutez plutôt une courte boucle de réessai autour de la première commande de migration au lieu d’assumer que la base est prête dès que son conteneur démarre.
  • Erreurs 500 sans détail dans la réponse : FastAPI n’expose pas les traces d’erreur dans les réponses par défaut en dehors d’outils de débogage ; vérifiez docker compose logs app pour la vraie exception.
  • ModuleNotFoundError pour psycopg à l’exécution malgré un build propre : habituellement une incohérence entre psycopg (la variante pure Python) et psycopg[binary] ou psycopg2-binary à travers des modifications de requirements.txt ; fixez-en un explicitement plutôt que de laisser un lockfile périmé en résoudre un autre que prévu.
  • Le HTTPS ne s’émet jamais, Caddy continue de réessayer : le DNS ne pointe pas encore vers le serveur. dig +short api.example.com devrait correspondre à l’IP du serveur.
  • Alembic dit « Target database is not up to date » au déploiement : la migration d’un déploiement précédent n’a pas tourné, souvent parce que la commande de migration de l’étape 7 a été manuellement sautée une fois. Exécutez docker compose exec app alembic current pour voir où la base de données en est réellement par rapport à alembic heads pour où le code l’attend.

FAQ

Pourquoi fastapi run plutôt qu’appeler uvicorn directement ? Ils font la même chose en dessous ; fastapi run est la commande que la documentation de FastAPI recommande pour la production dans les versions couvertes par ce guide, puisqu’elle choisit des valeurs par défaut sensées sans drapeaux supplémentaires.

Devrais-je utiliser plusieurs workers Uvicorn dans un seul conteneur ? La documentation de déploiement de FastAPI le déconseille pour tout ce qui tourne sous Docker Compose, Kubernetes, ou similaire : laissez plutôt l’orchestrateur faire tourner plusieurs répliques de conteneur qu’un seul conteneur jonglant avec plusieurs processus worker, puisque les répliques peuvent être redémarrées, mises à l’échelle et vérifiées indépendamment.

Postgres ou autre chose ? SQLAlchemy et la majeure partie de l’écosystème FastAPI sont agnostiques à la base de données ; ce guide utilise Postgres puisque c’est le pairage le plus commun, mais remplacer l’image db et le schéma de DATABASE_URL fonctionne de la même façon pour MySQL.

Un CX23 suffit-il vraiment ? Pour une seule API servant des volumes de requêtes typiques, oui ; 4 Go se partagent confortablement entre le conteneur de l’application et Postgres. Du travail de fond lourd en CPU (traitement de grandes données, inférence ML) est une raison de monter en taille.

Et si mon application utilise une app factory plutôt qu’une variable app au niveau du module ? Pointez le CMD du Dockerfile ou la commande détectée par Railyard explicitement vers elle, par exemple fastapi run app/main.py --port 8000 devient l’appel équivalent contre un nom de fonction factory si c’est ainsi que votre application est structurée ; l’exemple de ce guide suppose une instance app ordinaire.

Ça marche avec des points de terminaison async et sync mélangés dans une même application ? Oui ; FastAPI exécute automatiquement les opérations de chemin synchrones dans un pool de threads pour qu’elles ne bloquent pas la boucle d’événements, ce qui explique en partie pourquoi un seul processus Uvicorn gère un mélange des deux sans configuration supplémentaire de votre part.

Une application FastAPI sur un serveur Hetzner que vous avez provisionné vous-même coûte 5,99 € par mois, avec chaque dépendance, version de Python et étape de migration explicite dans trois fichiers que vous contrôlez. Expédier une nouvelle version une fois qu’ils existent, c’est reconstruire un conteneur et exécuter une commande de migration, rien de caché entre les deux.

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.

Sautez la configuration du serveur.

Poussez un dépôt FastAPI sur Railyard et il provisionne Postgres, exécute les migrations, et déploie à chaque push.

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