Django auf DigitalOcean deployen
Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig
Du hast eine Django-App, die auf deinem Laptop läuft, und einen DigitalOcean-Account. Was fehlt, ist alles dazwischen: ein Server, der oben bleibt, eine Postgres-Datenbank statt SQLite, ein Prozessmanager, der gunicorn nach einem Absturz neu startet, korrekt ausgelieferte statische Dateien, und ein Zertifikat, damit die Seite auf https:// antwortet statt auf einer blanken IP-Adresse. Keines davon ist für sich allein schwer, aber es ist leicht, ein Stück fast richtig hinzubekommen und einen Abend mit ALLOWED_HOSTS zu verbringen.
Dieser Guide baut das Ganze mit Docker Compose: ein Container für Postgres, einer für deine Django-App hinter gunicorn, und einer für Caddy, das HTTPS und statische Dateien übernimmt. Am Ende hast du ein Droplet, das deine App auf deiner eigenen Domain bedient, mit einem wiederholbaren Weg, neue Versionen zu deployen und die Datenbank zu sichern. Dann schauen wir uns die Abkürzung an: dasselbe Repository zu Railyard pushen und es requirements.txt für dich lesen lassen.
Was du brauchst
- Ein DigitalOcean-Droplet. Der Basic-Plan mit 1 vCPU / 2 GB RAM betreibt Postgres, gunicorn und Caddy zusammen bequem und kostet derzeit 12 $/Monat; der 1-GB-Plan (6 $/Monat) funktioniert für sehr kleine Apps, lässt aber wenig Spielraum, sobald auch Postgres läuft.
- Eine Domain, mit Zugriff, um einen
A-Record hinzuzufügen. - Ein GitHub- (oder jeder andere Git-Host-) Repository mit deinem Django-Projekt.
- Ein DigitalOcean-Account mit eingerichteter Abrechnung und einem zum Account hinzugefügten SSH-Key.
Schritt 1: Das Droplet erstellen
Erstelle im DigitalOcean-Dashboard ein neues Droplet: wähle das Ubuntu 24.04 LTS-Image, den 2-GB-Basic-Plan, eine Region nahe deinen Nutzern und deinen SSH-Key. Sobald es läuft, notiere die öffentliche IP-Adresse und richte den A-Record deiner Domain darauf aus:
myproject.example.com. A 203.0.113.10
Die DNS-Propagation kann einige Minuten bis einige Stunden dauern. Du kannst den Server in der Zwischenzeit weiter einrichten; Caddy braucht den korrekten DNS-Record erst, wenn es versucht, ein Zertifikat auszustellen.
SSH als root einloggen und Docker installieren:
ssh root@203.0.113.10
curl -fsSL https://get.docker.com | sh
apt-get install -y docker-compose-plugin
Prüfen, ob es funktioniert hat:
docker compose version
Du solltest einen Versionsstring für das Compose-Plugin sehen, keinen “command not found”-Fehler.
Schritt 2: Die Django-App für Produktion vorbereiten
Djangos Standardwerte gehen von einem Entwickler aus, der lokal manage.py runserver ausführt, nicht von einem Server. Ein paar Einstellungen müssen sich für Produktion ändern. In 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"
Pinne die benötigten Pakete in requirements.txt. Dieser Guide nutzt Django 6.1, gunicorn 26.2 als WSGI-Server und psycopg2-binary 2.9.13 zum Sprechen mit Postgres:
django==6.1.2
gunicorn==26.2.0
psycopg2-binary==2.9.13
Wenn du bereits django-environ oder dj-database-url nutzt, um DATABASE_URL als einen einzigen String zu lesen, funktioniert das auch; die obigen Umgebungsvariablen sind nur die simpelste Version.
Schritt 3: Das Dockerfile schreiben
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"]
Ersetze myproject.wsgi durch dein tatsächliches Projektpaket. migrate vor dem Start von gunicorn auszuführen bedeutet, dass jeder Deploy automatisch anstehende Migrationen anwendet; für ein Single-Server-Setup wie dieses ist das einfacher als ein separater Release-Schritt. Drei gunicorn-Worker sind ein vernünftiger Startpunkt für ein 2-GB-Droplet; beobachte den Speicher unter Last und passe an.
Schritt 4: docker-compose.yml schreiben
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:
Nur Caddy veröffentlicht Ports nach außen; web und db bleiben nur von Containern im selben Compose-Netzwerk aus erreichbar, was eine Sache weniger ist, die aus Versehen ins Internet exponiert wird.
Schritt 5: Das Caddyfile schreiben
myproject.example.com {
encode gzip
handle_path /static/* {
root * /srv/static
file_server
}
reverse_proxy web:8000
}
Caddy fordert und erneuert beim ersten Start automatisch ein Let’s-Encrypt-Zertifikat für myproject.example.com, solange das DNS der Domain schon auf diesen Server zeigt und die Ports 80 und 443 ihn erreichen. Statische Dateien unter /static/ werden direkt von Caddy aus dem geteilten Volume ausgeliefert; alles andere wird an gunicorn weitergeleitet.
Schritt 6: Umgebungsvariablen und Secrets setzen
Erstelle eine .env-Datei neben docker-compose.yml, und halte sie außerhalb der Versionskontrolle:
POSTGRES_DB=myproject
POSTGRES_USER=myproject
POSTGRES_PASSWORD=a-long-random-password
DJANGO_ALLOWED_HOSTS=myproject.example.com
SECRET_KEY=a-different-long-random-value
Generiere beide zufälligen Werte mit openssl rand -base64 32, statt etwas Merkbares einzutippen; diese Datenbank und diese Django-Instanz müssen nur unerratbar sein, nicht merkbar.
Schritt 7: Die Firewall öffnen und deployen
ufw allow 22
ufw allow 80
ufw allow 443
ufw enable
Dann, aus dem Verzeichnis mit deiner docker-compose.yml:
docker compose up -d --build
docker compose logs -f caddy
Du solltest sehen, wie Caddy loggt, dass es ein Zertifikat für deine Domain erhalten hat, dann sollte docker compose logs -f web zeigen, wie gunicorn startet und Migrationen angewendet werden. Besuche https://myproject.example.com in einem Browser: Du solltest deine App über HTTPS mit einem gültigen Zertifikat sehen, keine Browser-Warnung.
Backups und Upgrades
Sichere Postgres mit einem geplanten Dump, der außerhalb des Containers geschrieben wird, damit ein verlorenes Volume nicht auch das Backup mitnimmt:
docker compose exec -T db pg_dump -U myproject myproject > backup-$(date +%F).sql
Setze das in einen Cron-Job und kopiere das Ergebnis irgendwohin außerhalb des Droplets (DigitalOcean Spaces, S3, oder einfach scp auf eine andere Maschine). Um die App zu aktualisieren, ziehe deinen neuesten Code, baue neu, und fahre die Container wieder hoch; der migrate-Schritt im CMD des Dockerfiles läuft automatisch erneut:
git pull
docker compose up -d --build web
Die Abkürzung: mit Railyard machen
Alles oben (das Dockerfile, gunicorn, der Migrationsschritt, DATABASE_URL, der Caddy-Proxy und das Zertifikat) ist, was Railyard automatisch einrichtet, wenn du es auf dasselbe Repository zeigst. Aus den eigenen Erkennungsregeln: Django wird aus requirements.txt, pyproject.toml oder einem manage.py, das Django importiert, erkannt; der Release-Schritt führt python manage.py migrate --no-input aus, bevor die neue Version Traffic erhält; der Web-Prozess läuft auf gunicorn, gebunden an den von Railyard zugewiesenen Port; und eine psycopg/psycopg2-Abhängigkeit stellt automatisch eine Postgres-Datenbank bereit, wobei DATABASE_URL als Umgebungsvariable injiziert wird, kein Dockerfile oder Compose-File nötig. collectstatic läuft ebenfalls automatisch während des Builds, sodass WhiteNoise oder ein Standard-STATIC_ROOT-Setup funktioniert, ohne diesen Schritt selbst zu schreiben. Siehe die Django-Docs für die genauen Regeln und Datenbanken und Backups dafür, wie die gemanagte Postgres-Instanz gesichert und wiederhergestellt wird.
Um es so zu deployen: erstelle über Railyard ein Droplet oder einen Server (DigitalOcean ist einer der unterstützten Provider), füge dann die Git-URL deines Repositorys ein, oder füge das deinem README für einen Klick hinzu:
[](https://app.railyard.run/deploy?repo=https://github.com/you/your-django-app)
Der manuelle Weg oben ist weiterhin der richtige, um ihn zu kennen. Es ist das, was Railyard unter der Haube macht, und ihn zu verstehen bedeutet, dass du nie feststeckst, wenn etwas eine Änderung braucht, die das Dashboard noch nicht freigibt.
Troubleshooting
Caddy bekommt nie ein Zertifikat. Prüfe, dass der A-Record der Domain tatsächlich auf das Droplet auflöst (dig myproject.example.com) und dass die Ports 80 und 443 sowohl in der Firewall des Droplets als auch in DigitalOceans Cloud-Firewall offen sind, falls du eine hinzugefügt hast. Let’s Encrypt validiert zuerst über plain HTTP, also muss Port 80 erreichbar sein, obwohl die finale Seite HTTPS-only ist.
“Bad Request (400)” von Django. Das ist fast immer ALLOWED_HOSTS, das nicht zur besuchten Domain passt. Bestätige, dass DJANGO_ALLOWED_HOSTS in .env exakt übereinstimmt, deploye erneut mit docker compose up -d web, und prüfe docker compose logs web.
Statische Dateien 404. collectstatic muss laufen, bevor Caddy irgendetwas von /static/ ausliefern kann, und das static_files-Volume muss sowohl im web- als auch im caddy-Service an den jeweils erwarteten Pfaden gemountet sein. Falls du STATIC_ROOT geändert hast, aktualisiere das Dockerfile und die Volume-Pfade in docker-compose.yml entsprechend.
Postgres-Container startet nicht, oder Daten sehen nach einem Restart gelöscht aus. Prüfe docker compose logs db auf Berechtigungsfehler bei /var/lib/postgresql/data; das bedeutet meist, dass das benannte Volume entfernt und neu erstellt wurde (zum Beispiel durch docker compose down -v, was Volumes löscht). Führe dieses Flag nie aus, außer du willst die Daten wirklich löschen.
Port 80 oder 443 schon belegt. Ein anderer Webserver (oft ein von einem DigitalOcean-One-Click-Image installiertes nginx) ist bereits daran gebunden. Stoppe und deaktiviere ihn: systemctl stop nginx && systemctl disable nginx.
Gunicorn-Worker werden gekillt, oder das Droplet fühlt sich unter Last langsam an. Auf einem 1-GB-Droplet können Postgres, drei gunicorn-Worker und Caddy um Speicher konkurrieren. Reduziere auf zwei Worker, füge eine Swap-Datei als Sicherheitsnetz hinzu, oder gehe auf 2 GB, falls die App echten Traffic hat.
FAQ
Brauche ich Docker, oder kann ich das mit systemd und einer virtualenv betreiben? Docker Compose hält Postgres, die App und Caddy isoliert und leicht zwischen Servern verschiebbar, aber eine virtualenv mit per systemd laufendem gunicorn und einem systemweit installierten Postgres funktionieren genauso; der gunicorn-Befehl und die Caddy-Konfiguration oben ändern sich in beiden Fällen nicht.
Kann ich SQLite statt Postgres verwenden? Für ein einzelnes Droplet mit geringem Schreibvolumen ja, obwohl du dann später nicht mehr als einen web-Container laufen lassen kannst und Backups bedeuten, eine einzelne Datei zu kopieren statt einen Dump. Postgres kostet hier nichts extra, da es nur ein weiterer Container ist.
Was ist mit Celery oder Hintergrundjobs? Füge docker-compose.yml einen worker-Service hinzu, der celery -A myproject worker --loglevel=info gegen dasselbe Image und dieselbe .env-Datei ausführt; er braucht Redis oder RabbitMQ als Broker, was ein weiterer Container ist, genau wie Postgres.
Brauche ich ein größeres Droplet, wenn ich später mehr Apps hinzufüge? Erst, wenn dir tatsächlich der Speicher oder die CPU ausgehen. Ein Droplet kann mehrere Apps hinter derselben Caddy-Instanz auf verschiedenen Subdomains betreiben, jede mit eigenem Zertifikat, genauso wie es eine betreibt.
Eine Django-App auf einem Droplet ist nicht fragil, sobald die Teile korrekt verdrahtet sind, und keines der Teile hier ist Django-spezifisch: derselbe Postgres-Container, derselbe Caddy-Proxy und derselbe Backup-Cron-Job funktionieren auch für die nächste App, die du auf diesen Server stellst.
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.