
Contao 5 Performance, Caching & Zero-Downtime Deployment

Unser Strandkorb-Buchungssystem ist funktional komplett. Wir haben Datenbankmodelle entworfen, Frontend-Module programmiert, Backend-Routen abgesichert und sogar eine API für externe Apps bereitgestellt. Lokal, auf unserem eigenen Rechner, fühlt sich alles fehlerfrei an und auch die Contao 5 Performance scheint extrem schnell zu sein.
Doch zwischen einem Modul, das "auf meinem Rechner funktioniert", und einer professionellen Open-Source-Erweiterung, die in einer Live-Umgebung (Production) mit Tausenden von Besuchern standhält, liegt ein massiver Unterschied.
Bevor wir unseren Code also auf GitHub oder Packagist der Welt präsentieren, müssen wir ihn einem rigorosen Feinschliff unterziehen. Zwei kritische Baustellen haben wir in der bisherigen Entwicklung nämlich sträflich vernachlässigt:
Lokalisierung (i18n): Wenn wir einen Fehler ausgeben, steht in unserem Code aktuell
throw new \Exception("Der Strandkorb ist bereits gebucht.");. Was passiert, wenn eine spanische oder englische Agentur unser Modul installiert? Sie können unsere deutschen Sätze nicht gebrauchen.Performance (Caching): Bei jedem Seitenaufruf fragen wir die Datenbank nach freien Strandkörben ab und rendern das Twig-Template komplett neu. Wenn eine Marketing-Kampagne startet und 5.000 Besucher gleichzeitig auf die Startseite stürmen, wird unser Server unter der Last der Datenbankabfragen kapitulieren.
In diesem Kapitel verwandeln wir unser Modul in ein Enterprise-Grade-Paket.
Der Paradigmenwechsel in Contao 5
Wenn du früher (in Contao 3 oder 4) Erweiterungen geschrieben hast, kennst du für Übersetzungen wahrscheinlich noch die endlosen PHP-Arrays in Dateien wie system/modules/mein_modul/languages/de/default.php (z. B. $GLOBALS['TL_LANG']['MSC']['my_text'] = 'Hallo';). Für das Caching gab es einfache Checkboxen in den Seitenlayouts.
Mit Contao 5 und der vollständigen Integration der Symfony-Architektur hat sich das radikal modernisiert:
Der Symfony Translator: PHP-Arrays als Sprachdateien sind ein Auslaufmodell. Wir nutzen ab sofort den Branchenstandard XLIFF (
.xlf-Dateien). Diese XML-basierten Dateien können von professionellen Übersetzungsbüros und Software-Tools nahtlos verarbeitet werden. Contao 5 lädt diese Übersetzungen extrem performant direkt in den kompilierten Container.HTTP Caching & Reverse Proxies: Contao nutzt nicht mehr nur einfache interne Caches, sondern implementiert das mächtige HTTP-Caching-Protokoll. In Kombination mit dem FOSHttpCacheBundle und dem Symfony Reverse Proxy (oder Systemen wie Varnish) können wir Seiten im Millisekundenbereich ausliefern, ohne dass PHP überhaupt gestartet werden muss.
Die Roadmap für den Feinschliff
Wir werden in den kommenden Schritten unser Bundle systematisch auf Hochglanz polieren:
Wir extrahieren alle hartcodierten Texte aus unseren Controllern und Twig-Templates.
Wir erstellen strukturierte XLIFF-Dateien für Deutsch und Englisch.
Wir integrieren den
TranslatorInterfaceService in unsere PHP-Klassen.Wir tauchen tief in die Welt des HTTP-Cachings (Shared Cache vs. Private Cache) ein.
Wir versehen unsere Frontend-Module mit intelligenten Cache-Headern (TTL, Max-Age).
Wir lernen das Konzept des "Cache Taggings" kennen, um veraltete Daten (z. B. wenn ein Korb gebucht wird) automatisch blitzschnell aus dem Cache zu werfen.
Machen wir Schluss mit provisorischem Code. Im nächsten Abschnitt beginnen wir mit der Internationalisierung (i18n) und erstellen unsere erste XLIFF-Datei.

Lokalisierung: Weg mit hartcodierten Texten
Jeder Entwickler kennt die Versuchung: Man baut schnell ein neues Feature und schreibt die Erfolgsmeldung oder den Button-Text direkt in den PHP-Code oder das Twig-Template.
<button>Strandkorb jetzt buchen</button>In einer lokalen Testumgebung ist das kein Problem. Sobald dein Modul aber als Open-Source-Paket veröffentlicht wird, ist diese Vorgehensweise ein absolutes No-Go. Eine niederländische Agentur, die dein Modul für einen Strand in Zandvoort einsetzt, kann mit "Strandkorb jetzt buchen" nichts anfangen.
Wir müssen unser System also lokalisieren (i18n - Internationalization). In Contao 5 nutzen wir dafür das TranslatorInterface von Symfony und sogenannte XLIFF-Dateien (.xlf). XLIFF ist ein XML-basierter Industriestandard, der von nahezu jedem professionellen Übersetzungsbüro und Software-Tool verstanden wird.
Schritt 1: Die XLIFF-Dateien anlegen
Wir erstellen in unserem Bundle-Verzeichnis einen neuen Ordner namens translations/ (auf derselben Ebene wie src/ und config/).
Dort legen wir für jede unterstützte Sprache eine eigene Datei an. Der Dateiname folgt dem Muster [domain].[locale].xlf. Die Standard-Domain in Symfony ist messages.
1. Die deutsche Datei (translations/messages.de.xlf):
1<?xml version="1.0" encoding="utf-8"?>
2<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
3 <file source-language="en" target-language="de" datatype="plaintext" original="file.ext">
4 <body>
5 <trans-unit id="booking.button_submit">
6 <source>booking.button_submit</source>
7 <target>Strandkorb jetzt buchen</target>
8 </trans-unit>
9 <trans-unit id="booking.success_message">
10 <source>booking.success_message</source>
11 <target>Vielen Dank! Deine Buchung war erfolgreich.</target>
12 </trans-unit>
13 <trans-unit id="booking.error_already_booked">
14 <source>booking.error_already_booked</source>
15 <target>Entschuldigung, dieser Korb ist an diesem Datum bereits belegt.</target>
16 </trans-unit>
17 </body>
18 </file>
19</xliff>2. Die englische Fallback-Datei (translations/messages.en.xlf): Kopiere die deutsche Datei, ändere target-language="en" und übersetze die Inhalte in den <target>-Tags auf Englisch (z. B. "Book beach chair now"). Englisch sollte in Open-Source-Modulen immer als Fallback vorhanden sein.
Schritt 2: Übersetzungen im Twig-Template nutzen
Jetzt, da unsere Texte sauber in XLIFF-Dateien ausgelagert sind, können wir unsere Frontend-Templates aufräumen. Wir suchen alle hartcodierten Strings und ersetzen sie durch den Twig-Filter |trans.
Wir öffnen unser Template (z. B. templates/frontend_module/beachside_booking.html.twig) und passen es an:
{# Vorher: #}
<button type="submit">Strandkorb jetzt buchen</button>
{# Nachher: #}
<button type="submit">{{ 'booking.button_submit'|trans }}</button>Wenn Contao diese Seite nun rendert, schaut der Symfony Translator auf die im System eingestellte Sprache des Besuchers (z. B. anhand des Contao-Seitenbaums). Ist die Seite deutsch, sucht er in der messages.de.xlf nach dem Schlüssel booking.button_submit und wirft den korrekten Text aus.
Schritt 3: Übersetzungen in PHP-Klassen (Controller)
Nicht alle Texte werden im Twig-Template ausgegeben. Oft werfen wir Fehlermeldungen oder Flash-Messages direkt aus unserem Controller heraus (z. B. wenn die Formularvalidierung fehlschlägt).
Um den Translator in einer PHP-Klasse zu nutzen, injizieren wir das TranslatorInterface über den Konstruktor (Dependency Injection).
1<?php
2
3namespace Acme\BeachsideBundle\Controller;
4
5use Symfony\Component\HttpFoundation\Response;
6use Symfony\Contracts\Translation\TranslatorInterface;
7
8class BookingController
9{
10 public function __construct(
11 private readonly TranslatorInterface $translator
12 ) {
13 }
14
15 public function bookChair(): Response
16 {
17 // ... Validierungslogik ...
18 $isAlreadyBooked = true; // Simulierter Fehler
19
20 if ($isAlreadyBooked) {
21 // FALSCH: throw new \Exception('Korb ist schon weg!');
22
23 // RICHTIG: Den Translator nutzen
24 $errorMessage = $this->translator->trans('booking.error_already_booked');
25 throw new \Exception($errorMessage);
26 }
27
28 // ...
29 }
30}Mit dieser sauberen Trennung von Code und Sprache ist dein Modul nun extrem flexibel. Jeder Entwickler weltweit kann einfach eine messages.nl.xlf oder messages.fr.xlf in sein Projekt legen, und dein Modul spricht sofort Niederländisch oder Französisch, ohne dass auch nur eine einzige Zeile PHP-Code angefasst werden muss!
Doch saubere Texte allein machen noch kein Enterprise-System. Wenn Tausende Nutzer gleichzeitig auf "Buchen" klicken, brauchen wir eine intelligente Caching-Strategie. Das gehen wir im nächsten Schritt an.

Der Turbo-Boost: HTTP-Caching in Contao 5
Unsere Texte sind nun sauber in XLIFF-Dateien ausgelagert. Jetzt widmen wir uns dem wichtigsten Thema für hochfrequentierte Websites: der Performance.
Wenn du an Caching denkst, hast du vielleicht klassische Datenbank-Caches (wie Redis oder Memcached) im Kopf. Diese sind großartig, aber sie haben einen entscheidenden Nachteil: Um sie zu nutzen, muss der Server trotzdem erst den PHP-Prozess starten, Contao booten, die Datenbankverbindung aufbauen und dann feststellen, dass die Daten bereits im Cache liegen. Das kostet wertvolle Millisekunden und CPU-Leistung.
Contao 5 geht einen viel radikaleren Weg und setzt voll auf das HTTP-Caching-Protokoll. Der Leitsatz lautet: Die schnellste Anfrage ist die, die Contao gar nicht erst erreicht.
Wie funktioniert der HTTP Cache (Reverse Proxy)?
Zwischen dem Browser des Besuchers und unserer eigentlichen Contao-Applikation sitzt ein sogenannter "Reverse Proxy" (ein Shared Cache). In der Contao Managed Edition ist das standardmäßig der in Symfony integrierte HttpCache, bei großen Enterprise-Setups oft eine dedizierte Software wie Varnish oder ein CDN wie Cloudflare.
Wenn ein Nutzer unsere Strandkorb-Übersicht aufruft, passiert Folgendes:
Der Browser fragt die Seite an.
Der Proxy-Server fängt die Anfrage ab. Er merkt: "Hey, ich habe diese Seite nicht in meinem Speicher." Er leitet die Anfrage an Contao weiter.
Contao berechnet die Seite (Datenbankabfrage, Twig-Rendering) und sendet sie zurück an den Proxy, versehen mit einem digitalen Post-it: "Diese Seite ist für die nächsten 60 Minuten gültig."
Der Proxy speichert die Seite ab und liefert sie an den Nutzer aus.
Wenn 10.000 weitere Nutzer in den nächsten 59 Minuten dieselbe Seite aufrufen, liefert der Proxy die Seite in Bruchteilen von Millisekunden direkt aus seinem Arbeitsspeicher aus. Contao und PHP bekommen von diesem gigantischen Traffic absolut nichts mit. Dein Server langweilt sich förmlich.
Frontend-Module cachebar machen
Standardmäßig cacht Contao Frontend-Module nicht aggressiv, um zu verhindern, dass versehentlich veraltete oder private Nutzerdaten (wie ein Warenkorb) im globalen Shared Cache landen. Wir müssen Contao explizit mitteilen, dass unsere Strandkorb-Liste sicher gecacht werden darf.
Wir öffnen unseren BeachChairListController (den wir als Frontend-Modul gebaut haben) und passen das Response-Objekt an:
1<?php
2
3namespace Acme\BeachsideBundle\Controller\FrontendModule;
4
5use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
6use Contao\ModuleModel;
7use Contao\Template;
8use Symfony\Component\HttpFoundation\Request;
9use Symfony\Component\HttpFoundation\Response;
10use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
11
12#[AsFrontendModule(category: 'beachside')]
13class BeachChairListController extends AbstractFrontendModuleController
14{
15 protected function getResponse(Template $template, ModuleModel $model, Request $request): Response
16 {
17 // ... hier holen wir unsere Strandkörbe aus der Datenbank ...
18 $template->chairs = $this->fetchChairsFromDatabase();
19
20 // Wir holen uns die finale Response ab
21 $response = $template->getResponse();
22
23 // NEU: Wir konfigurieren das HTTP-Caching!
24
25 // 1. Wir erlauben, dass Proxy-Server diese Antwort cachen dürfen (Public)
26 $response->setPublic();
27
28 // 2. Wir setzen die Lebensdauer im Shared Cache (Proxy) auf 1 Stunde (3600 Sekunden)
29 $response->setSharedMaxAge(3600);
30
31 // 3. Wir setzen die Lebensdauer im Browser des Nutzers (Private) auf 10 Minuten
32 $response->setMaxAge(600);
33
34 return $response;
35 }
36}Mit diesen drei einfachen Zeilen Code haben wir unser Modul auf Enterprise-Geschwindigkeit getrimmt! Die HTTP-Header Cache-Control: public, s-maxage=3600, max-age=600 werden nun an den Server gesendet.
Das Problem mit der Aktualität
Wir haben jetzt ein Modul, das extrem schnell lädt. Doch hier lauert eine gefährliche Falle: Wir haben gesagt, der Cache gilt für eine Stunde. Was passiert, wenn ein Nutzer nach 5 Minuten einen Strandkorb bucht? Die Datenbank weiß, dass der Korb weg ist. Aber der Proxy-Server davor weiß davon nichts! Er wird den nächsten 55 Minuten lang allen Besuchern weiterhin stur die veraltete, gecachte Seite ausliefern, auf der der Strandkorb noch als "frei" markiert ist. Das führt unweigerlich zu Doppelbuchungen und massiven Problemen.
Wir müssen dem Proxy-Server also mitteilen können: "Hey, wirf diese spezifische Seite sofort aus deinem Speicher, da sich im Hintergrund etwas geändert hat!" Die Lösung für dieses Problem nennt sich Cache Tagging (Cache-Invalidierung). Wie wir das FOSHttpCacheBundle nutzen, um punktgenau Daten aus dem Cache zu löschen, implementieren wir im nächsten Schritt.

Cache Tagging: Intelligente Selbstheilung des Systems
Im vorherigen Schritt haben wir unser Frontend-Modul angewiesen, sich für eine Stunde im Proxy-Cache (dem Shared Cache) zur Ruhe zu setzen. Das sorgt für extreme Geschwindigkeit, führt aber zu dem fatalen Problem der veralteten Daten, sobald ein Korb gebucht wird.
Wir können den Cache nicht einfach global löschen, denn dann würden auch alle anderen gecachten Seiten (wie News oder FAQ) neu aufgebaut werden müssen. Das wäre hochgradig ineffizient.
Die Enterprise-Lösung für dieses Dilemma lautet: Cache Tagging (Etikettierung). Wir kleben gewissermaßen unsichtbare Etiketten an unsere HTTP-Antworten. Wenn Contao eine Liste von Strandkörben an den Proxy-Server ausliefert, sagen wir dem Proxy: "Speichere diese Seite für eine Stunde, aber merke dir, dass sie zum Etikett beach_chair_list gehört."
Wird nun ein Korb gebucht, ruft unser Skript in Richtung des Proxy-Servers: "Lösche sofort alles, was das Etikett beach_chair_list trägt!" Der Proxy wirft die veraltete Seite weg, alle anderen Seiten (News, Blog) bleiben aber unangetastet im schnellen Speicher.
Schritt 1: Dem Response ein Tag (Etikett) verpassen
Contao 5 nutzt im Hintergrund das FOSHttpCacheBundle, welches uns fantastische Werkzeuge für das Tagging bietet. Wir öffnen wieder unseren BeachChairListController und injizieren den ResponseTagger.
1<?php
2
3namespace Acme\BeachsideBundle\Controller\FrontendModule;
4
5use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
6use Contao\ModuleModel;
7use Contao\Template;
8use FOS\HttpCacheBundle\CacheManager; // WICHTIG: Import hinzufügen
9use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; // WICHTIG: Import hinzufügen
10use Symfony\Component\HttpFoundation\Request;
11use Symfony\Component\HttpFoundation\Response;
12use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
13
14#[AsFrontendModule(category: 'beachside')]
15class BeachChairListController extends AbstractFrontendModuleController
16{
17 public function __construct(
18 private readonly SymfonyResponseTagger $responseTagger
19 ) {
20 }
21
22 protected function getResponse(Template $template, ModuleModel $model, Request $request): Response
23 {
24 $template->chairs = $this->fetchChairsFromDatabase();
25 $response = $template->getResponse();
26
27 $response->setPublic();
28 $response->setSharedMaxAge(3600);
29 $response->setMaxAge(600);
30
31 // NEU: Wir heften ein Cache-Tag an diese Antwort
32 $this->responseTagger->addTags(['beach_chair_list']);
33
34 return $response;
35 }
36}Schritt 2: Den Cache gezielt invalidieren (löschen)
Nun müssen wir dafür sorgen, dass dieses Etikett gelöscht wird, sobald sich die Datenlage ändert. Wo ändert sich die Datenlage? In unserem Backend (DCA), wenn ein Administrator einen Korb anlegt, oder in unserem eigenen Buchungs-Controller, wenn ein Kunde bucht.
Lass uns unseren BookingController (der den Checkout-Prozess der Buchung verarbeitet) anpassen. Hier benötigen wir den CacheManager, um den Befehl zum Proxy zu senden.
1<?php
2
3namespace Acme\BeachsideBundle\Controller;
4
5use FOS\HttpCacheBundle\CacheManager;
6use Symfony\Component\HttpFoundation\Response;
7
8class BookingController
9{
10 public function __construct(
11 private readonly CacheManager $cacheManager
12 ) {
13 }
14
15 public function bookChair(): Response
16 {
17 // 1. Logik zum Speichern der Buchung in der Datenbank ausführen...
18 $this->saveBookingToDatabase();
19
20 // 2. Cache Invalidation auslösen!
21 // Wir teilen dem Proxy (z.B. Varnish oder dem Symfony HttpCache) mit,
22 // dass alle Seiten mit diesem Tag ab sofort ungültig sind.
23 $this->cacheManager->invalidateTags(['beach_chair_list']);
24
25 return new Response('Buchung erfolgreich!');
26 }
27}Die Magie des Systems
Was passiert jetzt in einer Live-Umgebung?
08:00 Uhr: 500 Nutzer rufen die Strandkorb-Übersicht auf. Die Seite wird exakt einmal von Contao generiert und 499-mal in Millisekunden aus dem Cache (Varnish/Proxy) geliefert.
08:14 Uhr: Ein Nutzer klickt auf "Buchen". Der
BookingControllerspeichert die Daten und sendet den Invalidierungs-Befehl fürbeach_chair_list. Der Proxy löscht diese spezielle Seite aus seinem Speicher.08:15 Uhr: Der nächste Nutzer ruft die Übersicht auf. Da der Cache für dieses Tag leer ist, leitet der Proxy die Anfrage an Contao weiter. Contao holt die topaktuellen Daten (der Korb ist nun besetzt), rendert die Seite, versieht sie wieder mit dem Tag
beach_chair_listund der Proxy speichert sie wieder für die nächsten 500 Nutzer.
Mit diesem System hast du ein Modul geschaffen, das selbst bei größtem Besucheransturm performant bleibt und trotzdem garantiert immer den korrekten Buchungsstand anzeigt.
Wir haben nun Lokalisierung (XLIFF) und Enterprise-Caching (HTTP Cache & Tags) implementiert. Um den Feinschliff unseres Codes abzuschließen, schauen wir uns im nächsten Schritt noch an, wie wir unsere Frontend-Ressourcen (CSS/JS) optimal über das Contao Asset Management integrieren.

Frontend Assets: CSS und JavaScript professionell einbinden
Unsere Server-Antwortzeiten sind dank des HTTP-Cachings nun auf einem absoluten Minimum. Doch die schnellste Server-Antwort bringt dem Nutzer nichts, wenn der Browser danach noch hunderte unkomprimierte CSS- und JavaScript-Dateien herunterladen muss.
Wenn Anfänger ein Frontend-Modul bauen, binden sie das nötige Styling oft direkt im Twig-Template per <link>-Tag ein oder schreiben <style>-Blöcke mitten in den HTML-Code. Für eine professionelle Contao-Erweiterung ist das ein fataler Fehler. Contao verfügt über einen internen "Combiner" (Kombinierer), der alle CSS- und JS-Dateien einer Seite zu einer einzigen Datei zusammenfasst und komprimiert. Hardcodierte Skripte im Template entgehen diesem System völlig.
Wir müssen unsere Assets (CSS, JS, Bilder) stattdessen über das offizielle Contao Asset Management registrieren.
Schritt 1: Der public-Ordner
Alle Dateien, die direkt über den Browser aufrufbar sein sollen (wie style.css oder booking.js), dürfen niemals im src/-Ordner liegen. Dieser ist aus Sicherheitsgründen für die Außenwelt gesperrt.
Erstelle in deinem Bundle-Root (neben src/ und config/) einen Ordner namens public/. Lege dort deine Dateien ab:
public/css/beachside.csspublic/js/booking.js
Wenn ein Nutzer später in seinem Projekt composer require acme/beachside-bundle ausführt, erkennt Symfony diesen Ordner automatisch. Es erstellt einen sogenannten Symlink (eine Verknüpfung) im öffentlichen Verzeichnis der Hauptinstallation. Deine Dateien sind dann unter der URL deinedomain.de/bundles/acmebeachside/css/beachside.css erreichbar.
Schritt 2: Assets im Twig-Template registrieren
Anstatt die Dateien nun hart in unser Strandkorb-Template zu schreiben, nutzen wir die speziellen Contao Twig-Funktionen. Öffne dein Frontend-Modul-Template (z.B. beachside_booking.html.twig) und füge ganz oben folgendes ein:
1{# Wir fügen unsere CSS-Datei zum globalen Contao-Combiner hinzu #}
2{% do addCssResource('bundles/acmebeachside/css/beachside.css|static') %}
3
4{# Wir laden unser JavaScript am Ende des <body>-Tags #}
5{% do addJavascriptResource('bundles/acmebeachside/js/booking.js|static') %}
6
7<div>
8 <h2>{{ 'booking.title'|trans }}</h2>
9</div>Was bewirkt das |static Flag? Das ist ein echter Performance-Trick! Es teilt Contao mit, dass diese Datei statisch ist und sich nicht bei jedem Seitenaufruf ändert. Contao nimmt diese Datei, wirft sie mit allen anderen CSS-Dateien des verwendeten Themes in einen großen Topf, entfernt alle Leerzeichen (Minification) und liefert dem Browser am Ende nur eine einzige, winzige assets/css/kombiniert-xyz.css aus.
Schritt 3: Die Production-PHP-Umgebung (OPcache)
Unser Code ist jetzt schnell, unsere Übersetzungen sind sauber und unsere Assets sind komprimiert. Wenn du das Modul nun auf einem echten Live-Server (Production) einsetzt, gibt es noch zwei Befehle, die den Unterschied zwischen "schnell" und "rasend schnell" ausmachen.
PHP ist eine interpretierte Sprache. Das bedeutet, der Server liest deine .php-Dateien bei jedem Aufruf theoretisch neu ein. Um das zu verhindern, nutzen moderne Server den Zend OPcache. Er speichert den vorkompilierten PHP-Code im Arbeitsspeicher.
Damit dein Strandkorb-System (und ganz Contao 5) optimal läuft, überprüfe in der php.ini deines Live-Servers unbedingt diese Werte:
Ini, TOML
1; Aktiviert den OPcache
2opcache.enable=1
3
4; WIE VIEL Arbeitsspeicher darf der Cache belegen? (128MB bis 256MB empfohlen)
5opcache.memory_consumption=256
6
7; WIE VIELE Dateien dürfen maximal gecacht werden?
8; Contao 5 hat extrem viele kleine Dateien (Klassen).
9; Setze diesen Wert zwingend nach oben!
10opcache.max_accelerated_files=20000
11
12; In Production: Prüfe NICHT bei jedem Aufruf, ob sich die Datei geändert hat!
13; (Das spart massiv Festplattenzugriffe)
14opcache.validate_timestamps=0Hinweis: Wenn du validate_timestamps=0 setzt, musst du nach jedem Update deines Bundles (oder wenn du eine PHP-Datei via FTP änderst) den OPcache manuell leeren (z.B. über den Contao Manager), da der Server Änderungen sonst nicht bemerkt!
Schritt 4: Der finale Composer-Befehl
Wenn du dein System final für den Kunden-Server aufbaust, nutze niemals den Standard-Befehl composer install. Nutze stattdessen das Setup für Produktionsumgebungen:
composer install --no-dev --optimize-autoloader--no-devverhindert, dass Entwickler-Tools (wie der Easy Coding Standard oder PHPUnit), die in dercomposer.jsonstehen, auf dem Live-Server landen.--optimize-autoloader(-o) weist Composer an, eine feste "Landkarte" aller PHP-Klassen (eine Classmap) zu erstellen. Anstatt bei jedem Aufruf einer Strandkorb-Klasse das gesamte Dateisystem zu durchsuchen, weiß PHP sofort, in welchem Ordner die Datei liegt.
Wir sind fast am Ziel! Unser Modul ist nun ein performantes Enterprise-System. Bevor wir diese Modul-Entwicklungsreise abschließen, fassen wir im nächsten (und für dieses Thema letzten) Schritt alles zusammen und werfen einen kurzen Blick auf den eigentlichen Deployment-Prozess mit automatisierten Tools wie Deployer.

Professionelles Deployment: Vergiss FTP!
Wir haben unsere Contao 5 Applikation lokal bis zur absoluten Perfektion getrieben. Die Performance stimmt, die Übersetzungen sind sauber, die Assets komprimiert. Nun stehen wir vor der klassischen Frage: Wie kommt der Code jetzt auf den Live-Server des Kunden?
Viele Entwickler greifen aus Gewohnheit zum FTP-Programm (wie FileZilla), markieren alle geänderten Dateien und ziehen sie rüber. Für ein Enterprise-System ist das jedoch ein fatales Vorgehen.
Downtime: Während der Übertragung ist die Website oft für Minuten im Wartungsmodus oder wirft Fehlermeldungen, weil eine halbe PHP-Klasse hochgeladen wurde.
Fehleranfälligkeit: Hast du wirklich alle neuen Dateien erwischt? Hast du daran gedacht,
composer installauf dem Server auszuführen? Was ist mit den Datenbank-Migrationen?Kein Rollback: Wenn das Update die Live-Seite zerschießt, hast du ein massives Problem. Du musst hektisch versuchen, alte Dateien wiederherzustellen.
Die Lösung der modernen PHP-Welt lautet: Zero-Downtime Deployment. Das offizielle und von der Contao-Community empfohlene Tool dafür ist Deployer (deployer.org).
Schritt 1: Deployer installieren
Deployer ist ein in PHP geschriebenes Tool, das du lokal auf deinem Rechner ausführst. Es verbindet sich via SSH mit deinem Live-Server, bereitet im Hintergrund alles vor und schaltet die neue Version in einem Sekundenbruchteil live.
Öffne dein lokales Terminal und installiere Deployer als Entwicklungs-Abhängigkeit:
composer require deployer/deployer --devSchritt 2: Die deploy.php konfigurieren
Nach der Installation erstellen wir im Hauptverzeichnis deines Projekts eine Datei namens deploy.php. Deployer bringt bereits ein fertiges "Rezept" für Contao mit, das alle Besonderheiten von Contao 5 (wie den Contao Manager oder Datenbank-Migrationen) kennt.
Hier ist eine minimale Konfiguration für den Live-Server:
1<?php
2namespace Deployer;
3
4// 1. Das Contao-Rezept laden
5require 'recipe/contao.php';
6
7// 2. Wo liegt der Code? (Dein Git-Repository)
8set('repository', 'git@github.com:deine-agentur/kundenprojekt.git');
9
10// 3. Welche Dateien/Ordner sollen bei jedem Update erhalten bleiben?
11// (z.B. User-Uploads oder die geheime .env Datei)
12add('shared_files', ['.env.local']);
13add('shared_dirs', ['public/files', 'var/logs']);
14
15// 4. Den Live-Server konfigurieren
16host('live-server')
17 ->setHostname('ssh.dein-hoster.de')
18 ->setRemoteUser('ssh-nutzer')
19 ->setDeployPath('/var/www/vhosts/kundenprojekt');
20
21// Optional: Den Contao Manager automatisch mit herunterladen
22after('deploy:update_code', 'contao:manager:download');Schritt 3: Die Magie der Symlinks (Zero Downtime)
Wenn du nun auf deinem lokalen Rechner den Befehl vendor/bin/dep deploy ausführst, passiert etwas Geniales. Deployer überschreibt nicht einfach deinen aktuellen Live-Ordner. Stattdessen nutzt es eine hochintelligente Ordnerstruktur auf dem Server:
/releases/(Hier liegen die letzten 5 Versionen deiner Seite)/shared/(Hier liegen die geteilten Dateien, wie Bilder auspublic/files)/current/(Das ist ein Symlink, der exakt auf das aktuellste Release zeigt)
Der Ablauf im Hintergrund:
Deployer loggt sich via SSH ein und erstellt einen neuen Ordner
releases/2.Es lädt deinen aktuellen Code aus GitHub dorthin herunter.
Es führt
composer install --no-dev -oim neuen Ordner aus.Es führt die Contao Datenbank-Migration aus (
contao:migrate).Der magische Moment: Erst wenn alles 100% fehlerfrei lief, ändert Deployer den Symlink
/current/so, dass er vonreleases/1aufreleases/2zeigt.
Dieser Wechsel dauert weniger als eine Millisekunde. Deine Besucher bemerken von dem gesamten Update-Vorgang absolut nichts. Keine Fehler, kein Wartungsmodus.
Und wenn doch etwas schiefgeht? Angenommen, du hast einen logischen Fehler im Code übersehen, der erst auf dem Live-Server zum Absturz führt. Anstatt in Panik zu verfallen, tippst du einfach:
vendor/bin/dep rollbackDeployer ändert den Symlink sofort wieder auf den alten Ordner releases/1. Die Seite ist innerhalb einer Sekunde wieder online und du kannst den Fehler in Ruhe lokal suchen.
Unser System lässt sich nun professionell und ausfallsicher deployen. Doch diesen Befehl manuell einzutippen, ist auf Dauer immer noch nervig. Im nächsten Teil bringen wir diesen Prozess in die Cloud und lassen GitLab CI oder GitHub Actions das Deployment nach jedem Git-Push völlig automatisch übernehmen.

Vollautomatisierung: Continuous Deployment mit GitHub Actions
Im vorherigen Schritt haben wir gelernt, wie wir mit Deployer unsere Website fehlerfrei und ohne Ausfallzeiten (Zero-Downtime) auf den Server bringen. Der Befehl vendor/bin/dep deploy ist ein gigantischer Fortschritt gegenüber FTP.
Doch es gibt immer noch einen Haken: Dieser Prozess ist an deinen lokalen Rechner gebunden. Was ist, wenn du im Urlaub bist und ein Kollege einen dringenden Bugfix einspielt? Er muss Deployer lokal einrichten, braucht die SSH-Zugangsdaten für den Server und muss hoffen, dass seine lokale PHP-Version mit der auf dem Server übereinstimmt.
Die absolute Königsklasse der Webentwicklung löst dieses Problem durch Continuous Deployment (CD). Wir übergeben die Verantwortung für das Deployment komplett an unseren Git-Server (z. B. GitHub oder GitLab).
Das Ziel: Sobald jemand neuen Code in den main-Branch pusht, startet in der Cloud automatisch ein unsichtbarer Roboter, der unseren Code testet und eigenständig den Deployer-Befehl ausführt.
Schritt 1: Das Geheimnis der SSH-Keys (Secrets)
Damit GitHub sich auf deinem Live-Server einloggen darf, benötigt es einen SSH-Schlüssel. Diesen privaten Schlüssel darfst du niemals direkt in deinen Code (das Repository) kopieren!
Stattdessen nutzen wir die sicheren Tresore der Git-Anbieter.
Generiere ein neues SSH-Schlüsselpaar (oder nutze dein bestehendes).
Hinterlege den öffentlichen Schlüssel (
.pub) auf deinem Live-Server in der Datei~/.ssh/authorized_keys.Gehe auf GitHub in dein Repository unter Settings -> Secrets and variables -> Actions.
Erstelle ein neues Secret namens
SSH_PRIVATE_KEYund füge dort den privaten Schlüssel ein.
Schritt 2: Die Workflow-Datei erstellen
GitHub Actions werden über einfache YAML-Dateien gesteuert. Erstelle in deinem Projekt die Ordnerstruktur .github/workflows/ und lege dort eine Datei namens deploy.yml an.
Hier ist der Bauplan für unseren automatischen Deployment-Roboter:
1name: Deploy to Production
2
3# Wann soll dieser Roboter starten?
4on:
5 push:
6 branches: [ main ] # Bei jedem Push in den Haupt-Branch
7
8jobs:
9 deploy:
10 name: Run Deployer
11 runs-on: ubuntu-latest # Wir mieten uns für wenige Sekunden einen Ubuntu-Server von GitHub
12
13 steps:
14 # 1. Lade unseren Code auf den GitHub-Server
15 - name: Checkout Code
16 uses: actions/checkout@v4
17
18 # 2. Installiere die richtige PHP-Version auf dem GitHub-Server
19 - name: Setup PHP
20 uses: shivammathur/setup-php@v2
21 with:
22 php-version: '8.1'
23 tools: composer:v2
24
25 # 3. Lade den geheimen SSH-Schlüssel aus dem GitHub-Tresor
26 - name: Setup SSH
27 uses: webfactory/ssh-agent@v0.8.0
28 with:
29 ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
30
31 # 4. Erlaube die Verbindung zu unserem Server (Schutz vor Man-in-the-Middle)
32 - name: Add Known Hosts
33 run: ssh-keyscan -H ssh.dein-hoster.de >> ~/.ssh/known_hosts
34
35 # 5. Lade alle PHP-Pakete (inklusive Deployer) herunter
36 - name: Install Dependencies
37 run: composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
38
39 # 6. Der magische Moment: Führe Deployer aus!
40 - name: Run Deployment
41 run: vendor/bin/dep deploy liveWas passiert nun in der Praxis?
Du hast lokal an deinem Strandkorb-Modul gearbeitet und das CSS für einen Button angepasst. Du tippst in dein Terminal:
git add .
git commit -m "Design des Buchungs-Buttons optimiert"
git push origin mainDu klappst deinen Laptop zu und gehst Kaffee trinken.
Im Hintergrund passiert nun die Magie:
GitHub erkennt den Push auf
main.Ein virtueller Server in der Cloud fährt hoch.
Der Server lädt deinen Code, installiert die Composer-Abhängigkeiten und verbindet sich via SSH mit deinem Live-Server bei All-Inkl, Hetzner oder Contabo.
Deployer legt einen neuen Release-Ordner an, migriert die Contao-Datenbank, leert den Symfony-Cache und schaltet den Symlink um.
Du erhältst eine kurze E-Mail: Deployment successful.
Willkommen im Enterprise-Niveau! Dein Projekt ist nun vollständig entkoppelt. Das bedeutet, du kannst von jedem Rechner der Welt – theoretisch sogar vom iPad aus dem Urlaub – eine Zeile Code in GitHub ändern, und das System aktualisiert den Live-Server vollautomatisch, sicher und ohne eine einzige Sekunde Downtime.

Was für eine Reise! Wir haben unser Strandkorb-Buchungssystem von einer einfachen, lokal funktionierenden Erweiterung in ein ausfallsicheres Enterprise-System verwandelt. Wenn es um maximale Contao 5 Performance geht, liegt der Unterschied zwischen einem Hobby-Projekt und einer professionellen Architektur genau in den Details, die wir in diesem Artikel durchgearbeitet haben.
Die ultimative "Production Ready" Checkliste
Bevor du jemals wieder ein Contao 5 Projekt auf einem Live-Server veröffentlichst, gehe diese Punkte im Kopf durch:
Keine hartcodierten Texte: Nutzt dein Code durchgehend den Symfony
TranslatorInterfaceund saubere.xlf-Dateien für alle Ausgaben?HTTP-Caching aktiviert: Sind deine öffentlichen Frontend-Module mit
$response->setSharedMaxAge()konfiguriert, um den Reverse Proxy (Varnish/Symfony Cache) zu nutzen?Cache-Tagging implementiert: Werden veraltete Ansichten beim Speichern von Datensätzen über den
CacheManagerund spezifische Tags (beach_chair_list) sofort invalidiert?Assets registriert: Werden deine CSS- und JS-Dateien über das Contao Asset Management eingebunden, damit der Combiner sie minimieren kann?
OPcache optimiert: Ist der Zend OPcache auf dem Live-Server aktiviert und auf Contao 5 abgestimmt (
opcache.max_accelerated_files=20000)?Production-Abhängigkeiten: Wurde der Code mit
composer install --no-dev -oinstalliert?Zero-Downtime Deployment: Nutzt du Tools wie Deployer anstelle von FTP, um Ausfallzeiten bei Updates komplett zu eliminieren?

Teil der Serie
Contao 5 Masterclass: The Beachside Project
Contao 5 Bundle Entwicklung: Die Masterclass für echte Entwickler Pillar
Contao 5 Bundle Setup: Das Fundament für professionelle Erweiterungen
Contao 5 Doctrine Entities: Moderne Datenmodellierung statt SQL-Chaos
Contao 5 DCA: Perfekte Backend-Masken für Entities erstellen
Contao 5 Doctrine Relations: Wir bauen die Buchungs-Logik
Contao 5 Service Layer: Business-Logik sauber kapseln
Contao 5 Unit Testing: Code-Absicherung mit PHPUnit & Test-Case
Contao 5 Twig Templates: Frontend-Ausgabe mit Fragment Controllern
Contao 5 Symfony Forms: Professionelle Formularverarbeitung
Contao 5 Notification Center: Zentrale Kommunikation & E-Mail-Workflows
Contao 5 Backend Dashboards & Custom Routing
Contao 5 Console Commands: Automatisierung & Hintergrund-Jobs
Contao 5 Headless CMS API: REST-Schnittstellen bauen
Contao 5 Performance, Caching & Zero-Downtime Deployment
Häufig gestellte Fragen (FAQ)
Der Kreis schließt sich
Dein Modul ist nun nicht nur funktional brillant, sondern auch absolut kugelsicher, blitzschnell und internationalisiert. Es ist im wahrsten Sinne des Wortes Production Ready.
Erinnerst du dich an den allerletzten, großen Schritt, den wir bereits detailliert vorbereitet haben? Genau! Die Veröffentlichung auf Packagist und die Integration in den Contao Manager.
Möchtest du, dass wir nun alle Teile unserer Masterclass Revue passieren lassen, oder hast du noch spezifische Fragen zum Feinschliff, bevor wir das Kapitel Contao 5 Modulentwicklung glorreich abschließen?

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


