Ghost Blog – Umzug auf neuen Server (Docker Compose + NGINX)

Ghost Blog – Umzug auf neuen Server (Docker Compose + NGINX)
Photo by Kevin Ache / Unsplash
Ziel: Komplette Ghost-Instanz (Inhalte, Datenbank, Medien, Theme) von Server A auf Server B migrieren – ohne Datenverlust, mit minimaler Downtime. Die Domain bleibt gleich. NGINX läuft auf beiden Servern nativ (nicht im Container).

Inhaltsverzeichnis

  1. Voraussetzungen
  2. Überblick über die Verzeichnisstruktur
  3. Phase 1 – Alte Instanz sichern
  4. Phase 2 – Neue Instanz vorbereiten
  5. Phase 3 – Daten übertragen
  6. Phase 4 – Neue Instanz starten & prüfen
  7. Phase 5 – NGINX auf dem neuen Server konfigurieren
  8. Phase 6 – DNS umstellen & Go-Live
  9. Phase 7 – Alten Server abschalten
  10. Rollback-Plan
  11. Checkliste

1. Voraussetzungen

Auf dem alten Server (Server A):

  • Ghost läuft als docker-compose-Stack
  • NGINX ist nativ installiert und als Reverse Proxy konfiguriert
  • SSH-Zugang vorhanden
  • rsync oder scp verfügbar

Auf dem neuen Server (Server B):

  • Ubuntu 22.04 LTS empfohlen
  • Docker + Docker Compose installiert
  • NGINX installiert (apt install nginx)
  • SSH-Zugang vorhanden
  • Gleiche oder höhere Docker-Version wie auf Server A

Lokal:

  • SSH-Keys für beide Server hinterlegt
⚠️ Wichtig: Notiere dir vor dem Start die Ghost-Version auf Server A (docker inspect <ghost-container> | grep -i image). Die neue Instanz muss dieselbe Version verwenden – erst nach erfolgreichem Umzug updaten.

2. Überblick über die Verzeichnisstruktur

Eine typische Ghost-Docker-Compose-Instanz sieht so aus:

/opt/ghost/
├── docker-compose.yml
├── .env                    # Umgebungsvariablen (DB-Passwörter etc.)
├── ghost-content/          # Ghost Content-Verzeichnis (Volumes)
│   ├── data/               # SQLite-Datenbank (falls nicht MySQL)
│   ├── images/
│   ├── themes/
│   ├── files/
│   └── logs/
└── mysql-data/             # MySQL-Daten (falls MySQL genutzt)
Die genauen Pfade hängen von deiner docker-compose.yml ab. Passe alle Pfade in dieser Anleitung entsprechend an.

3. Phase 1 – Alte Instanz sichern

3.1 Aktuellen Zustand dokumentieren

# Auf Server A
cd /opt/ghost

# Ghost-Version notieren
docker compose ps
docker compose images

# Laufende Container anzeigen
docker ps -a

# Verwendete Ports anzeigen
docker compose port ghost 2368

3.2 Ghost in den Wartungsmodus versetzen (optional, empfohlen)

Damit während des Backups keine neuen Inhalte verloren gehen, Ghost kurz stoppen:

# Auf Server A
cd /opt/ghost
docker compose stop ghost
Der MySQL-Container kann weiterlaufen – wir greifen gleich direkt darauf zu.

3.3 MySQL-Datenbank exportieren

# Auf Server A – Datenbankname, User und Passwort aus .env entnehmen
source /opt/ghost/.env

docker compose exec mysql mysqldump \
  -u"${MYSQL_USER}" \
  -p"${MYSQL_PASSWORD}" \
  "${MYSQL_DATABASE}" \
  > /opt/ghost/backup_ghost_db_$(date +%Y%m%d_%H%M%S).sql

# Dump prüfen (sollte mehrere MB groß sein)
ls -lh /opt/ghost/backup_ghost_db_*.sql
Falls du SQLite nutzt (kein MySQL-Container), liegt die Datenbank unter ghost-content/data/ghost.db – diese Datei wird im nächsten Schritt mit dem Content-Verzeichnis gesichert.

3.4 Ghost Content-Verzeichnis sichern

# Auf Server A
tar -czf /opt/ghost/backup_ghost_content_$(date +%Y%m%d_%H%M%S).tar.gz \
  -C /opt/ghost ghost-content/

# Integrität prüfen
tar -tzf /opt/ghost/backup_ghost_content_*.tar.gz | head -20

3.5 Docker Compose-Konfiguration sichern

# Auf Server A
cp /opt/ghost/docker-compose.yml /opt/ghost/docker-compose.yml.bak
cp /opt/ghost/.env /opt/ghost/.env.bak

# Alles in ein Archiv
tar -czf /opt/ghost/backup_config_$(date +%Y%m%d_%H%M%S).tar.gz \
  /opt/ghost/docker-compose.yml \
  /opt/ghost/.env

3.6 NGINX-Konfiguration sichern

# Auf Server A
sudo cp -r /etc/nginx /tmp/nginx_backup_$(date +%Y%m%d)
sudo tar -czf /opt/ghost/backup_nginx_$(date +%Y%m%d_%H%M%S).tar.gz /tmp/nginx_backup_*

3.7 Backups auf lokalen Rechner herunterladen (Sicherheitskopie)

# Lokal ausführen
scp user@server-a:/opt/ghost/backup_*.* ./ghost-migration-backup/

4. Phase 2 – Neue Instanz vorbereiten

4.1 Docker und Docker Compose installieren (falls noch nicht vorhanden)

# Auf Server B
sudo apt update && sudo apt upgrade -y

# Docker installieren
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker

# Docker Compose Plugin prüfen
docker compose version

4.2 Verzeichnisstruktur anlegen

# Auf Server B
sudo mkdir -p /opt/ghost/ghost-content/{data,images,themes,files,logs}
sudo mkdir -p /opt/ghost/mysql-data
sudo chown -R $USER:$USER /opt/ghost

4.3 NGINX installieren und vorbereiten

# Auf Server B
sudo apt install -y nginx certbot python3-certbot-nginx
sudo systemctl enable nginx
sudo systemctl start nginx

4.4 Docker Compose-Konfiguration übertragen und anpassen

Kopiere die docker-compose.yml von Server A:

# Lokal oder direkt von Server A auf Server B
scp user@server-a:/opt/ghost/docker-compose.yml user@server-b:/opt/ghost/
scp user@server-a:/opt/ghost/.env user@server-b:/opt/ghost/

Wichtige Anpassungen in der docker-compose.yml auf Server B:

version: '3.8'

services:
  ghost:
    image: ghost:5.x.x   # Gleiche Version wie auf Server A!
    restart: always
    ports:
      - "127.0.0.1:2368:2368"   # Nur lokal binden – NGINX proxyt nach außen
    environment:
      database__client: mysql
      database__connection__host: mysql
      database__connection__user: ${MYSQL_USER}
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ${MYSQL_DATABASE}
      url: https://deine-domain.de  # Domain anpassen
      mail__transport: SMTP         # Mail-Einstellungen ggf. anpassen
    volumes:
      - ./ghost-content:/var/lib/ghost/content
    depends_on:
      mysql:
        condition: service_healthy

  mysql:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - ./mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
Port-Hinweis: Ghost intern läuft auf Port 2368. Binde ihn an 127.0.0.1, damit er nicht direkt öffentlich erreichbar ist. NGINX auf dem Host proxyt dann auf diesen Port.

5. Phase 3 – Daten übertragen

5.1 Ghost Content-Verzeichnis übertragen

# Auf Server A – Content direkt auf Server B übertragen
rsync -avz --progress \
  /opt/ghost/ghost-content/ \
  user@server-b:/opt/ghost/ghost-content/

# Alternativ: Archiv übertragen und entpacken
scp /opt/ghost/backup_ghost_content_*.tar.gz user@server-b:/opt/ghost/
ssh user@server-b "cd /opt/ghost && tar -xzf backup_ghost_content_*.tar.gz"

5.2 Datenbank-Dump übertragen

# Auf Server A
scp /opt/ghost/backup_ghost_db_*.sql user@server-b:/opt/ghost/

5.3 MySQL auf Server B starten (nur den DB-Container)

# Auf Server B – nur MySQL starten, Ghost noch nicht
cd /opt/ghost
docker compose up -d mysql

# Warten bis MySQL bereit ist (ca. 15-30 Sekunden)
docker compose logs -f mysql
# Warten auf: "ready for connections"

5.4 Datenbank-Dump einspielen

# Auf Server B
source /opt/ghost/.env

# SQL-Dump in den Container kopieren
docker compose cp /opt/ghost/backup_ghost_db_*.sql mysql:/tmp/ghost_backup.sql

# Dump einspielen
docker compose exec mysql mysql \
  -u"${MYSQL_USER}" \
  -p"${MYSQL_PASSWORD}" \
  "${MYSQL_DATABASE}" \
  < /tmp/ghost_backup.sql

# Erfolg prüfen: Tabellen anzeigen
docker compose exec mysql mysql \
  -u"${MYSQL_USER}" \
  -p"${MYSQL_PASSWORD}" \
  -e "USE ${MYSQL_DATABASE}; SHOW TABLES;"

5.5 Berechtigungen im Content-Verzeichnis setzen

# Auf Server B – Ghost läuft im Container als UID 1000
sudo chown -R 1000:1000 /opt/ghost/ghost-content/

6. Phase 4 – Neue Instanz starten & prüfen

6.1 Ghost-Container starten

# Auf Server B
cd /opt/ghost
docker compose up -d

# Logs beobachten
docker compose logs -f ghost
# Warten auf: "Ghost boot" oder "Listening on: http://..."

6.2 Interne Erreichbarkeit testen

# Auf Server B – direkt gegen Ghost testen (ohne NGINX)
curl -I http://127.0.0.1:2368

# Erwartete Antwort: HTTP/1.1 200 OK oder 301 Redirect

6.3 Admin-Panel über direkten Zugriff prüfen (temporär)

Trage kurzzeitig eine /etc/hosts-Zeile auf deinem lokalen Rechner ein:

# Lokal: /etc/hosts
<IP-Server-B>    deine-domain.de

Dann https://deine-domain.de/ghost aufrufen und prüfen:

  • Alle Beiträge vorhanden
  • Bilder laden korrekt
  • Theme aktiv

Danach den Eintrag aus /etc/hosts wieder entfernen.


7. Phase 5 – NGINX auf dem neuen Server konfigurieren

7.1 NGINX-Konfiguration erstellen

# Auf Server B
sudo nano /etc/nginx/sites-available/ghost

Inhalt:

server {
    listen 80;
    server_name deine-domain.de www.deine-domain.de;

    # Weiterleitung zu HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name deine-domain.de www.deine-domain.de;

    # SSL-Zertifikat (Let's Encrypt oder eigenes)
    ssl_certificate /etc/letsencrypt/live/deine-domain.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/deine-domain.de/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Sicherheits-Header
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";

    # Client Upload-Größe (für Medien-Uploads)
    client_max_body_size 50m;

    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;

        proxy_buffering off;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

7.2 Konfiguration aktivieren und SSL einrichten

# Auf Server B
sudo ln -s /etc/nginx/sites-available/ghost /etc/nginx/sites-enabled/

# Konfiguration testen
sudo nginx -t

# SSL-Zertifikat ausstellen (funktioniert erst nach DNS-Umstellung)
# Alternativ: Zertifikat vom alten Server übertragen (siehe 7.3)
sudo certbot --nginx -d deine-domain.de -d www.deine-domain.de

7.3 Zertifikate vom alten Server übertragen (Alternative)

Falls du das SSL-Zertifikat direkt übertragen möchtest (vor DNS-Umstellung):

# Auf Server A – Zertifikate kopieren
sudo tar -czf /tmp/letsencrypt_backup.tar.gz /etc/letsencrypt/
scp /tmp/letsencrypt_backup.tar.gz user@server-b:/tmp/

# Auf Server B – entpacken
sudo tar -xzf /tmp/letsencrypt_backup.tar.gz -C /
sudo systemctl reload nginx
⚠️ Let's Encrypt-Zertifikate sind an die Domain gebunden, nicht an den Server. Das Übertragen ist legitim und funktioniert problemlos.

7.4 NGINX neu laden

# Auf Server B
sudo systemctl reload nginx

8. Phase 6 – DNS umstellen & Go-Live

8.1 Vorbereitungen

# Auf Server A – Ghost wieder starten (falls noch gestoppt)
cd /opt/ghost
docker compose start ghost

# Auf Server A – aktuellen DNS-TTL prüfen
dig deine-domain.de +short

8.2 DNS-Eintrag umstellen

Ändere den A-Record deiner Domain in deinem DNS-Provider auf die IP von Server B:

deine-domain.de    A    <IP-Server-B>    TTL: 300
www.deine-domain.de  A  <IP-Server-B>    TTL: 300
Setze den TTL vor der Umstellung auf 300 Sekunden (5 Minuten), damit die Propagation schneller erfolgt.

8.3 Propagation beobachten

# Lokal – DNS-Propagation prüfen
watch -n 10 "dig deine-domain.de +short"

# Alternativ: https://dnschecker.org

8.4 Letzten Inhalt sichern und übertragen

Sobald der DNS propagiert ist, aber bevor du Server A abschaltest, synchronisiere ein letztes Mal alle Inhalte, die während der Umstellung entstanden sind:

# Auf Server A – finalen DB-Dump erstellen
source /opt/ghost/.env
docker compose exec mysql mysqldump \
  -u"${MYSQL_USER}" \
  -p"${MYSQL_PASSWORD}" \
  "${MYSQL_DATABASE}" > /opt/ghost/backup_ghost_db_final.sql

# Auf Server A – Ghost stoppen
docker compose stop ghost

# Finalen Dump auf Server B übertragen und einspielen
scp /opt/ghost/backup_ghost_db_final.sql user@server-b:/opt/ghost/

# Auf Server B – finalen Dump einspielen
source /opt/ghost/.env
docker compose stop ghost
docker compose cp /opt/ghost/backup_ghost_db_final.sql mysql:/tmp/ghost_final.sql
docker compose exec mysql mysql \
  -u"${MYSQL_USER}" \
  -p"${MYSQL_PASSWORD}" \
  "${MYSQL_DATABASE}" < /tmp/ghost_final.sql

# Neue Medien-Dateien synchronisieren
rsync -avz user@server-a:/opt/ghost/ghost-content/images/ \
  /opt/ghost/ghost-content/images/

# Ghost auf Server B wieder starten
docker compose start ghost

9. Phase 7 – Alten Server abschalten

9.1 Finale Überprüfung

  • [ ] Alle Beiträge auf https://deine-domain.de erreichbar
  • [ ] Bilder und Medien laden korrekt
  • [ ] Admin-Panel (/ghost) funktioniert
  • [ ] Neue Beiträge können erstellt werden
  • [ ] E-Mail-Versand funktioniert
  • [ ] SSL-Zertifikat gültig (https:// ohne Warnung)

9.2 NGINX auf Server A deaktivieren

# Auf Server A
sudo systemctl stop nginx
docker compose down

9.3 Monitoring einrichten (empfohlen)

# Auf Server B – einfaches Monitoring mit cron
(crontab -l 2>/dev/null; echo "*/5 * * * * curl -sf https://deine-domain.de > /dev/null || echo 'Ghost down!' | mail -s 'Alert' dein@email.de") | crontab -

9.4 Alten Server archivieren oder löschen

Behalte Server A noch mindestens 7 Tage aktiv (aber ohne Traffic), bevor du ihn endgültig abschaltest oder löschst.


10. Rollback-Plan

Falls etwas schiefläuft, kannst du schnell zurück:

  1. DNS zurück auf Server A zeigen (A-Record auf alte IP setzen)
  2. NGINX auf Server A starten: sudo systemctl start nginx
  3. Ghost auf Server A starten: cd /opt/ghost && docker compose start ghost
  4. Propagation abwarten (5–15 Minuten bei TTL 300)

11. Checkliste

Server A (alt)

  • [ ] Ghost-Version notiert
  • [ ] MySQL-Dump erstellt und geprüft
  • [ ] Content-Verzeichnis gesichert
  • [ ] docker-compose.yml und .env gesichert
  • [ ] NGINX-Konfiguration gesichert
  • [ ] SSL-Zertifikate gesichert
  • [ ] Backups lokal heruntergeladen

Server B (neu)

  • [ ] Docker + Docker Compose installiert
  • [ ] NGINX installiert
  • [ ] Verzeichnisstruktur angelegt
  • [ ] docker-compose.yml angepasst (Version, Port 127.0.0.1:2368)
  • [ ] .env übertragen
  • [ ] MySQL-Dump eingespielt
  • [ ] Content-Verzeichnis übertragen
  • [ ] Berechtigungen gesetzt (UID 1000)
  • [ ] Ghost intern erreichbar (curl http://127.0.0.1:2368)
  • [ ] NGINX-Konfiguration erstellt und aktiviert
  • [ ] SSL-Zertifikat vorhanden

Go-Live

  • [ ] DNS-TTL auf 300 gesetzt
  • [ ] A-Record auf Server B umgestellt
  • [ ] DNS-Propagation abgewartet
  • [ ] Finaler DB-Dump eingespielt
  • [ ] Finale Medien-Synchronisation durchgeführt
  • [ ] Ghost auf Server A gestoppt
  • [ ] Alle Funktionen auf Server B getestet

Erstellt für Ghost 5.x mit Docker Compose und nativem NGINX als Reverse Proxy.