Ghost mit MySQL selbst hosten
Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig
Ghosts eigener Ein-Zeilen-Installer bringt dir schnell einen funktionierenden Blog, geht aber stillschweigend von SQLite und einer einzigen Maschine aus, die du nie umziehen wirst. Sobald du MySQL willst, einen zweiten Server für Background-Jobs, oder einfach eine Docker-Compose-Datei, die du selbst lesen und sichern kannst, bist du auf dich gestellt. Diese Anleitung baut dieses Setup komplett auf: Ghost und MySQL als zwei Container, Caddy davor für HTTPS, echte Backups und ein Upgrade-Pfad, bei dem du nicht die Daumen drücken musst. Am Ende läuft eine Publikation auf einer Infrastruktur, die du verstehst, plus die Ein-Klick-Version, falls du das YAML lieber überspringst.
Diese Anleitung nutzt für das manuelle Setup unten Ghosts aktuelle stabile Release-Linie (Ghost 6.x) und MySQL 8.0.
Was du brauchst
- Einen Server. Ein DigitalOcean Basic Droplet mit 2 vCPUs und 2 GB RAM betreibt Ghost und MySQL komfortabel für 18 $/Monat; der Droplet mit 1 vCPU / 1 GB für 6 $/Monat ist zu knapp, sobald MySQL und Node beide resident sind. Jede Ubuntu-22.04- oder 24.04-Box funktioniert genauso.
- Einen Domainnamen, dessen DNS-A-Record auf die IP des Servers zeigt, bevor du zum HTTPS-Schritt kommst.
- Einen SMTP-Anbieter (Postmark, SES, Mailgun oder ähnlich) für transaktionale E-Mails. Ghost braucht das für Staff-Einladungen, Passwort-Resets und Member-Login-Links; das ist unabhängig vom Newsletter-Versand, den Ghost selbst übernimmt, sobald Mail konfiguriert ist.
Schritt 1: DNS setzen und den Server vorbereiten
Lege zuerst den A-Record für deine Domain an, da die spätere Zertifikatsausstellung davon abhängt, dass er schon aufgelöst wird:
dig +short blog.example.com
Du solltest die IP-Adresse deines Servers sehen, sobald die Propagierung abgeschlossen ist, typischerweise innerhalb von Minuten bei den meisten Registraren.
Verbinde dich per SSH und installiere Docker Engine und das Compose-Plugin:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
Melde dich ab und wieder an, damit die Gruppenänderung wirksam wird, und bestätige:
docker compose version
Du solltest einen Compose-Versions-String (v2.x) sehen, keinen “command not found”-Fehler.
Schritt 2: Die Docker-Compose-Datei schreiben
Lege ein Projektverzeichnis und eine docker-compose.yml mit drei Diensten an: Ghost, MySQL und Caddy als Reverse-Proxy.
services:
ghost:
image: ghost:6-alpine
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
NODE_ENV: production
url: https://${DOMAIN:?set DOMAIN in .env}
database__client: mysql
database__connection__host: db
database__connection__port: "3306"
database__connection__user: ghost
database__connection__password: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}
database__connection__database: ghost
mail__transport: SMTP
mail__options__host: ${SMTP_HOST}
mail__options__port: "465"
mail__options__secure: "true"
mail__options__auth__user: ${SMTP_USER}
mail__options__auth__pass: ${SMTP_PASSWORD}
mail__from: ${MAIL_FROM}
volumes:
- ghost_content:/var/lib/ghost/content
networks:
- ghost_net
db:
image: mysql:8.0.44
restart: unless-stopped
environment:
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}
MYSQL_ROOT_PASSWORD: ${DATABASE_ROOT_PASSWORD:?set DATABASE_ROOT_PASSWORD in .env}
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${DATABASE_ROOT_PASSWORD}"]
interval: 5s
start_period: 30s
retries: 20
networks:
- ghost_net
caddy:
image: caddy:2
restart: unless-stopped
depends_on:
- ghost
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- ghost_net
networks:
ghost_net:
volumes:
ghost_content:
mysql_data:
caddy_data:
caddy_config:
Die Umgebungsvariablen mit doppeltem Unterstrich (database__connection__host) sind Ghosts eigene Konvention, um die verschachtelte JSON-Konfiguration zu überschreiben, ohne eine Datei zu berühren, dokumentiert in Ghosts Konfigurationsreferenz. url muss von Anfang an die echte https://-Adresse sein: Ghost bäckt sie in generierte Links und Themes ein, und eine spätere Änderung braucht einen vollständigen ghost config- und Cache-Rebuild statt einer schnellen Bearbeitung.
Lege neben der Compose-Datei eine .env mit echten Werten an:
DOMAIN=blog.example.com
DATABASE_PASSWORD=a-long-random-string
DATABASE_ROOT_PASSWORD=a-different-long-random-string
SMTP_HOST=smtp.postmarkapp.com
SMTP_USER=your-smtp-username
SMTP_PASSWORD=your-smtp-password
MAIL_FROM="Your Blog <hello@blog.example.com>"
Generiere die zwei Passwörter, statt sie von Hand einzutippen:
openssl rand -hex 24
Du solltest jedes Mal einen 48-Zeichen-Hex-String zurückbekommen; nutze einen für DATABASE_PASSWORD und einen zweiten für DATABASE_ROOT_PASSWORD.
Schritt 3: Die Caddyfile schreiben
Caddy fordert das Zertifikat automatisch an und erneuert es, sobald es für deine Domain auf den Ports 80 und 443 antworten kann:
blog.example.com {
reverse_proxy ghost:2368
}
Das ist die gesamte Datei. Caddys ACME-Challenge braucht Port 80 offen für das öffentliche Internet, auch wenn der eigentliche Traffic zu HTTPS wechselt, also blockiere ihn nicht an der Firewall.
Schritt 4: Den Stack hochfahren
docker compose up -d
docker compose logs -f ghost
Du solltest sehen, wie MySQLs Health-Check besteht, dann loggt Ghost eine Zeile wie Ghost is running in production..., gefolgt von der URL, auf der es läuft. Öffne https://blog.example.com/ghost, um beim Setup-Assistenten zu landen und deinen ersten Admin-Account anzulegen.
Schritt 5: MySQL und das Content-Volume sichern
Ghosts eigentliche Daten leben an zwei Orten: MySQL (Posts, Members, Settings) und dem ghost_content-Volume (Themes, Bilder, Uploads). Sichere beide zusammen, damit eine Wiederherstellung konsistent ist:
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p backups
docker compose exec -T db mysqldump -u root -p"$DATABASE_ROOT_PASSWORD" ghost | gzip > "backups/ghost-db-$STAMP.sql.gz"
docker run --rm -v ghost_content:/content -v "$(pwd)/backups:/backup" alpine \
tar czf "/backup/ghost-content-$STAMP.tar.gz" -C /content .
Führe es aus demselben Verzeichnis wie deine Compose-Datei aus (damit ghost_content auf das richtige Projekt-Volume verweist) und lege es als tägliche Cron-Aufgabe an. Kopiere das backups/-Verzeichnis vom Server selbst weg, ein gecrontes rsync auf eine andere Maschine oder ein kleiner Object-Storage-Bucket reicht; ein Backup, das nur auf dem Server liegt, den es schützen soll, ist keins.
Du solltest nach jedem Lauf zwei neue Dateien in backups/ sehen, und ihre Größe sollte sich über die Zeit grob mit deiner Content-Größe entwickeln, nicht bei null bleiben.
Schritt 6: Upgrades
Ziehe das neue Image und erstelle nur den Ghost-Container neu; MySQL und sein Daten-Volume bleiben unberührt:
docker compose pull ghost
docker compose up -d ghost
Ghost führt seine eigenen Datenbank-Migrationen beim Boot aus, prüfe also die Logs auf eine Zeile, die den Abschluss der Migration bestätigt, bevor du das Upgrade als erledigt betrachtest:
docker compose logs --tail=50 ghost
Mach vor jedem Upgrade ein Backup (Schritt 5), besonders über eine Hauptversion wie 5 auf 6, denn genau das ist die Art von Änderung, bei der eine Migration auf halbem Weg fehlschlagen kann.
Die Abkürzung: mit Railyard
All das oben, die zwei Container, die Verdrahtung dazwischen, der Reverse-Proxy und das Zertifikat, ist das, was Railyards Ghost + MySQL-Vorlage für dich aus dem Vorlagenkatalog einrichtet. Sie betreibt ghost:5.130.6-alpine mit einem mysql:8.0.39-Sidecar: RAILYARD_URL und der MySQL-Connection-String werden automatisch verdrahtet, es gibt also kein database__connection__host von Hand einzutippen, und ein ghost-content-Volume plus ein ghost-db-data-Volume werden angelegt und gesichert, ohne dass du einen Cron-Job selbst schreibst.
Öffne https://app.railyard.run/deploy?stack=ghost-mysql und wähle Ghost + MySQL im Katalog. Deine eigenen SMTP-Zugangsdaten für Mail fügst du weiterhin selbst hinzu (Railyard liefert keinen E-Mail-Transport), aber die Datenbank, die Volumes und das HTTPS-Zertifikat sind schon erledigt. Siehe Deploy-to-Railyard-Button, falls du denselben Ein-Klick-Flow lieber aus deinem eigenen README veröffentlichen willst, und Datenbanken und Backups, wie geplante Backups und Wiederherstellungen funktionieren, sobald es läuft.
Troubleshooting
Port 80 oder 443 ist schon belegt. Etwas anderes (oft eine Standard-Apache- oder Nginx-Installation) ist schon gebunden. sudo ss -tlnp | grep -E ':80|:443' zeigt dir, was, und du stoppst es oder räumst es aus dem Weg, bevor Caddy diese Ports binden kann.
Das Zertifikat wird nie ausgestellt. Caddy-Logs zeigen wiederholte ACME-Fehler. Die fast universelle Ursache ist DNS: dig +short blog.example.com muss die IP des Servers zurückgeben, bevor Caddy die Inhaberschaft bei Let’s Encrypt nachweisen kann. Prüfe auch, dass nichts vorgeschaltet (eine Cloud-Firewall, die Standard-Security-Group eines Anbieters) eingehenden Port 80 blockiert.
Ghost startet ständig neu und die Logs zeigen einen MySQL-Verbindungsfehler. database__connection__host muss dem Compose-Dienstnamen entsprechen (db in der Datei oben), und das Passwort in Ghosts Umgebung muss exakt derselbe String wie MYSQL_PASSWORD im db-Dienst sein. Ein Passwort mit $ oder # muss an beiden Stellen konsistent gequotet oder escaped werden, da sowohl Compose als auch die Shell versuchen, diese Zeichen zu interpretieren.
Permission denied beim Schreiben ins Content-Volume. Das tritt nach dem Wiederherstellen eines Backups auf, das mit tar als Root in ein Volume geschrieben wurde, das Ghosts Container als sein eigener Nutzer besitzen will. Stelle mit docker run --rm -v ghost_content:/content alpine tar xzf /backup/ghost-content-TIMESTAMP.tar.gz -C /content wieder her, statt Dateien mit deinem eigenen Nutzeraccount hineinzukopieren.
MySQL läuft auf einem 1-GB-Server aus dem Speicher. MySQLs Buffer-Pool-Defaults skalieren mit der Box, und auf einem 1-GB-Droplet bleibt zu wenig für Ghost und Node übrig. Deshalb ist der 18-$/Monat-2-GB-Droplet oben das echte Minimum, nicht die günstigste gelistete Größe.
FAQ
Kann ich SQLite statt MySQL nutzen? Ja, Ghosts Standard-CLI-Installation nutzt noch SQLite, und das ist für eine einzelne Low-Traffic-Seite wirklich in Ordnung. MySQL lohnt seine Komplexität, sobald du mehr als eine Ghost-Instanz gegen dieselben Daten laufen lassen willst, oder du von einem Host umziehst, der dir schon Managed MySQL gibt.
Unterstützt das Ghosts Newsletter- und Membership-Features? Ja, sobald SMTP (für transaktionale Mail) und ein Bulk-E-Mail-Anbieter (Ghost integriert speziell Mailgun für den Newsletter-Versand) beide konfiguriert sind. Die zwei sind getrennte Einstellungen in Ghosts Admin, leicht zu verwechseln, weil beides “E-Mail” ist.
Kann ich Ghost ohne Docker betreiben? Ghost unterstützt eine native Node.js-Installation auf Ubuntu, dokumentiert auf docs.ghost.org, mit MySQL als Systempaket installiert statt als Container. Der Docker-Weg in dieser Anleitung ist einfacher zu sichern und zwischen Servern zu bewegen, deshalb haben wir ihn hier genutzt.
Wie ziehe ich eine existierende Ghost-Seite in dieses Setup um? Nutze Ghosts eigenen Export/Import (Settings → Migration options) für Content, oder stelle einen mysqldump direkt in der Datenbank des db-Containers wieder her, wenn du von einer anderen MySQL-gestützten Installation umziehst, und kopiere dann das alte content/images-Verzeichnis in das neue ghost_content-Volume.
Was passiert, wenn der Server neu startet? restart: unless-stopped auf jedem Dienst bedeutet, Docker bringt die Container automatisch wieder hoch, sobald der Docker-Daemon selbst startet, was auf einer systemd-basierten Distribution wie Ubuntu standardmäßig beim Booten passiert.
Beide Wege geben dir eine Ghost-Seite, die du wirklich kontrollierst: der manuelle, wenn du jeden bewegten Teil sehen willst, die Vorlage, wenn du heute lieber schon schreiben willst.
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.