Konfigurationsdateien

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: typo3

Der 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-env gibt dem Block einen Namen, *typo3-env setzt ihn an anderer Stelle ein. <<: *typo3-env mischt ihn in eine Liste ein, die noch eigene Einträge bekommt.

Die Schreibweisen für Variablen:

AusdruckBedeutung
${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/var

Nach 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.php aus der Installation und die Site-Konfiguration
  • public/fileadmin: hochgeladene Dateien
  • public/typo3temp: generierte Bilder und Assets
  • var: 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-stopped startet 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-data hält die Datenbankdateien. Die zweite Zeile ist ein Bind-Mount: Sie bindet die Datei typo3.cnf aus dem Projekt direkt ein, :ro heißt nur lesend. Sie setzt utf8mb4 und InnoDB-Einstellungen.
  • Healthcheck: Das mitgelieferte healthcheck.sh prüft, ob Verbindungen möglich sind und InnoDB bereit ist. start_period gibt 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: 60s

Elasticsearch 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 aus ELASTICSEARCH_PASSWORD. TLS ist aus, weil der Dienst keinen Port veröffentlicht: Erreichbar ist er nur im internen Docker-Netz, für php und scheduler.
  • 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_OPTS setzt den Java-Heap, Standard 512 MB. Der Container braucht etwa das Doppelte.
  • Volume: es-data hält den Index. Er lässt sich jederzeit aus der Datenbank neu aufbauen und gehört deshalb nicht ins Backup.
  • Healthcheck: curl fragt 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: 120s
  • build baut das Dockerfile im Projektverzeichnis, und zwar die Stage app. image gibt dem Ergebnis einen festen Namen. Den nutzen der scheduler und die Backup-Skripte.
  • depends_on mit service_healthy: Der Container startet erst, wenn der Healthcheck der Datenbank erfolgreich ist. Bloß „gestartet“ reicht nicht. Bei elasticsearch genügt dagegen service_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 keine settings.php gibt.
  • Healthcheck: typo3-healthcheck aus dem Image. start_period: 120s lä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.
  • ports veröffentlicht Port 80 des Containers auf dem Rechner, standardmäßig als Port 8088. HTTP_PORT in der .env ändert ihn. Für einen Server hinter einem Reverse Proxy empfiehlt sich 127.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. :ro verhindert, dass nginx dort etwas ändert.
  • Healthcheck: wget ruft /fpm-ping auf. 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-volumes

Der 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 php und baut nichts selbst.
  • entrypoint ersetzt den TYPO3-Entrypoint durch eine Endlosschleife: scheduler:run, dann 60 Sekunden warten. Installation und Schema-Updates übernimmt allein der php-Container.
  • Jeder 15. Durchlauf, also alle 15 Minuten, gleicht zusätzlich den Suchindex mit search:index ab. 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 $out nicht als eigene Variablen auswertet.
  • user: www-data lä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──▶  scheduler

So bekommt nginx nie Anfragen, bevor TYPO3 installiert ist, und der Scheduler startet nie auf einer halb eingerichteten Datenbank.