docker-compose.yml: die Container
Fünf Services, gemeinsame Variablen und Volumes, Healthchecks und die Startreihenfolge.
Überblick
docker-compose.yml beschreibt, welche Container laufen und wie sie zusammenhängen. Docker Compose erzeugt daraus Container, ein gemeinsames Netzwerk und die Volumes. Im Netzwerk erreichen sich die Container über ihren Service-Namen, etwa db oder php.
Alle Werte, die sich je Installation unterscheiden, kommen als ${VARIABLE} aus der .env im Projektverzeichnis. Die Datei selbst bleibt dadurch für alle Installationen gleich.
Projektname
name: typo3Der Projektname ist das Präfix für Container, Netzwerk und Volumes, zum Beispiel typo3-php-1 und typo3_db-data. Die Umgebungsvariable COMPOSE_PROJECT_NAME überschreibt ihn. So läuft eine zweite, völlig getrennte Instanz neben der ersten, etwa zum Testen einer Wiederherstellung:
COMPOSE_PROJECT_NAME=typo3test HTTP_PORT=8089 docker compose up -d
Gemeinsame Umgebungsvariablen
x-typo3-env: &typo3-env
TYPO3_CONTEXT: ${TYPO3_CONTEXT:-Production}
TYPO3_DB_HOST: db
TYPO3_DB_PORT: 3306
TYPO3_DB_DBNAME: ${DB_NAME:-typo3}
TYPO3_DB_USERNAME: ${DB_USER:-typo3}
TYPO3_DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD fehlt in .env}
TYPO3_TRUSTED_HOSTS_PATTERN: ${TYPO3_TRUSTED_HOSTS_PATTERN:-typo3-v14\.localhost(:\d+)?}
TYPO3_REVERSE_PROXY_IP: ${TYPO3_REVERSE_PROXY_IP:-}
TYPO3_SMTP_SERVER: ${TYPO3_SMTP_SERVER:-}
…
TYPO3_OIDC_ISSUER: ${TYPO3_OIDC_ISSUER:-}
TYPO3_OIDC_CLIENT_ID: ${TYPO3_OIDC_CLIENT_ID:-}
TYPO3_OIDC_CLIENT_SECRET: ${TYPO3_OIDC_CLIENT_SECRET:-}
TYPO3_OIDC_SCOPES: ${TYPO3_OIDC_SCOPES:-openid,profile,email}
TYPO3_OIDC_DEBUG: ${TYPO3_OIDC_DEBUG:-0}
ELASTICSEARCH_URL: http://elasticsearch:9200
ELASTICSEARCH_USERNAME: elastic
ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD:?ELASTICSEARCH_PASSWORD fehlt in .env}
ELASTICSEARCH_INDEX_PREFIX: ${ELASTICSEARCH_INDEX_PREFIX:-typo3}php und scheduler brauchen dieselben Variablen. Damit sie nur einmal dastehen, nutzt die Datei zwei Mittel:
- Erweiterungsfelder: Schlüssel, die mit
x-beginnen, ignoriert Compose. Sie dienen nur als Ablage. - YAML-Anker:
&typo3-envgibt dem Block einen Namen,*typo3-envsetzt ihn an anderer Stelle ein.<<: *typo3-envmischt ihn in eine Liste ein, die noch eigene Einträge bekommt.
Die Schreibweisen für Variablen:
| Ausdruck | Bedeutung |
|---|---|
${VAR:-wert} | Wert aus der .env, sonst wert als Standard |
${VAR:-} | Wert aus der .env, sonst leer, etwa um OIDC oder SMTP abzuschalten |
${VAR:?text} | Pflichtwert. Fehlt er, bricht Compose mit text ab, statt mit leerem Passwort zu starten. |
TYPO3_DB_HOST: db ist der Service-Name der Datenbank im Docker-Netz, ebenso elasticsearch in ELASTICSEARCH_URL. Ausgewertet werden alle Variablen in additional.php.
Gemeinsame Volumes
x-typo3-volumes: &typo3-volumes
- typo3-config:/var/www/html/config
- typo3-fileadmin:/var/www/html/public/fileadmin
- typo3-temp:/var/www/html/public/typo3temp
- typo3-var:/var/www/html/varNach demselben Muster steht hier die Liste der Volumes für php und scheduler. Jede Zeile legt ein benanntes Volume über einen Ordner im Container. Der Inhalt bleibt erhalten, wenn Container neu erstellt werden:
config:settings.phpaus der Installation und die Site-Konfigurationpublic/fileadmin: hochgeladene Dateienpublic/typo3temp: generierte Bilder und Assetsvar: Caches, Logs, Sprachpakete
Beim allerersten Start ist ein Volume leer. Docker füllt es dann mit dem Inhalt, den das Image an dieser Stelle hat.
Service db
db:
image: mariadb:11.8
restart: unless-stopped
environment:
MARIADB_DATABASE: ${DB_NAME:-typo3}
MARIADB_USER: ${DB_USER:-typo3}
MARIADB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD fehlt in .env}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD fehlt in .env}
volumes:
- db-data:/var/lib/mysql
- ./config/mariadb/typo3.cnf:/etc/mysql/conf.d/typo3.cnf:ro
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s- Image statt Build: Das offizielle MariaDB-Image wird unverändert verwendet.
restart: unless-stoppedstartet den Container nach Abstürzen und nach einem Neustart des Rechners wieder, außer man hat ihn bewusst gestoppt. Das gilt für alle fünf Services.- Umgebung: Die
MARIADB_*-Variablen legen Datenbank, Benutzer und Passwörter an. Sie wirken nur beim ersten Start mit leerem Volume. Ein späterer Passwortwechsel in der.envändert die bestehende Datenbank nicht. - Volumes:
db-datahält die Datenbankdateien. Die zweite Zeile ist ein Bind-Mount: Sie bindet die Dateitypo3.cnfaus dem Projekt direkt ein,:roheißt nur lesend. Sie setzt utf8mb4 und InnoDB-Einstellungen. - Healthcheck: Das mitgelieferte
healthcheck.shprüft, ob Verbindungen möglich sind und InnoDB bereit ist.start_periodgibt der Datenbank 30 Sekunden Anlaufzeit, bevor Fehlschläge zählen.
Service elasticsearch
elasticsearch:
image: elasticsearch:9.5.3
restart: unless-stopped
environment:
discovery.type: single-node
cluster.name: typo3
xpack.security.enabled: "true"
xpack.security.http.ssl.enabled: "false"
xpack.security.transport.ssl.enabled: "false"
ELASTIC_PASSWORD: ${ELASTICSEARCH_PASSWORD:?ELASTICSEARCH_PASSWORD fehlt in .env}
ingest.geoip.downloader.enabled: "false"
xpack.ml.enabled: "false"
ES_JAVA_OPTS: ${ELASTICSEARCH_JAVA_OPTS:--Xms512m -Xmx512m}
volumes:
- es-data:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD-SHELL", "curl -fsS -u \"elastic:$${ELASTIC_PASSWORD}\" 'http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=5s' >/dev/null"]
interval: 10s
timeout: 10s
retries: 12
start_period: 60sElasticsearch hält den Suchindex der Website. TYPO3 schreibt Seiten und Inhalte hinein, die Suchseite fragt ihn ab.
- Image: das offizielle Image mit fester Version. Einstellungen mit Punkt im Namen, etwa
discovery.type, übernimmt Elasticsearch direkt aus der Umgebung. discovery.type: single-node: ein einzelner Knoten ohne Cluster-Suche. Die Indizes legt TYPO3 deshalb ohne Replikate an, sonst bliebe ihr Status dauerhaft „gelb“.- Sicherheit: Die Anmeldung ist eingeschaltet, Benutzer
elastic, Passwort ausELASTICSEARCH_PASSWORD. TLS ist aus, weil der Dienst keinen Port veröffentlicht: Erreichbar ist er nur im internen Docker-Netz, fürphpundscheduler. - Datenschutz:
ingest.geoip.downloader.enabled: "false"verhindert, dass Elasticsearch regelmäßig GeoIP-Datenbanken bei Elastic herunterlädt. Machine Learning wird nicht gebraucht und ist ebenfalls aus. - Speicher:
ES_JAVA_OPTSsetzt den Java-Heap, Standard 512 MB. Der Container braucht etwa das Doppelte. - Volume:
es-datahält den Index. Er lässt sich jederzeit aus der Datenbank neu aufbauen und gehört deshalb nicht ins Backup. - Healthcheck:
curlfragt den Cluster-Status ab und wartet bis zu fünf Sekunden auf mindestens „gelb“.$$verhindert, dass Compose die Variable selbst ersetzt. So setzt erst die Shell im Container das Passwort ein.
Service php
php:
build:
context: .
target: app
image: typo3-appliance-php:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
elasticsearch:
condition: service_started
environment:
<<: *typo3-env
TYPO3_SETUP_ADMIN_USERNAME: ${TYPO3_ADMIN_USERNAME:-admin}
TYPO3_SETUP_ADMIN_PASSWORD: ${TYPO3_ADMIN_PASSWORD:?TYPO3_ADMIN_PASSWORD fehlt in .env}
TYPO3_SETUP_ADMIN_EMAIL: ${TYPO3_ADMIN_EMAIL:-}
TYPO3_PROJECT_NAME: ${TYPO3_PROJECT_NAME:-TYPO3}
TYPO3_SITE_URL: ${TYPO3_SITE_URL:-http://typo3-v14.localhost:8088/}
volumes: *typo3-volumes
healthcheck:
test: ["CMD", "typo3-healthcheck"]
interval: 10s
timeout: 5s
retries: 5
start_period: 120sbuildbaut dasDockerfileim Projektverzeichnis, und zwar die Stageapp.imagegibt dem Ergebnis einen festen Namen. Den nutzen derschedulerund die Backup-Skripte.depends_onmitservice_healthy: Der Container startet erst, wenn der Healthcheck der Datenbank erfolgreich ist. Bloß „gestartet“ reicht nicht. Beielasticsearchgenügt dagegenservice_started: TYPO3 soll auch ohne Suche laufen. Der Entrypoint wartet höchstens etwa eine Minute auf Elasticsearch und gleicht dann den Suchindex ab.- Umgebung: alle gemeinsamen Variablen und zusätzlich die Werte für die Erstinstallation.
TYPO3_SETUP_*, Projektname und Site-URL wertet der Entrypoint nur aus, solange es noch keinesettings.phpgibt. - Healthcheck:
typo3-healthcheckaus dem Image.start_period: 120slässt der Erstinstallation genug Zeit, inklusive Download des Sprachpakets.
Der Container veröffentlicht keinen Port. PHP-FPM ist nur im internen Netz für nginx erreichbar.
Service web
web:
build:
context: .
target: web
image: typo3-appliance-web:latest
restart: unless-stopped
depends_on:
php:
condition: service_healthy
ports:
- "${HTTP_PORT:-8088}:80"
volumes:
- typo3-fileadmin:/var/www/html/public/fileadmin:ro
- typo3-temp:/var/www/html/public/typo3temp:ro
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/fpm-ping"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s- Build: dasselbe Dockerfile, diesmal die Stage
web. portsveröffentlicht Port 80 des Containers auf dem Rechner, standardmäßig als Port 8088.HTTP_PORTin der.envändert ihn. Für einen Server hinter einem Reverse Proxy empfiehlt sich127.0.0.1:8088:80, dann ist der Port nur lokal erreichbar.- Volumes, nur lesend: nginx liefert Uploads und generierte Bilder direkt aus und bekommt dafür dieselben Volumes wie
php.:roverhindert, dass nginx dort etwas ändert. - Healthcheck:
wgetruft/fpm-pingauf. nginx reicht die Anfrage an den Ping-Endpunkt von PHP-FPM weiter. Ein erfolgreicher Check beweist also die ganze Kette nginx → PHP-FPM. Der Endpunkt ist nur von 127.0.0.1 erreichbar, also nicht von außen.
Service scheduler
scheduler:
image: typo3-appliance-php:latest
restart: unless-stopped
depends_on:
php:
condition: service_healthy
user: www-data
entrypoint:
- sh
- -c
- |
i=0
while true; do
vendor/bin/typo3 scheduler:run
if [ $$((i % 15)) -eq 0 ]; then
out=$$(vendor/bin/typo3 search:index 2>&1) || echo "$$out"
fi
i=$$((i + 1))
sleep 60
done
environment: *typo3-env
volumes: *typo3-volumesDer TYPO3-Scheduler erledigt wiederkehrende Aufgaben, etwa Aufräumarbeiten, Index-Updates oder Berichte. Er braucht einen Taktgeber. Statt eines Cron-Dienstes übernimmt das ein eigener Container:
- Er verwendet das fertige Image von
phpund baut nichts selbst. entrypointersetzt den TYPO3-Entrypoint durch eine Endlosschleife:scheduler:run, dann 60 Sekunden warten. Installation und Schema-Updates übernimmt allein derphp-Container.- Jeder 15. Durchlauf, also alle 15 Minuten, gleicht zusätzlich den Suchindex mit
search:indexab. Das holt Änderungen nach, die beim Speichern nicht ankamen, etwa während Elasticsearch neu startete. Die Ausgabe erscheint nur bei einem Fehler im Log des Containers. - Der Skriptblock steht als YAML-Liste mit
|(mehrzeiliger Text).$$schreibt ein wörtliches$, damit Compose$((…))und$outnicht als eigene Variablen auswertet. user: www-datalässt die Aufgaben unter demselben Benutzer laufen wie PHP-FPM. So gehören erzeugte Dateien dem richtigen Benutzer.- Dieselben Variablen und Volumes wie
php, damit beide auf denselben Daten arbeiten.
Volumes und Startreihenfolge
volumes:
db-data:
es-data:
typo3-config:
typo3-fileadmin:
typo3-temp:
typo3-var:Der Abschnitt volumes meldet die benannten Volumes an. Ohne weitere Angaben verwaltet Docker sie selbst. Sie überleben docker compose down und Rebuilds. Erst docker compose down -v löscht sie.
Aus den depends_on-Bedingungen ergibt sich die Startreihenfolge. elasticsearch startet parallel zur Datenbank. php wartet nur darauf, dass der Container läuft, nicht auf dessen Healthcheck:
db ──healthy──▶ php ──healthy──▶ web
elasticsearch ──started──▶ php └─healthy──▶ schedulerSo bekommt nginx nie Anfragen, bevor TYPO3 installiert ist, und der Scheduler startet nie auf einer halb eingerichteten Datenbank.