Ghost-Instanz mit Docker Compose migrieren – ohne Datenverlust
Ein vollständiger Leitfaden zum Umzug deiner Ghost-Blog-Installation auf einen neuen Server mit minimaler Downtime
Irgendwann kommt der Moment: Der alte Server ist zu langsam, zu teuer, oder du willst einfach zu einem besseren Anbieter wechseln. Wenn dein Ghost-Blog mit Docker Compose betrieben wird, ist die Migration grundsätzlich gut beherrschbar – aber es gibt einige Fallstricke, die schnell zu Datenverlust oder längerer Downtime führen können.
Dieser Artikel zeigt dir den gesamten Prozess Schritt für Schritt: von der Vorbereitung über den Datenbankexport bis zur NGINX-Konfiguration auf dem neuen Server, in der richtigen Reihenfolge, mit echten Beispielen.
Voraussetzungen & Überblick
Wir gehen von folgender Ausgangslage aus:
- Ghost läuft mit Docker Compose (Ghost + MySQL/MariaDB + ggf. NGINX als Reverse Proxy)
- Als Datenbank wird MySQL 8 verwendet (Ghost unterstützt kein SQLite in Produktion empfohlen)
- Der alte Server läuft weiter, bis der neue vollständig bereit ist
- Auf beiden Servern ist Docker, Docker Compose und NGINX installiert
Die Migration erfolgt in dieser groben Reihenfolge:
- Neuen Server vorbereiten
- Ghost auf dem neuen Server aufsetzen (noch ohne Traffic)
- Datenbank exportieren und importieren
- Content-Files (Bilder, Themes, etc.) übertragen
- Ghost-Konfiguration anpassen
- NGINX auf dem neuen Server konfigurieren
- DNS umschalten (minimale Downtime)
- Alten Server abschalten
Schritt 1: Den alten Server verstehen – was liegt wo?
Bevor du irgendetwas kopierst, verschaffe dir einen Überblick. Eine typische Docker-Compose-Struktur für Ghost sieht so aus:
/opt/ghost/
├── docker-compose.yml
├── config.production.json # optional, falls außerhalb des Containers
├── ghost-data/ # Volume: Ghost Content
│ ├── themes/
│ ├── images/
│ ├── files/
│ ├── media/
│ └── ...
└── mysql-data/ # Volume: MySQL Daten
Eine typische docker-compose.yml:
version: '3.8'
services:
ghost:
image: ghost:5-alpine
container_name: ghost
restart: always
ports:
- "2368:2368"
environment:
database__client: mysql
database__connection__host: db
database__connection__user: ghost
database__connection__password: supersecretpassword
database__connection__database: ghost
url: https://meinblog.de
NODE_ENV: production
volumes:
- ghost-data:/var/lib/ghost/content
depends_on:
- db
db:
image: mysql:8.0
container_name: ghost-db
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: supersecretpassword
volumes:
- mysql-data:/var/lib/mysql
volumes:
ghost-data:
mysql-data:
Wichtig: Notiere dir alle Umgebungsvariablen – insbesondere die Datenbank-Credentials und die url. Du brauchst sie auf dem neuen Server.Schritt 2: Neuen Server vorbereiten
Installiere auf dem neuen Server Docker und Docker Compose:
# Docker installieren (Ubuntu/Debian)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Docker Compose Plugin prüfen
docker compose version
Erstelle das Verzeichnis für Ghost:
sudo mkdir -p /opt/ghost
cd /opt/ghost
Kopiere deine docker-compose.yml vom alten Server (per scp oder manuell):
# Vom alten Server aus ausführen:
scp /opt/ghost/docker-compose.yml user@neuer-server:/opt/ghost/docker-compose.yml
Starte Ghost auf dem neuen Server NOCH NICHT vollständig – zunächst nur die Datenbank, damit MySQL initialisiert wird:
# Nur die Datenbank starten
docker compose up -d db
# Kurz warten, bis MySQL bereit ist
sleep 15
docker compose logs db | tail -20
Schritt 3: Datenbank exportieren (alter Server)
Das ist der kritischste Schritt. Achte darauf, während des Exports keine Änderungen am Blog vorzunehmen (keine neuen Posts, keine Kommentare).
Option A: mysqldump direkt aus dem Container
# Auf dem alten Server:
docker exec ghost-db mysqldump \
-u ghost \
-psupersecretpassword \
--single-transaction \
--routines \
--triggers \
ghost > /tmp/ghost_backup.sql
# Größe prüfen
ls -lh /tmp/ghost_backup.sql
--single-transaction ist essenziell bei InnoDB – es erstellt einen konsistenten Snapshot ohne die Datenbank zu sperren.Option B: Vollständiges Backup inkl. aller Datenbanken
docker exec ghost-db mysqldump \
-u root \
-prootpassword \
--all-databases \
--single-transaction \
> /tmp/ghost_full_backup.sql
SQL-Dump auf den neuen Server übertragen:
scp /tmp/ghost_backup.sql user@neuer-server:/tmp/ghost_backup.sql
Schritt 4: Datenbank importieren (neuer Server)
# Auf dem neuen Server:
# Dump in den laufenden MySQL-Container kopieren
docker cp /tmp/ghost_backup.sql ghost-db:/tmp/ghost_backup.sql
# Import ausführen
docker exec -i ghost-db mysql \
-u ghost \
-psupersecretpassword \
ghost < /tmp/ghost_backup.sql
# Alternativ: direkt über STDIN (ohne cp)
docker exec -i ghost-db mysql \
-u ghost \
-psupersecretpassword \
ghost < /tmp/ghost_backup.sql
Import verifizieren:
docker exec -it ghost-db mysql \
-u ghost \
-psupersecretpassword \
-e "USE ghost; SHOW TABLES; SELECT COUNT(*) FROM posts;" ghost
Die Ausgabe sollte alle Ghost-Tabellen und deine Posts zeigen.
Schritt 5: Content-Volume übertragen (Bilder, Themes, Files)
Der Ghost-Content liegt im Docker Volume ghost-data. Wir müssen den gesamten Inhalt von /var/lib/ghost/content übertragen.
Methode 1: tar-Archiv über SSH (empfohlen)
# Auf dem alten Server:
# Content aus dem Container in ein Archiv packen
docker run --rm \
-v ghost_ghost-data:/source \
-v /tmp:/backup \
alpine \
tar czf /backup/ghost_content.tar.gz -C /source .
# Archiv auf neuen Server übertragen
scp /tmp/ghost_content.tar.gz user@neuer-server:/tmp/ghost_content.tar.gz
Hinweis: Der Volume-Name hängt vom Verzeichnis ab, in dem deinedocker-compose.ymlliegt. Mitdocker volume lskannst du den exakten Namen ermitteln.
docker volume ls | grep ghost
# Beispiel-Output:
# local ghost_ghost-data
# local ghost_mysql-data
Auf dem neuen Server entpacken:
# Volume muss existieren (wird beim ersten `docker compose up` erstellt)
# Falls noch nicht vorhanden:
docker compose up -d db # Ghost noch nicht starten!
# Warten bis das Volume angelegt wurde, dann Inhalt einspielen
docker run --rm \
-v ghost_ghost-data:/target \
-v /tmp:/backup \
alpine \
sh -c "cd /target && tar xzf /backup/ghost_content.tar.gz"
# Inhalt prüfen
docker run --rm \
-v ghost_ghost-data:/data \
alpine \
ls -la /data/
Methode 2: rsync direkt (falls Volumes im Filesystem zugänglich)
Wenn die Volumes unter /var/lib/docker/volumes/ liegen und du Root-Zugriff hast:
# Auf dem alten Server (als root):
rsync -avz --progress \
/var/lib/docker/volumes/ghost_ghost-data/_data/ \
user@neuer-server:/tmp/ghost-content-sync/
# Auf dem neuen Server in Volume einspielen:
docker run --rm \
-v ghost_ghost-data:/target \
-v /tmp/ghost-content-sync:/source \
alpine \
cp -a /source/. /target/
Schritt 6: Ghost-Konfiguration auf dem neuen Server anpassen
Passe die docker-compose.yml auf dem neuen Server an. Die URL muss bereits auf den neuen Server zeigen (oder noch die alte Domain, wenn du einen Testlauf machen willst):
environment:
url: https://meinblog.de # Bleibt gleich
database__connection__host: db # Bleibt gleich (interner Service-Name)
# Alle anderen Werte wie gehabt
Falls du einen Testlauf über die IP-Adresse des neuen Servers machen willst:
environment:
url: http://123.45.67.89:2368 # Temporär für den Test
Fallstrick: Ghost speichert die URL in der Datenbank (in der Tabelle settings). Wenn du die URL später änderst, musst du Ghost neu starten oder den Wert in der DB anpassen:Schritt 7: NGINX auf dem neuen Server konfigurieren
Grundlegende NGINX-Konfiguration für Ghost
Erstelle die Konfiguration (z.B. unter /etc/nginx/sites-available/meinblog.de):
server {
listen 80;
server_name meinblog.de www.meinblog.de;
# Weiterleitung zu HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name meinblog.de www.meinblog.de;
# SSL-Zertifikat (Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/meinblog.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/meinblog.de/privkey.pem;
# SSL-Härtung
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Sicherheits-Header
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header X-XSS-Protection "1; mode=block" always;
# Client-Upload-Größe (für Bilder/Dateien)
client_max_body_size 50m;
# Proxy zu Ghost
location / {
proxy_pass http://127.0.0.1:2368;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Wichtig für Ghost's URL-Erkennung:
proxy_set_header X-Forwarded-Host $host;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# WebSocket-Support (Ghost Admin nutzt das)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Statische Assets direkt aus dem Volume ausliefern (optional, performanter)
location ~* ^/content/images/ {
root /var/lib/docker/volumes/ghost_ghost-data/_data;
rewrite ^/content/images/(.*) /images/$1 break;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}
Konfiguration aktivieren und testen:
sudo ln -s /etc/nginx/sites-available/meinblog.de /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
SSL-Zertifikat mit Certbot holen (vor dem DNS-Wechsel)
Du kannst das Zertifikat bereits vor dem DNS-Umzug mit dem DNS-Challenge-Verfahren ausstellen:
sudo certbot certonly \
--manual \
--preferred-challenges dns \
-d meinblog.de \
-d www.meinblog.de
Oder nach dem DNS-Wechsel einfach:
sudo certbot --nginx -d meinblog.de -d www.meinblog.de
Schritt 8: Ghost auf dem neuen Server starten und testen
cd /opt/ghost
docker compose up -d
# Logs beobachten
docker compose logs -f ghost
Typische Erfolgsmeldung in den Logs:
ghost | [2024-01-15 10:23:45] INFO Ghost is running in production...
ghost | [2024-01-15 10:23:45] INFO Your blog is available on https://meinblog.de
Funktionstest über die IP-Adresse
Teste den Blog direkt über die IP (falls url noch auf die Domain zeigt, werden interne Links zur Domain zeigen – das ist normal):
curl -I http://123.45.67.89:2368
# HTTP/1.1 200 OK erwartet
Oder öffne http://123.45.67.89:2368 im Browser und prüfe:
- Sind alle Posts vorhanden?
- Werden Bilder korrekt angezeigt?
- Funktioniert das Admin-Panel unter
/ghost?
Schritt 9: Finales Backup kurz vor dem DNS-Wechsel
Direkt vor dem DNS-Wechsel machst du ein letztes, aktuelles Backup – damit keine Daten zwischen dem letzten regulären Backup und dem Umzug verloren gehen:
# Auf dem alten Server – finale Sicherung
docker exec ghost-db mysqldump \
-u ghost \
-psupersecretpassword \
--single-transaction \
ghost > /tmp/ghost_final_backup_$(date +%Y%m%d_%H%M%S).sql
# Nur neue/geänderte Files synchronisieren (Delta-Sync)
docker run --rm \
-v ghost_ghost-data:/source \
-v /tmp:/backup \
alpine \
tar czf /backup/ghost_content_final.tar.gz -C /source .
scp /tmp/ghost_final_backup_*.sql user@neuer-server:/tmp/
scp /tmp/ghost_content_final.tar.gz user@neuer-server:/tmp/
Dieses finale Backup auf dem neuen Server einspielen (wie in Schritt 4 und 5 beschrieben).
Schritt 10: DNS umschalten – minimale Downtime
Das ist der Moment, in dem die Downtime entsteht. Mit der richtigen Vorbereitung ist sie minimal.
Vorbereitung: TTL vorher reduzieren
24–48 Stunden vor dem Umzug die TTL deines DNS-Eintrags auf einen niedrigen Wert setzen:
meinblog.de. 300 IN A <alte-ip> # TTL: 300 Sekunden (5 Minuten)
So propagiert der neue DNS-Eintrag nach dem Wechsel viel schneller.
DNS-Eintrag aktualisieren
Im DNS-Panel (Hetzner, Cloudflare, etc.) den A-Record auf die neue Server-IP setzen:
meinblog.de. 300 IN A <neue-ip>
www.meinblog.de. 300 IN A <neue-ip>
Propagation prüfen:
# Warte und prüfe, bis die neue IP aufgelöst wird
watch -n 5 "dig +short meinblog.de"
# Oder von verschiedenen Standorten aus:
curl -s https://dns.google/resolve?name=meinblog.de&type=A | python3 -m json.tool
Schritt 11: Alten Server herunterfahren
Sobald der DNS vollständig auf den neuen Server zeigt und alles korrekt funktioniert:
# Auf dem alten Server Ghost stoppen
cd /opt/ghost
docker compose down
# Optional: Backup der alten Daten vor dem Löschen
tar czf /tmp/ghost_old_server_final.tar.gz /opt/ghost/
Lass den alten Server noch 1–2 Tage laufen (ohne Ghost), bevor du ihn abschaltest – für den Fall, dass noch etwas schiefläuft.
Bekannte Fallstricke und wie du sie vermeidest
❌ Fallstrick 1: Falsche Volume-Namen
Docker-Compose-Volume-Namen enthalten den Verzeichnisnamen als Präfix. Wenn du das Projekt in /opt/ghost hast, heißt das Volume ghost_ghost-data, nicht ghost-data.
# Immer zuerst prüfen:
docker volume ls
❌ Fallstrick 2: Ghost startet, aber Bilder fehlen
Ursache: Die Content-Files wurden nicht vollständig übertragen, oder die Berechtigungen stimmen nicht.
# Berechtigungen im Volume prüfen und korrigieren
docker run --rm \
-v ghost_ghost-data:/data \
alpine \
chown -R 1000:1000 /data
Ghost läuft im Container als User node (UID 1000).
❌ Fallstrick 3: Ghost-Admin zeigt "Password Reset" oder Login funktioniert nicht
Ursache: Die URL in der Ghost-Datenbank stimmt nicht mit der tatsächlichen URL überein. Ghost sendet Reset-Links und Admin-Tokens auf Basis dieser URL.
docker exec -it ghost-db mysql -u ghost -psupersecretpassword ghost -e \
"SELECT value FROM settings WHERE key='url';"
# Falls falsch:
docker exec -it ghost-db mysql -u ghost -psupersecretpassword ghost -e \
"UPDATE settings SET value='https://meinblog.de' WHERE key='url';"
# Ghost neustarten
docker compose restart ghost
❌ Fallstrick 4: NGINX gibt 502 Bad Gateway zurück
Ghost ist noch nicht bereit, oder der Port stimmt nicht.
# Ghost-Status prüfen
docker compose ps
docker compose logs ghost | tail -30
# Port-Mapping prüfen
docker port ghost
Häufige Ursache: Ghost braucht beim ersten Start mit einer neuen Datenbank etwas länger. Warte 30–60 Sekunden und lade neu.
❌ Fallstrick 5: MySQL 8 Auth-Plugin-Probleme
MySQL 8 nutzt standardmäßig caching_sha2_password. Falls Ghost Verbindungsprobleme hat:
docker exec -it ghost-db mysql -u root -prootpassword -e \
"ALTER USER 'ghost'@'%' IDENTIFIED WITH mysql_native_password BY 'supersecretpassword';"
❌ Fallstrick 6: Zu große MySQL-Dumps / Timeout beim Import
Bei großen Blogs kann der Import lange dauern. Erhöhe die MySQL-Timeouts:
docker exec -i ghost-db mysql \
-u ghost \
-psupersecretpassword \
--connect-timeout=300 \
--max-allowed-packet=256M \
ghost < /tmp/ghost_backup.sql
❌ Fallstrick 7: Ghost Emails funktionieren nicht mehr
Falls du Mailgun, SendGrid oder SMTP konfiguriert hast, prüfe die Mailkonfiguration in der docker-compose.yml. Diese ist nicht in der Datenbank, sondern in den Umgebungsvariablen:
environment:
mail__transport: SMTP
mail__options__host: smtp.mailgun.org
mail__options__port: 587
mail__options__auth__user: postmaster@meinblog.de
mail__options__auth__pass: dein-smtp-passwort
mail__from: noreply@meinblog.de
❌ Fallstrick 8: Members/Subscriptions mit Stripe
Falls du Ghost Members mit Stripe-Integration nutzt, musst du sicherstellen, dass die Stripe-Webhooks auf die neue Domain zeigen. Im Stripe-Dashboard unter Developers → Webhooks die Endpoint-URL aktualisieren.
Komplettes Migrations-Cheatsheet
# === ALTER SERVER ===
# 1. Datenbank-Backup
docker exec ghost-db mysqldump -u ghost -psupersecretpassword \
--single-transaction ghost > /tmp/ghost_backup.sql
# 2. Content-Backup
docker run --rm -v ghost_ghost-data:/source -v /tmp:/backup alpine \
tar czf /backup/ghost_content.tar.gz -C /source .
# 3. Konfiguration sichern
cp /opt/ghost/docker-compose.yml /tmp/docker-compose.yml.bak
# 4. Auf neuen Server übertragen
scp /tmp/ghost_backup.sql user@NEUE_IP:/tmp/
scp /tmp/ghost_content.tar.gz user@NEUE_IP:/tmp/
scp /opt/ghost/docker-compose.yml user@NEUE_IP:/opt/ghost/
# === NEUER SERVER ===
# 5. MySQL starten und warten
cd /opt/ghost && docker compose up -d db && sleep 20
# 6. Datenbank importieren
docker exec -i ghost-db mysql -u ghost -psupersecretpassword ghost \
< /tmp/ghost_backup.sql
# 7. Content einspielen
docker run --rm -v ghost_ghost-data:/target -v /tmp:/backup alpine \
sh -c "cd /target && tar xzf /backup/ghost_content.tar.gz"
# 8. NGINX konfigurieren und SSL holen
sudo certbot --nginx -d meinblog.de
# 9. Ghost starten
docker compose up -d
# 10. Testen, dann DNS umschalten
Fazit
Eine Ghost-Migration mit Docker Compose ist gut durchführbar, wenn man die Reihenfolge einhält: erst Datenbank sichern, dann Content übertragen, dann den neuen Server vollständig testen – und erst ganz am Ende den DNS umschalten. Mit einer vorher reduzierten TTL ist die tatsächliche Downtime auf wenige Minuten begrenzt.
Die häufigsten Probleme entstehen durch falsche Volume-Namen, Berechtigungen oder die URL-Einstellung in der Ghost-Datenbank. Mit den hier gezeigten Prüfschritten findest du diese Probleme schnell.
Hast du Fragen zu einem spezifischen Schritt oder einer anderen Datenbankversion? Schreib es in die Kommentare.
Zuletzt aktualisiert: 2026 – getestet mit Ghost 5.x, MySQL 8.0, Docker Compose v2