Ghost mit Docker Compose, MySQL, Backup und Traefik als universelles Template betreiben
Wer mehrere Webseiten oder Blogs mit Ghost betreibt, landet schnell bei wiederkehrenden docker-compose.yml-Dateien. Die Struktur ist meistens identisch: Ghost als Anwendung, MySQL als Datenbank, ein Backup-Container und Traefik als Reverse Proxy.
Damit nicht für jede neue Webseite die komplette Compose-Datei angepasst werden muss, lohnt sich ein universelles Template. Die eigentlichen Projektdaten wie Domain, Container-Namen, Datenbank-Zugangsdaten, SMTP-Server und Traefik-Router werden dabei über eine .env-Datei gesteuert.
Der Vorteil:
Die docker-compose.yml bleibt für alle Ghost-Instanzen gleich. Nur die .env unterscheidet sich je Projekt.
Ziel des Setups
Das Setup besteht aus drei Containern:
- app - Ghost-Blog
- db - MySQL-Datenbank
- backup - automatischer MySQL-Dump
Zusätzlich werden zwei Docker-Netzwerke verwendet:
- traefik - externes Netzwerk für den Reverse Proxy
- backend - internes Netzwerk zwischen Ghost, MySQL und Backup
Traefik übernimmt dabei die automatische Veröffentlichung der Webseite per HTTPS.
Verzeichnisstruktur
Eine mögliche Struktur auf dem Server sieht so aus:
/opt/docker/apps/
└── www.meineseite.de/
├── docker-compose.yml
└── .env
Die eigentlichen Daten der jeweiligen Webseite liegen zum Beispiel unter:
/opt/docker/volumes/www.meineseite.de/
├── ghost/
├── mysql/
└── backup/
Damit sind Konfiguration, Anwendungsdaten, Datenbank und Backups sauber voneinander getrennt.
Die universelle docker-compose.yml
Die folgende docker-compose.yml ist so aufgebaut, dass sie für unterschiedliche Ghost-Projekte wiederverwendet werden kann.
services:
app:
image: ${GHOST_IMAGE}
container_name: ${PROJECT_NAME}_ghost
restart: ${RESTART_POLICY}
depends_on:
- db
environment:
url: ${GHOST_URL}
database__client: mysql
database__connection__host: db
database__connection__user: ${MYSQL_USER}
database__connection__password: ${MYSQL_PASSWORD}
database__connection__database: ${MYSQL_DATABASE}
mail__transport: SMTP
mail__from: ${SMTP_FROM}
mail__options__service: SMTP
mail__options__host: ${SMTP_HOST}
mail__options__port: ${SMTP_PORT}
mail__options__auth__user: ${SMTP_USER}
mail__options__auth__pass: ${SMTP_PASSWORD}
volumes:
- ${DATA_PATH}/ghost:/var/lib/ghost/content
networks:
- traefik
- backend
labels:
- "traefik.enable=true"
- "traefik.docker.network=${TRAEFIK_NETWORK}"
# HTTPS Router
- "traefik.http.routers.${TRAEFIK_ROUTER_NAME}-secure.entrypoints=${TRAEFIK_HTTPS_ENTRYPOINT}"
- "traefik.http.routers.${TRAEFIK_ROUTER_NAME}-secure.rule=${TRAEFIK_HOST_RULE}"
- "traefik.http.routers.${TRAEFIK_ROUTER_NAME}-secure.tls=true"
- "traefik.http.routers.${TRAEFIK_ROUTER_NAME}-secure.tls.certresolver=${TRAEFIK_CERTRESOLVER}"
- "traefik.http.routers.${TRAEFIK_ROUTER_NAME}-secure.service=${TRAEFIK_SERVICE_NAME}"
# HTTP zu HTTPS Redirect
- "traefik.http.routers.${TRAEFIK_ROUTER_NAME}.entrypoints=${TRAEFIK_HTTP_ENTRYPOINT}"
- "traefik.http.routers.${TRAEFIK_ROUTER_NAME}.rule=${TRAEFIK_HOST_RULE}"
- "traefik.http.routers.${TRAEFIK_ROUTER_NAME}.middlewares=${TRAEFIK_REDIRECT_MIDDLEWARE}"
# Service Definition
- "traefik.http.services.${TRAEFIK_SERVICE_NAME}.loadbalancer.server.port=${GHOST_INTERNAL_PORT}"
# Middleware
- "traefik.http.middlewares.${TRAEFIK_REDIRECT_MIDDLEWARE}.redirectscheme.scheme=https"
- "traefik.http.middlewares.${TRAEFIK_REDIRECT_MIDDLEWARE}.redirectscheme.permanent=true"
db:
image: ${MYSQL_IMAGE}
container_name: ${PROJECT_NAME}_mysql
restart: ${RESTART_POLICY}
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
volumes:
- ${DATA_PATH}/mysql:/var/lib/mysql
- ${DATA_PATH}/backup:/backup
networks:
- backend
backup:
image: ${MYSQL_BACKUP_IMAGE}
container_name: ${PROJECT_NAME}_backup
user: "0"
restart: ${RESTART_POLICY}
depends_on:
- db
networks:
- backend
environment:
- DB_DUMP_TARGET=/backup
- DB_USER=${BACKUP_DB_USER}
- DB_PASS=${BACKUP_DB_PASSWORD}
- DB_SERVER=db
- DB_PORT=3306
- DB_DUMP_BEGIN=${BACKUP_BEGIN}
- DB_DUMP_FREQ=${BACKUP_FREQ}
command:
- dump
volumes:
- ${DATA_PATH}/backup:/backup
networks:
traefik:
name: ${TRAEFIK_NETWORK}
external: true
backend:
name: ${PROJECT_NAME}_backend
external: false
Die passende .env-Datei
Die eigentliche Steuerung erfolgt über die .env-Datei. Hier werden Projektname, Domain, Datenbankzugänge, SMTP-Parameter und Traefik-Werte definiert.
# =============================================================================
# Projekt
# =============================================================================
PROJECT_NAME=www-meineseite-de
DATA_PATH=/opt/docker/volumes/www.meineseite.de
RESTART_POLICY=always
# =============================================================================
# Ghost
# =============================================================================
GHOST_IMAGE=ghost:latest
GHOST_URL=https://www.meineseite.de
GHOST_INTERNAL_PORT=2368
# =============================================================================
# MySQL
# =============================================================================
MYSQL_IMAGE=mysql:8.0
MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD
MYSQL_USER=ghost
MYSQL_PASSWORD=CHANGE_ME_GHOST_PASSWORD
MYSQL_DATABASE=ghostdb
# =============================================================================
# Mail / SMTP
# =============================================================================
SMTP_FROM=noreply@meineseite.de
SMTP_HOST=mail.meineseite.de
SMTP_PORT=587
SMTP_USER=noreply@meineseite.de
SMTP_PASSWORD=CHANGE_ME_SMTP_PASSWORD
# =============================================================================
# Backup
# =============================================================================
MYSQL_BACKUP_IMAGE=databack/mysql-backup
BACKUP_DB_USER=root
BACKUP_DB_PASSWORD=CHANGE_ME_ROOT_PASSWORD
# Start des ersten Backups:
# +1 = ca. 1 Minute nach Containerstart
BACKUP_BEGIN=+1
# Backup-Frequenz in Minuten:
# 1440 = täglich
BACKUP_FREQ=1440
# =============================================================================
# Traefik
# =============================================================================
TRAEFIK_NETWORK=traefik
TRAEFIK_ROUTER_NAME=ghost-meineseite
TRAEFIK_SERVICE_NAME=ghost-meineseite-svc
TRAEFIK_HTTP_ENTRYPOINT=web
TRAEFIK_HTTPS_ENTRYPOINT=websecure
TRAEFIK_CERTRESOLVER=hetzner
TRAEFIK_REDIRECT_MIDDLEWARE=redirect-to-https
TRAEFIK_HOST_RULE=Host(`www.meineseite.de`) || Host(`meineseite.de`)
Wichtige Variablen erklärt
PROJECT_NAME
Der Projektname wird für Container- und Netzwerknamen verwendet.
PROJECT_NAME=www-meineseite-de
Daraus entstehen zum Beispiel:
www-meineseite-de_ghost
www-meineseite-de_mysql
www-meineseite-de_backup
www-meineseite-de_backend
Ich verwende hier bewusst keine Punkte im Projektnamen. Das macht die Namen etwas robuster und besser lesbar.
DATA_PATH
Über DATA_PATH wird der Speicherort der persistenten Daten gesteuert.
DATA_PATH=/opt/docker/volumes/www.meineseite.de
Daraus ergeben sich automatisch:
/opt/docker/volumes/www.meineseite.de/ghost
/opt/docker/volumes/www.meineseite.de/mysql
/opt/docker/volumes/www.meineseite.de/backup
GHOST_URL
Diese Variable ist für Ghost besonders wichtig:
GHOST_URL=https://www.meineseite.de
Ghost verwendet diese URL intern für Links, Weiterleitungen, Assets und die Admin-Oberfläche. Der Wert sollte daher exakt der später genutzten öffentlichen URL entsprechen.
Datenbankzugang
Ghost verbindet sich intern über den Servicenamen db mit MySQL:
database__connection__host: db
Der Hostname muss also nicht angepasst werden. Variabel sind nur Benutzer, Passwort und Datenbankname:
MYSQL_USER=ghost
MYSQL_PASSWORD=CHANGE_ME_GHOST_PASSWORD
MYSQL_DATABASE=ghostdb
Der Backup-Container nutzt in diesem Beispiel den MySQL-Root-Benutzer:
BACKUP_DB_USER=root
BACKUP_DB_PASSWORD=CHANGE_ME_ROOT_PASSWORD
Das ist einfach und funktioniert zuverlässig. Wer es restriktiver möchte, kann später einen eigenen Backup-Benutzer mit passenden Rechten anlegen.
Traefik-Router
Die Labels in der Compose-Datei werden ebenfalls über Variablen gesteuert.
Für die produktive Webseite ist diese Regel entscheidend:
TRAEFIK_HOST_RULE=Host(`www.meineseite.de`) || Host(`meineseite.de`)
Damit reagiert der Container sowohl auf:
https://www.meineseite.de
https://meineseite.de
Der HTTPS-Router verwendet den konfigurierten Zertifikatsresolver:
TRAEFIK_CERTRESOLVER=hetzner
Das setzt voraus, dass Traefik bereits entsprechend eingerichtet ist und der externe Docker-Network-Name traefik existiert.
Voraussetzung: Traefik-Netzwerk
Da das Netzwerk traefik als extern definiert ist, muss es bereits existieren.
Prüfen kann man das mit:
docker network ls
Falls es noch nicht existiert, kann es so erstellt werden:
docker network create traefik
Das interne Backend-Netzwerk wird hingegen automatisch durch Docker Compose erzeugt.
Container starten
Das Projekt wird wie gewohnt gestartet:
docker compose --env-file .env up -d
Anschließend kann der Status geprüft werden:
docker compose ps
Die Logs der einzelnen Dienste lassen sich so anzeigen:
docker compose logs -f app
docker compose logs -f db
docker compose logs -f backup
Beispiel: zweite Ghost-Instanz
Der große Vorteil dieses Templates zeigt sich bei einer zweiten Webseite. Die docker-compose.yml bleibt unverändert. Nur die .env wird angepasst.
Beispiel:
PROJECT_NAME=blog-example-com
DATA_PATH=/opt/docker/volumes/blog.example.com
GHOST_URL=https://blog.example.com
MYSQL_ROOT_PASSWORD=NEUES_ROOT_PASSWORT
MYSQL_PASSWORD=NEUES_GHOST_DB_PASSWORT
SMTP_FROM=noreply@example.com
SMTP_USER=noreply@example.com
SMTP_PASSWORD=NEUES_SMTP_PASSWORT
TRAEFIK_ROUTER_NAME=ghost-example
TRAEFIK_SERVICE_NAME=ghost-example-svc
TRAEFIK_HOST_RULE=Host(`blog.example.com`)
Damit lässt sich mit derselben Compose-Datei eine weitere Ghost-Instanz betreiben, ohne die eigentliche Infrastrukturdefinition anzufassen.
Backup-Konzept
Der Container databack/mysql-backup erstellt regelmäßig Dumps der MySQL-Datenbank.
Im Beispiel startet das erste Backup kurz nach dem Containerstart:
BACKUP_BEGIN=+1
Die Frequenz ist auf täglich gesetzt:
BACKUP_FREQ=1440
Die Angabe erfolgt in Minuten.1440 Minuten entsprechen 24 Stunden.
Die Backups landen im Verzeichnis:
/opt/docker/volumes/www.meineseite.de/backup
Ein Blick in das Verzeichnis zeigt, ob Dumps erzeugt werden:
ls -lah /opt/docker/volumes/www.meineseite.de/backup
Sicherheitshinweise
Die .env enthält sensible Daten wie Datenbank- und SMTP-Passwörter. Sie sollte daher nicht öffentlich in einem Git-Repository landen.
Sinnvoll ist eine .gitignore:
.env
Zusätzlich sollten produktive Passwörter ausreichend stark und eindeutig sein:
MYSQL_ROOT_PASSWORD=P;1kM~<K!/]J*~\vhg\=vb*WHhmq-}^,
MYSQL_PASSWORD=CdOsaWG?]1VqQxI&5@`}N\YJ+ik}vT[l
SMTP_PASSWORD=dTng+wo"B@-RkHXz?_`Elh]3ah45_i?O
Starke Passwörter können unter Linux z. B. mit dem Tool pwgenerzeugt werden:
❯ pwgen -s 32 -y
}hxL6nM+lOREc4~iPtF=#b@Ec6:L#.+y ,)ukp<bG]BpaH$7Ld\'My2oTw6y[#%G]
vsrY09B^O9H\/7v4-/b.&Y*XKb%/L^_, g0P]=eU6)U:MFp/$U![C7+$jfWd)b&TF
4Qoit*VBb/Fxxd'(D<M~Hps6:3yR5uz> $!^@SY0HyB|4fu@JgQMb(0^3ZTq/y'po
6C<88B)PGYF0b_#(9/d,L]R_^_|wr|U] *RHyMI(+tnfe%w@Ylm&&A3L`5Z70&kIB
>B%W,fk'oZ?g=f/t:B-!JP,4:Ku?&Z-C /|#-c5']dmmpE"Y7Rr6@46DCGA|52#3s
H7($(+0g[AI,QO91PtN~0t?5<cbXiTpU :cdBiY){s0jW0~#ggH0w#;8k3&EduUFq
>mRZXUXAW!z-\:yQ59Hi@k`$m}{DEtc. T$Ht(w>>+<w*UFQ=,P^3zMRG3A%44gKe
6+]u@wy)v{[@lp[GK%u3Tu=9{&,]4rKx AGcFZoAaWV&Q#Y;G5CM)3F!&sgpi~0h:
H6Y?rA.=3N!XX4|kDU{I#\(['P_'0/'` w?H,TENf8+&q12b:jTw9>_"zH#i*g:.q
gjD^m&xdu9Qv|j-3@PkY+;AENP@x"|_: P2WUPHcBqyBltKs[wm(ql[U0_<I*~O\(
^-dE;b972Bdad>2#gA&aJ44&UAJLZ1QS wU}W1IA/7JJ.qWKQTc<udoW:(91;Kel\
38YACxV1R(RggL<tmbN{;#k~~LFCHr@O tj\\y<+mu:._9^~t*RT"EG!>Ljc+kP:-
_l@*KaI/O""sVW4jZH<1,]aoGQ-s6DlD vl#'{X}>k`\9f`AjBY`8i?n>AvosVFFw
lw+(Lc</-zb{C9O7z+m5&$%5-{mebgwo {Pf?o|YG*534$d*ywdtWKRS,A%+L|!jr
=_HGY"tW~we$_+e@(!{M/-|G3mH))amO JFMK8kdps0\wMuN)Qzf?e(lI\Z</O@Pg
H9?HZ%4?%%6E#U%e>aMwKdRec?d{g!gz vGxkb"whcr|B7$F8=DKGC*?.9@Gz8rY2
2=~t%zM'e\[/Xpg<BHm!_/7O>#N]=Po. ~R`!r'|hZ_`;rHsDR+AS7"kjUZq(&Wnu
1M5+4%xsTb-n&"I|]@a\n!-4U99/ykb/ 4eHYFG.u(Q}$Q4cw]<x<JOFrT2k@6g0_
=pDS.|3>G|%15A%d[Fy(,&8{_&eiMraO /Zy3q"j+by>N];3%[I6Z.@YnW\t[!wp9
$#i9q=xp'C)1yHH|d%QR|1RkPyMdK_>k ^fWHX5=Jli0ds1aNO#]IXxG9XA0F5MKB
Für einfache Setups kann der Backup-Container mit dem MySQL-Root-Benutzer arbeiten. Für ein härter abgesichertes Setup wäre ein dedizierter Backup-Benutzer die bessere Variante.
Fazit
Mit diesem Template lässt sich Ghost sehr sauber und wiederverwendbar per Docker Compose betreiben. Die docker-compose.yml beschreibt nur noch die technische Struktur, während alle projektspezifischen Werte in der .env liegen.
Das macht den Betrieb mehrerer Ghost-Instanzen deutlich einfacher:
- weniger Copy-and-Paste
- weniger Fehler bei neuen Projekten
- einheitliche Struktur
- klare Trennung von Template und Konfiguration
- einfache Anpassung von Domain, SMTP, Datenbank und Traefik
Für kleine und mittlere Blog-Setups ist das eine pragmatische und gut wartbare Lösung.