FastAPI auf Hetzner deployen
Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig
uvicorn main:app --reload ist, wie eine FastAPI-App auf einem Laptop läuft; für die Produktion braucht sie einen echten Prozessmanager, eine Datenbank, die einen Neustart überlebt, Migrationen, die laufen, bevor neuer Code live geht, und HTTPS davor. Nichts davon ist speziell für FastAPI, aber die Details unterscheiden sich genug von einem Flask- oder Django-Deploy, ein ASGI-Server statt WSGI, Alembic statt Djangos eingebauten Migrationen, dass das Kopieren eines generischen Python-Dockerfiles meist etwas halb konfiguriert lässt. Diese Anleitung baut das echte Ding in Docker auf einem Hetzner-Cloud-Server, mit Caddy, das Zertifikate übernimmt, und endet mit einer FastAPI-App, die auf deiner eigenen Domain läuft und von der du weißt, wie man sie aktualisiert und weiterentwickelt.
Was du brauchst
- Einen Hetzner-Cloud-Server. Ein CX23 (2 vCPU, 4 GB Speicher) für 5,99 €/Monat exkl. MwSt. betreibt eine einzelne FastAPI-App mit ihrer Postgres-Datenbank daneben bequem; steig auf einen CX33 (4 vCPU, 8 GB, 8,99 €/Monat) um, wenn die App auch echte Hintergrundarbeit erledigt.
- Eine Domain, deren DNS bereits auf den Server zeigt, bevor du HTTPS anforderst.
- Einen Hetzner-Cloud-Account und ein API-Token (Security → API Tokens, Lesen & Schreiben), falls du den Server über die API statt über die Konsole bereitstellen willst.
- Eine FastAPI-App in einem Git-Repository. Diese Anleitung verwendet FastAPI 0.161.1, das Python 3.10 oder neuer benötigt; das Dockerfile unten pinnt Python 3.13.
- Docker und das Compose-Plugin, entweder lokal, um den Build vorher zu testen, oder direkt auf dem Server.
Schritt 1: Den Server erstellen
Erstelle in der Hetzner-Cloud-Konsole bei Bedarf ein Projekt, dann Add Server: wähle Ubuntu 24.04, den CX23 Shared-vCPU-Typ, einen Standort (Nürnberg, Falkenstein und Helsinki bleiben alle in der EU), füge deinen SSH-Key hinzu und erstelle ihn.
ssh root@your-server-ip
Du solltest in einer Root-Shell landen, ohne nach einem Passwort gefragt zu werden.
Schritt 2: Docker installieren
curl -fsSL https://get.docker.com | sh
docker --version
docker compose version
Schritt 3: Das Dockerfile schreiben
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 ist der Produktionsbefehl, auf den FastAPIs eigene Docs verweisen; er führt Uvicorn darunter aus, ohne einen separaten Befehl merken zu müssen. libpq-dev und build-essential sind für psycopg da, das gegen Postgres’ Client-Bibliothek compilieren muss; lass sie weg, wenn deine requirements.txt stattdessen psycopg[binary] oder asyncpg verwendet, da beide vorgebaute Wheels liefern.
Eine minimale requirements.txt für diese Anleitung:
fastapi[standard]>=0.161
psycopg[binary]>=3.2
sqlalchemy>=2.0
alembic>=1.13
Baue es einmal, um zu bestätigen, dass es sauber compiliert:
docker build -t my-fastapi-app .
Schritt 4: Die Compose-Datei schreiben
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:
Deine .env-Datei auf dem Server:
DB_NAME=app
DB_USER=app
DB_PASSWORD=change-this
DATABASE_URL=postgresql+psycopg://app:change-this@db:5432/app
Vor dem Start validieren:
docker compose config
Schritt 5: Die Caddyfile schreiben
api.example.com {
reverse_proxy app:8000
}
Schritt 6: Erster Start und Migrationen
docker compose up -d
docker compose exec app alembic upgrade head
docker compose ps
Du solltest sehen, dass app, db und caddy alle running sind, und https://api.example.com sollte mit einem gültigen Zertifikat antworten. Die interaktiven Docs von FastAPI leben standardmäßig unter /docs; sie zu besuchen sollte die tatsächlichen Endpunkte deiner API zeigen.
Schritt 7: Neue Versionen deployen
git pull
docker compose build app
docker compose up -d --no-deps app
docker compose exec app alembic upgrade head
Nur app neu zu bauen lässt db und caddy unberührt, sodass Zertifikat und Datenbank während des Deploys beide weiterlaufen. Die Migration auszuführen, nachdem der neue Container läuft, statt als Teil des Builds, bedeutet, dass ein Migrationsfehler als klarer Befehlsfehler auftaucht statt als halb deploytes Image.
Backups
Postgres im db-Container ist das einzige Stück Zustand in diesem Stack, das sich nicht aus Git reproduzieren lässt. Plane einen täglichen Dump außerhalb des Servers:
docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz
Kopiere diese Datei per Cronjob in einen Object Storage oder auf eine andere Maschine, und stelle sie regelmäßig in eine Scratch-Datenbank wieder her, um zu bestätigen, dass sie tatsächlich brauchbar ist, bevor du sie wirklich brauchst.
Die Abkürzung: mit Railyard
FastAPI auf Hetzner deployen führt dasselbe Rezept von einer Repository-URL aus, ohne dass du das Dockerfile, die Compose-Datei oder die Caddyfile selbst schreiben musst. Verbinde einen Server mit einem Token aus der Hetzner-Cloud-Konsole, oder bring einen bereits bereitgestellten mit. Railyard erkennt FastAPI anhand von requirements.txt oder pyproject.toml, bedient main:app auf $PORT mit uvicorn (automatisch auf dein eigenes Modul ausgerichtet, falls die App woanders liegt) und erkennt psycopg, asyncpg oder SQLAlchemy als Abhängigkeit, um Postgres mit eingefügtem DATABASE_URL bereitzustellen. Setze alembic upgrade head als Release-Befehl unter Settings → Deploy, um bei jedem Release zu migrieren, genauso wie Schritt 6 oben, nur automatisch ausgeführt, bevor jede neue Version Traffic bekommt. Jeder Prozess in einem Procfile wird zu einem Worker, den du unabhängig vom Web-Prozess skalieren kannst. Siehe die Deploy-from-a-repository-Docs für die genauen Erkennungsregeln.
Troubleshooting
connection refusedbeim ersten Start:appstartete, bevor Postgres die Initialisierung abgeschlossen hat.depends_onmit einer Healthcheck-Condition (wie in Schritt 4) sollte das verhindern, aber falls du den Healthcheck entfernst, füge stattdessen eine kurze Retry-Schleife um den ersten Migrationsbefehl ein, statt anzunehmen, dass die Datenbank sofort nach Containerstart bereit ist.- 500-Fehler ohne Details in der Antwort: FastAPI zeigt standardmäßig keine Tracebacks in Antworten außerhalb von Debug-Tooling; prüfe
docker compose logs appfür die echte Exception. ModuleNotFoundErrorfürpsycopgzur Laufzeit trotz saubermen Build: meist eine Diskrepanz zwischenpsycopg(der „pure Python”-Variante) undpsycopg[binary]oderpsycopg2-binaryüber Änderungen anrequirements.txthinweg; pinne eine explizit, statt eine veraltete Lockfile eine andere auflösen zu lassen als beabsichtigt.- HTTPS kommt nie, Caddy versucht es immer wieder: DNS zeigt noch nicht auf den Server.
dig +short api.example.comsollte mit der IP des Servers übereinstimmen. - Alembic sagt beim Deploy „Target database is not up to date”: eine Migration eines vorherigen Deploys lief nicht, oft weil der Migrationsbefehl aus Schritt 7 einmal manuell übersprungen wurde. Führe
docker compose exec app alembic currentaus, um zu sehen, wo die Datenbank tatsächlich steht, gegenüberalembic headsdafür, wo der Code sie erwartet.
FAQ
Warum fastapi run statt uvicorn direkt aufzurufen? Beide machen unter der Haube dasselbe; fastapi run ist der Befehl, den FastAPIs eigene Docs für die Produktion empfehlen, da er sinnvolle Standardwerte ohne zusätzliche Flags wählt.
Sollte ich mehrere Uvicorn-Worker in einem Container verwenden? FastAPIs eigene Deployment-Docs raten davon ab, für alles, was unter Docker Compose, Kubernetes oder ähnlichem läuft: lass den Orchestrator mehrere Container-Replikate ausführen statt einen Container, der mehrere Worker-Prozesse jongliert, da Replikate unabhängig voneinander neu gestartet, skaliert und auf Gesundheit geprüft werden können.
Postgres oder etwas anderes? SQLAlchemy und der Großteil des FastAPI-Ökosystems sind datenbankunabhängig; diese Anleitung verwendet Postgres, da es die gängigste Kombination ist, aber das Tauschen des db-Images und des DATABASE_URL-Schemas funktioniert genauso für MySQL.
Reicht ein CX23 wirklich aus? Für eine einzelne API mit typischem Request-Volumen, ja; 4 GB teilen sich bequem zwischen App-Container und Postgres auf. CPU-intensive Hintergrundarbeit (große Datenverarbeitung, ML-Inferenz) ist ein Grund, hochzuskalieren.
Was, wenn meine App eine App-Factory statt einer Modulebenen-app-Variable verwendet? Richte den CMD des Dockerfiles oder Railyards erkannten Befehl explizit darauf aus, zum Beispiel wird fastapi run app/main.py --port 8000 zum äquivalenten Aufruf gegen einen Factory-Funktionsnamen, falls deine App so strukturiert ist; das Beispiel dieser Anleitung nimmt eine einfache app-Instanz an.
Funktioniert das mit async-only-Endpunkten und synchronen Endpunkten gemischt in einer App? Ja; FastAPI führt synchrone Pfad-Operationen automatisch in einem Thread-Pool aus, sodass sie den Event-Loop nicht blockieren, was Teil des Grundes ist, warum ein einzelner Uvicorn-Prozess eine Mischung aus beidem ohne zusätzliche Konfiguration deinerseits handhabt.
Eine FastAPI-App auf einem selbst bereitgestellten Hetzner-Server kostet 5,99 € im Monat, mit jeder Abhängigkeit, Python-Version und jedem Migrationsschritt explizit in drei Dateien, die du kontrollierst. Ein neues Release auszuliefern, nachdem sie existieren, ist ein Rebuild eines Containers und ein Migrationsbefehl, nichts, was dazwischen versteckt bliebe.
Warum Leute sich dafür Railyard entscheiden
- Dein eigenes Cloud-Konto. Deploye auf DigitalOcean, Hetzner, Vultr, AWS, OVHcloud oder jedem Linux-Server per SSH, direkt von deinem Anbieter abgerechnet, ohne Aufschlag.
- Ein fester Preis pro Team. Free ist für immer kostenlos: ein Server plus ein Datenbankserver, drei Apps, keine Karte. Pro kostet 19 $ im Monat für bis zu fünf Server und unbegrenzte Apps. Team kostet 49 $ im Monat mit unbegrenzten Servern. Keine Kosten pro App, pro Deployment, pro Sitz oder pro Gigabyte.
- Templates mit einem Klick. Deploye Open-Source-Apps, wie Datenbanken, Analytics und Dev-Tools, mit einem Klick auf deinem eigenen Server.
- Verwaltete Datenbanken. Postgres, MySQL und Redis mit geplanten, wiederherstellungsgeprüften Backups und Point-in-Time-Recovery.
- Push-to-Deploy. Deploye von GitHub, GitLab oder der CLI, mit automatischem HTTPS und Releases ohne Ausfallzeit.