Flask auf DigitalOcean deployen
Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig
flask run ist mit Absicht ein Entwicklungsserver, Flasks eigene Docs sagen das direkt, und er ist nicht dafür gedacht, echten Traffic zu bedienen. Eine Flask-App in Produktion zu bringen bedeutet, einen WSGI-Server auszuwählen, ihn auf das richtige Modul zu richten, eine Datenbank bereitzustellen und zu entscheiden, wie Migrationen laufen, Entscheidungen, die ein minimalistisches Framework wie Flask bewusst der App überlässt, statt eine einzige Konvention anzunehmen, wie es ein größeres Framework vielleicht täte. Diese Anleitung macht das alles in Docker auf einem DigitalOcean-Droplet, mit Caddy, das HTTPS übernimmt, und endet mit einer Flask-App auf deiner eigenen Domain, von der du weißt, wie man sie neu deployt.
Was du brauchst
- Ein DigitalOcean-Droplet. Der Basic-Plan mit 2 GB Speicher, 1 vCPU, 50 GB Disk (12 $/Monat) betreibt eine einzelne Flask-App mit Postgres daneben bequem; die 1-GB-Stufe (6 $/Monat) funktioniert für eine App mit wenig Traffic ohne viel Spielraum übrig.
- Eine Domain, deren DNS bereits auf das Droplet zeigt, bevor du HTTPS anforderst.
- Einen DigitalOcean-Account.
- Eine Flask-App in einem Git-Repository. Diese Anleitung verwendet Flask 3.1.3, das Python 3.9 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 Droplet.
Schritt 1: Das Droplet erstellen
Im DigitalOcean-Kontrollzentrum, Create → Droplets: wähle Ubuntu 24.04, die Region nahe an deinen Nutzern, den Basic-Plan mit 2 GB/1 vCPU, füge deinen SSH-Key hinzu und erstelle es.
ssh root@your-droplet-ip
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 ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "app:app"]
Flasks eigene Deployment-Docs verweisen direkt auf gunicorn: „es lässt sich meist leicht in Hosting-Plattformen integrieren” und braucht keine zusätzlichen Abhängigkeiten zur Installation. --workers 4 ist ein sinnvoller Ausgangspunkt (eine gängige Faustregel aus gunicorns eigenen Docs ist etwa 2 * CPU-Kerne + 1); passe es an die tatsächliche vCPU-Zahl des Droplets an. Falls deine App eine Application-Factory statt einer Modulebenen-app-Variable verwendet, wird der Befehl stattdessen zu gunicorn --bind 0.0.0.0:8000 'app:create_app()'.
Eine minimale requirements.txt:
flask>=3.1
gunicorn>=23.0
psycopg[binary]>=3.2
flask-sqlalchemy>=3.1
flask-migrate>=4.0
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:
DB_NAME=app
DB_USER=app
DB_PASSWORD=change-this
SECRET_KEY=change-this-too
FLASK_ENV=production
DATABASE_URL=postgresql+psycopg://app:change-this@db:5432/app
Vor dem Start validieren:
docker compose config
Schritt 5: Die Caddyfile schreiben
app.example.com {
reverse_proxy app:8000
}
Schritt 6: Erster Start und Migrationen
docker compose up -d
docker compose exec app flask db upgrade
docker compose ps
Du solltest sehen, dass app, db und caddy alle running sind, und https://app.example.com sollte mit einem gültigen Zertifikat antworten.
Schritt 7: Neue Versionen deployen
git pull
docker compose build app
docker compose up -d --no-deps app
docker compose exec app flask db upgrade
Nur app neu zu bauen lässt db und caddy unberührt, sodass Zertifikat und Datenbank während des Deploys beide weiterlaufen.
Backups
docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz
Kopiere die Datei per Cronjob vom Droplet weg und teste gelegentlich eine Wiederherstellung in eine Scratch-Datenbank, damit du weißt, dass es tatsächlich funktioniert.
Statische Dateien ausliefern
Flask kann seinen eigenen static/-Ordner direkt ausliefern, aber Caddy steht bereits vor jedem Request und ist dafür besser geeignet, als gunicorn-Worker Zeit mit dem Ausliefern von Dateien statt der Verarbeitung von Anwendungslogik verbringen zu lassen. Füge einen Matcher vor dem Catch-all-Proxy ein:
app.example.com {
handle_path /static/* {
root * /srv/static
file_server
}
reverse_proxy app:8000
}
Mounte das statische Verzeichnis der App in ein Volume, das Caddy auch lesen kann, und führe flask ... als den Befehl aus, den dein Projekt verwendet, um statische Assets während des Docker-Builds in diesen Pfad zu sammeln oder zu kopieren, damit die Dateien existieren, bevor Caddys Container danach zu suchen beginnt.
Die Abkürzung: mit Railyard
Flask auf DigitalOcean deployen führt dasselbe Rezept von einer Repository-URL aus. Verbinde einen DigitalOcean-Account mit einem Token, das Droplet-Schreibzugriff hat, oder bring einen bereits bereitgestellten Server mit. Railyard erkennt Flask anhand von requirements.txt oder pyproject.toml, bedient app:app auf $PORT mit gunicorn (ausgerichtet auf dein eigenes Modul, falls es woanders liegt) und erkennt psycopg als Abhängigkeit, um Postgres mit automatisch eingefügtem DATABASE_URL bereitzustellen. Setze flask db upgrade oder alembic upgrade head als Release-Befehl, um bei jedem Release zu migrieren, genauso wie Schritt 6 oben, nur automatisch ausgeführt, bevor jede neue Version Traffic bekommt. RQ wird als Worker angeboten, wenn es eine Abhängigkeit ist, und jeder andere Procfile-Prozess wird auch einer. Siehe die Flask-Docs für die genauen Erkennungsregeln.
Troubleshooting
- „Connection refused” beim ersten Start:
appstartete, bevor Postgres die Initialisierung abgeschlossen hat; die Healthcheck-Condition in Schritt 4 sollte das verhindern, aber eine kurze Retry-Schleife um den ersten Migrationsbefehl ist eine sinnvolle zusätzliche Absicherung. - gunicorn startet, aber Requests hängen: meist eine zu hohe Worker-Zahl für die tatsächliche CPU des Droplets, oder ein blockierender Aufruf innerhalb eines Request-Handlers, der die anderen Worker aushungert; prüfe
docker statsunter Last und reduziere--workers, bevor du stattdessen eine größere Droplet-Größe wählst. sqlalchemy.exc.OperationalError, obwohl der Datenbankcontainer läuft: bestätige, dassDATABASE_URLin.envtatsächlich mit den Zugangsdaten desdb-Containers übereinstimmt; eine veraltete.env, die von der lokalen Entwicklung übrig blieb, ist die häufigste Ursache.- HTTPS kommt nie, Caddy versucht es immer wieder: DNS zeigt noch nicht auf das Droplet.
dig +short app.example.comsollte mit der IP des Droplets übereinstimmen. flask db upgradeschlägt mit „Can’t locate revision” fehl: dermigrations/-Ordner wurde nicht ins Repository committet, oder das Image wurde aus einem veralteten Checkout gebaut, dem eine neue Migrationsdatei fehlt; bestätige, dassgit statusauf dem Server sauber ist, bevor du neu baust.- Statische Assets geben über Caddy 404 zurück, funktionieren aber bei direktem Zugriff auf gunicorn: Der
handle_path-Matcher im Abschnitt zu statischen Dateien oben traf nicht auf den Request-Pfad zu, meist weil das tatsächliche statische URL-Präfix der App sich von/static/unterscheidet; prüfe, welches Präfix die App selbst im gerenderten HTML erzeugt, bevor du annimmst, Caddy sei falsch konfiguriert.
FAQ
Warum gunicorn statt Flasks eingebautem Server? Flasks eigene Docs sind ausdrücklich, dass der Entwicklungsserver nicht besonders sicher, stabil oder effizient sein soll; gunicorn ist ein Produktions-WSGI-Server, genau für sowas gebaut, ohne zusätzliche Abhängigkeiten, die kompiliert werden müssten.
Was ist mit asynchronen Flask-Routen? Flask unterstützt async def-View-Funktionen seit Flask 2.0, aber gunicorns Standard-Sync-Worker führt keinen Event-Loop aus; wenn die App im großen Maßstab wirklich asynchrone Request-Verarbeitung braucht, sind gunicorns gevent-Worker-Klasse oder der Wechsel zu einem ASGI-basierten Framework die zwei echten Optionen, nicht etwas, das der Standard-Worker dieser Anleitung gut handhabt.
Postgres oder MySQL? Flask-SQLAlchemy ist datenbankunabhängig; diese Anleitung verwendet Postgres, aber das Tauschen des db-Images und des Schemas der SQLAlchemy-Connection-URI funktioniert genauso für MySQL.
Brauche ich wirklich ein 2-GB-Droplet? Ein 1-GB-Droplet betreibt Flask und Postgres zusammen, aber mit sehr wenig Speicherreserve für Traffic-Spitzen oder einen zweiten Prozess; 2 GB ist eine realistischere Basislinie, sobald echte Nutzer auftauchen.
Was, wenn meine App einen Hintergrund-Worker braucht, nicht nur den Web-Prozess? Füge einen Procfile-artigen Prozess (zum Beispiel worker: rq worker --url $REDIS_URL) als eigenen Service in der Compose-Datei hinzu, gebaut aus demselben Image wie app, mit eigenem command-Override, statt ihn im selben Container wie den Web-Prozess laufen zu lassen.
Wie sehe ich Logs und grundlegende Metriken, ohne jedes Mal per SSH reinzugehen? DigitalOceans eigenes Droplet-Dashboard zeigt CPU-, Speicher- und Disk-Diagramme ohne zusätzliches Setup; für Anwendungslogs bleibt docker compose logs -f app über SSH die direkteste Option, außer du leitest sie an einen externen Log-Aggregator weiter, was sich lohnt, sobald mehr als eine Person regelmäßig Logs prüfen muss.
Eine Flask-App auf einem selbst bereitgestellten DigitalOcean-Droplet kostet 12 $ im Monat, mit gunicorn, Postgres und Migrationen explizit in Dateien, die du kontrollierst, statt angenommen. Ein neues Release auszuliefern, nachdem sie existieren, ist ein Rebuild eines Containers und ein Migrationsbefehl, nichts weiter zu merken.
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.