
Projektstruktur & Entwicklungsumgebung für Contao und Next.js

Repository-Strategie – Polyrepo und Trunk-Based Development
Die Wahl der richtigen Contao Next.js Projektstruktur ist die absolut wichtigste Architektur-Entscheidung, noch bevor wir die erste Zeile Code schreiben oder einen Docker-Container hochfahren. In unserem entkoppelten System prallen zwei fundamental unterschiedliche Welten aufeinander: Ein PHP-basiertes Symfony-Backend (Contao) und ein JavaScript-basiertes Frontend (Next.js).
Viele Entwickler tendieren heute aus Bequemlichkeit zu einem Monorepo (alles in einem Git-Repository), gesteuert durch Tools wie Turborepo. Für reine JavaScript-Stacks ist das hervorragend. Für unsere Contao-Headless-Architektur ist ein Monorepo jedoch ein strategischer Fehler. Beide Systeme haben völlig unterschiedliche Lebenszyklen, Abhängigkeits-Manager (Composer vs. NPM) und Deployment-Strategien.
Wir setzen für unsere Contao Next.js Projektstruktur kompromisslos auf eine Polyrepo-Strategie (zwei getrennte Repositories in einem Workspace). Das Backend agiert als autarke Datenquelle, die völlig unabhängig von der konsumierenden Präsentationsschicht versioniert wird. Diese physische Trennung zwingt uns von Tag eins an zu einer sauberen API-Planung, da wir keine schmutzigen "Shortcuts" zwischen den Systemen programmieren können.
Um die Entwicklung neuer Features über beide Repositories hinweg synchron zu halten, etablieren wir Trunk-Based Development. Features werden in kurzlebigen Branches entwickelt, die im Frontend und Backend exakt gleich heißen (z. B. feat/ticket-42-navigation).
Praxis & Code: Die Workspace-Initialisierung
Wir legen nun die Basis-Struktur in unserem System an. Öffne dein Terminal und führe die folgenden Befehle aus, um die Ordnerstruktur zu generieren und die getrennten Git-Repositories initial aufzusetzen.
1# 1. Erstelle einen zentralen Workspace für die gesamte Masterclass
2mkdir -p workspace/contao-headless-masterclass
3cd workspace/contao-headless-masterclass
4
5# 2. Erstelle die getrennten Verzeichnisse für Backend und Frontend
6mkdir contao-backend nextjs-frontend
7
8# 3. Initialisiere das Backend-Repository (Contao)
9cd contao-backend
10git init
11echo "# Contao Headless Backend API" > README.md
12git add README.md
13git commit -m "chore: initial commit backend"
14# Erstelle direkt den ersten synchronen Feature-Branch
15git checkout -b feat/init-docker-setup
16
17# 4. Initialisiere das Frontend-Repository (Next.js)
18cd ../nextjs-frontend
19git init
20echo "# Next.js Headless Frontend" > README.md
21git add README.md
22git commit -m "chore: initial commit frontend"
23# Erstelle den exakt gleich benannten Feature-Branch
24git checkout -b feat/init-docker-setup
25
26# Zurück ins Hauptverzeichnis navigieren
27cd ..Du hast nun zwei isolierte, saubere Git-Repositories, die durch die synchrone Branch-Benennung logisch miteinander verknüpft sind.

Das WSL2-Fundament härten und die I/O-Falle umgehen
Wer als Entwickler unter Windows arbeitet und moderne Web-Stacks betreibt, nutzt unweigerlich das Windows Subsystem for Linux (WSL2). Es bietet einen vollwertigen Linux-Kernel, der für Docker-basierte Workflows unerlässlich ist. Doch genau hier tappen 90 % der Entwickler in eine architektonische Falle, die das gesamte System ausbremst: Die Cross-OS I/O-Performance.
Wenn du deine Projektdateien im regulären Windows-Dateisystem belässt (z. B. unter C:\Users\Name\Projekte) und Docker aus WSL2 heraus darauf zugreifen lässt (über den internen Mount-Point /mnt/c/), muss jeder einzelne Datei-Lese- und Schreibvorgang über das 9P-Protokoll zwischen dem Windows-NTFS und dem Linux-ext4-Dateisystem übersetzt werden.
Bei einem monolithischen Projekt mit wenigen Dateien fällt das kaum auf. Bei einer Headless-Architektur mit zehntausenden kleinen Dateien im node_modules-Ordner (Next.js) und dem vendor-Ordner (Contao/Symfony) führt dies jedoch zu massiven Verzögerungen. Das Hot-Module-Replacement (HMR) im Frontend stockt, und das Rebuilden des Symfony-Caches dauert quälend lange.
Die kompromisslose Lösung: Der gesamte Quellcode muss zwingend in das native Linux-Dateisystem umziehen.
Bevor wir den Code jedoch verschieben, müssen wir ein weiteres, bekanntes WSL2-Problem lösen: den unbegrenzten Ressourcenhunger. Standardmäßig darf die virtuelle Maschine (der vmmem-Prozess) beinahe den gesamten Arbeitsspeicher deines Host-Systems konsumieren. Um zu verhindern, dass dein Rechner bei intensiven Docker-Builds einfriert, limitieren wir die Ressourcen hart.
Schritt 1: Ressourcen-Limits setzen (.wslconfig)
Drücke unter Windows die Tastenkombination Win + R, tippe %USERPROFILE% ein und drücke Enter. Erstelle in diesem Verzeichnis (deinem Windows-Benutzerordner) eine neue Datei namens .wslconfig. Öffne sie in einem Texteditor und füge folgende Konfiguration ein:
Ini, TOML
1[wsl2]
2# Limitiert den Arbeitsspeicher für das gesamte Linux-Subsystem (anpassen je nach System)
3memory=8GB
4
5# Begrenzt die Anzahl der CPU-Kerne, die WSL2 nutzen darf
6processors=4
7
8# Verhindert, dass WSL2 unnötig Swap-Speicher auf der Windows-SSD anlegt
9swap=0Wichtig: Um diese Änderungen zu übernehmen, musst du WSL2 neustarten. Öffne PowerShell als Administrator und führe wsl --shutdown aus.
Schritt 2: Das Projekt im Linux-Dateisystem anlegen
Öffne nun dein Ubuntu-Terminal. Wir befinden uns jetzt direkt im nativen Linux-Dateisystem (ext4). Hier legen wir den zentralen Workspace an und ziehen unsere zuvor initialisierten Repositories hinein (bzw. legen sie hier neu an, falls du Abschnitt 1 auf Windows ausgeführt hast).
1# 1. Wechsle in das sichere Linux-Home-Verzeichnis
2cd ~
3
4# 2. Erstelle den Master-Workspace
5mkdir -p workspace/contao-headless-masterclass
6cd workspace/contao-headless-masterclass
7
8# 3. Erstelle die Verzeichnisse für Frontend und Backend
9mkdir contao-backend nextjs-frontendUm nun aus Windows heraus extrem performant an diesem Code zu arbeiten, nutzen wir die Remote-Fähigkeiten von Visual Studio Code. Anstatt den Code über Windows-Pfade zu öffnen, startest du den Editor direkt aus dem Ubuntu-Terminal heraus:
# Startet den VS Code WSL-Server im aktuellen Verzeichnis
code .VS Code läuft nun als leichtgewichtiger Client auf Windows, greift aber über einen lokalen Server direkt auf die rasend schnellen ext4-Dateien in Ubuntu zu. Du hast damit die absolute I/O-Performance eines nativen Linux-Systems erreicht.

Die Docker-Orchestrierung und das "Compose Watch" Feature
Unser Code liegt nun sicher und extrem performant im nativen Linux-Dateisystem. Der nächste Schritt zur absoluten Architektur-Konsistenz ist die Containerisierung. Wir installieren weder PHP noch Node.js oder einen Datenbankserver global auf unserem Betriebssystem. Stattdessen isolieren wir jeden Dienst in einem eigenen Docker-Container. Das garantiert, dass die lokale Entwicklungsumgebung exakt denselben Spezifikationen entspricht wie der spätere Live-Server ("It works on my machine" gehört damit der Vergangenheit an).
Wir nutzen Docker Compose, um unsere vier benötigten Dienste (Datenbank, PHP-FPM, Nginx-Webserver und Next.js-Frontend) zentral zu orchestrieren.
Hierbei setzen wir für das Frontend auf ein relativ neues, aber absolut bahnbrechendes Feature: Docker Compose Watch (verfügbar ab Docker Compose v2.22). Traditionell bindet man bei der lokalen Entwicklung den Quellcode über ein "Volume" in den Container ein (volumes: - ./code:/app). Bei Node.js-Projekten führt das oft zu Problemen, da der gigantische node_modules-Ordner ständige Datei-Events feuert oder Berechtigungskonflikte zwischen Host und Container verursacht.
Mit dem watch-Attribut ändern wir die Strategie: Wir verzichten auf das Volume für das Frontend. Stattdessen weisen wir Docker an, unseren lokalen Code zu "beobachten". Ändern wir eine React-Komponente, synchronisiert Docker diese Datei in Millisekunden in den laufenden Container. Ändern wir hingegen die package.json (weil wir ein neues NPM-Paket installieren), erkennt Docker dies und baut das Image automatisch im Hintergrund neu. Das Ergebnis ist ein fehlerfreies, rasend schnelles Hot-Module-Replacement (HMR) für Next.js.
Praxis & Code: Die docker-compose.yml erstellen
Stelle sicher, dass du dich im Hauptverzeichnis deines Workspaces befindest (~/workspace/contao-headless-masterclass). Erstelle dort eine neue Datei namens docker-compose.yml und füge den folgenden Code ein:
Datei: workspace/contao-headless-masterclass/docker-compose.yml
1version: '3.8'
2
3services:
4 # 1. Die Datenbank für Contao
5 db:
6 image: mariadb:10.11
7 environment:
8 MYSQL_ROOT_PASSWORD: root
9 MYSQL_DATABASE: contao
10 MYSQL_USER: contao
11 MYSQL_PASSWORD: contao_password
12 volumes:
13 - db_data:/var/lib/mysql
14 ports:
15 - "3306:3306"
16
17 # 2. Der PHP-Prozess für das Contao Backend
18 php:
19 build:
20 context: ./contao-backend
21 dockerfile: Dockerfile.dev
22 volumes:
23 - ./contao-backend:/var/www/html
24 depends_on:
25 - db
26
27 # 3. Nginx als Webserver und lokales API-Gateway
28 web:
29 image: nginx:alpine
30 ports:
31 - "8080:80"
32 volumes:
33 - ./contao-backend:/var/www/html
34 - ./contao-backend/docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
35 depends_on:
36 - php
37
38 # 4. Next.js Frontend (mit dem Compose Watch Feature)
39 frontend:
40 build:
41 context: ./nextjs-frontend
42 dockerfile: Dockerfile.dev
43 ports:
44 - "3000:3000"
45 environment:
46 - NODE_ENV=development
47 # Interne Container-Auflösung: Das Frontend spricht mit dem Nginx-Container
48 - NEXT_PUBLIC_API_URL=http://web:80/api
49 develop:
50 watch:
51 # Bei Änderung der package.json: Image sofort neu bauen
52 - action: rebuild
53 path: ./nextjs-frontend/package.json
54 # Bei Code-Änderungen: Dateien extrem schnell in den Container synchronisieren
55 - action: sync
56 path: ./nextjs-frontend
57 target: /app
58 ignore:
59 - node_modules/
60 - .next/
61
62volumes:
63 db_data:Diese Datei ist der Bauplan für unsere gesamte lokale Infrastruktur. Sie vernetzt die Container untereinander (Next.js kann z. B. über http://web:80 sicher auf die Contao-API zugreifen, ohne das lokale Docker-Netzwerk zu verlassen) und exponiert nur die Ports 8080 (Contao) und 3000 (Next.js) nach außen an unseren Windows-Browser.

Lokale Container konfigurieren (Dockerfiles & Nginx)
Unsere docker-compose.yml aus dem vorherigen Schritt ist der Dirigent, aber ihr fehlen noch die Musiker. Sie referenziert in den Zeilen build: context: ... lokale Baupläne – sogenannte Dockerfiles.
Wir können für das Contao-Backend kein fertiges "Out-of-the-box"-Image verwenden. Contao 5 hat strikte Systemanforderungen und benötigt spezifische PHP-Erweiterungen (wie intl für Internationalisierung und gd für die Bildverarbeitung), die in Standard-Images nicht vorinstalliert sind. Für das Frontend benötigen wir hingegen nur einen extrem schlanken Node.js-Container, da das eigentliche Datei-Synchronisieren unser watch-Befehl übernimmt.
Zusätzlich müssen wir unserem Nginx-Webserver exakt mitteilen, wie er Anfragen verarbeiten soll. Contao nutzt ein modernes Routing-Konzept, bei dem aus Sicherheitsgründen sämtlicher Traffic zwingend in das Verzeichnis /public geleitet werden muss.
Legen wir nun diese drei essenziellen Konfigurationsdateien an.
Praxis & Code: Die Konfigurationsdateien erstellen
1. Das PHP-Dockerfile für Contao
Navigiere in deinem Code-Editor in den Ordner contao-backend und erstelle dort eine Datei namens Dockerfile.dev. Füge diesen Code ein:
Datei: workspace/contao-headless-masterclass/contao-backend/Dockerfile.dev
1# Wir nutzen ein extrem leichtgewichtiges Alpine-Linux als Basis
2FROM php:8.2-fpm-alpine
3
4# 1. System-Abhängigkeiten installieren, die zum Kompilieren der PHP-Erweiterungen nötig sind
5RUN apk add --no-cache \
6 freetype-dev \
7 libjpeg-turbo-dev \
8 libpng-dev \
9 icu-dev \
10 libzip-dev \
11 zip \
12 unzip \
13 git \
14 bash
15
16# 2. Spezifische PHP-Erweiterungen für Contao 5 konfigurieren und installieren
17RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
18 && docker-php-ext-install -j$(nproc) gd intl pdo_mysql zip opcache
19
20# 3. Composer (den PHP Paketmanager) global in den Container kopieren
21COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
22
23# 4. Arbeitsverzeichnis festlegen
24WORKDIR /var/www/html
25
26# 5. Berechtigungen für den Webserver-Nutzer setzen
27RUN chown -R www-data:www-data /var/www/html2. Die Nginx vHost-Konfiguration
Erstelle im Ordner contao-backend einen neuen Unterordner docker/nginx. Erstelle in diesem neuen Ordner die Datei default.conf.
Datei: workspace/contao-headless-masterclass/contao-backend/docker/nginx/default.conf
1server {
2 listen 80;
3 server_name localhost;
4
5 # WICHTIG: Contao 5 erwartet das Root-Verzeichnis zwingend im Ordner /public
6 root /var/www/html/public;
7 index index.php index.html;
8
9 # Alle Anfragen, die keine echten Dateien sind, an die index.php weiterleiten
10 location / {
11 try_files $uri $uri/ /index.php$is_args$args;
12 }
13
14 # PHP-Dateien an unseren PHP-FPM Container (Port 9000) übergeben
15 location ~ \.php$ {
16 fastcgi_split_path_info ^(.+\.php)(/.+)$;
17 fastcgi_pass php:9000;
18 fastcgi_index index.php;
19 include fastcgi_params;
20 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
21 fastcgi_param PATH_INFO $fastcgi_path_info;
22 }
23}3. Das Node.js-Dockerfile für Next.js
Navigiere in den Ordner nextjs-frontend und erstelle dort ebenfalls eine Datei namens Dockerfile.dev.
Datei: workspace/contao-headless-masterclass/nextjs-frontend/Dockerfile.dev
1FROM node:20-alpine
2
3WORKDIR /app
4
5# Wir kopieren nur die package.json (falls vorhanden).
6# Den restlichen Code injiziert Docker Compose Watch dynamisch zur Laufzeit.
7COPY package.json package-lock.json* ./
8
9# Wenn eine package.json existiert, installiere Abhängigkeiten (npm ci).
10# Der '|| true' Fallback verhindert einen Absturz, falls das Projekt noch komplett leer ist.
11RUN npm ci || true
12
13EXPOSE 3000
14
15# Startet den Development-Server
16CMD ["npm", "run", "dev"]Die Konfiguration ist hiermit vollständig abgeschlossen. Alle Systeme wissen nun exakt, was sie zu tun haben, wie sie miteinander kommunizieren und welche Abhängigkeiten sie benötigen.

Systemstart, Verifizierung und Fazit
Unsere gesamte lokale Infrastruktur ist nun konfiguriert. Wir haben das Linux-Dateisystem vorbereitet, die Git-Repositories isoliert und die Docker-Container (MariaDB, PHP, Nginx, Next.js) exakt aufeinander abgestimmt. Jetzt bringen wir die Architektur ans Netz.
Wenn wir Docker Compose anweisen, die Umgebung hochzufahren, passieren mehrere Dinge gleichzeitig: Die Basis-Images werden heruntergeladen, unsere individuellen Dockerfile.dev-Skripte installieren die nötigen Systemabhängigkeiten (wie die PHP-Erweiterungen für Contao) und das interne Docker-Netzwerk verknüpft alle vier Container sicher miteinander. Abschließend starten wir den watch-Prozess, der unser lokales Dateisystem überwacht und Code-Änderungen in Millisekunden in den Next.js-Container synchronisiert.
Praxis & Code: Die Umgebung hochfahren
Stelle sicher, dass du dich in deinem Ubuntu-Terminal im Hauptverzeichnis des Workspaces befindest (~/workspace/contao-headless-masterclass). Führe nun nacheinander diese beiden Befehle aus:
1# 1. Baue die Images (nur beim ersten Mal oder bei Änderungen an den Dockerfiles nötig)
2# und starte die Container im Hintergrund (-d für detached)
3docker compose up --build -d
4
5# 2. Aktiviere den Live-Sync (Hot-Reload) für das Next.js Frontend
6docker compose watchWie verifizieren wir, ob das Setup funktioniert? Das Terminal wird nun anzeigen, dass der Watch-Modus aktiv ist. Da wir die eigentliche Software (Contao und Next.js) noch nicht installiert haben, sondern lediglich die "nackten" Server-Container (PHP-FPM, Nginx, Node) betreiben, ist das System aktuell noch "leer".
Wenn du
http://localhost:8080im Browser öffnest, wirst du vermutlich einen404 Not Foundoder403 ForbiddenFehler von Nginx sehen. Das ist absolut korrekt! Nginx sucht im Ordnerpublic/index.php, welchen wir noch nicht via Composer generiert haben.Wenn du
http://localhost:3000öffnest, wird die Seite aktuell noch nicht laden, da im Frontend-Container das Kommandonpm run devmangels einer installierten Next.js-Applikation noch ins Leere läuft.
Fazit zu Teil 2: Wir haben unser Ziel erreicht. Das Fundament ist gegossen. Anstatt uns später mit quälend langsamen Datei-Speicher-Vorgängen (I/O-Lags) unter Windows oder unvorhersehbaren Versionskonflikten herumzuärgern, haben wir eine professionelle, komplett isolierte Enterprise-Umgebung geschaffen. Durch das Trunk-Based Development in zwei getrennten Repositories (Polyrepo) sind wir zudem von Tag eins an perfekt auf spätere automatisierte Deployments (CI/CD) vorbereitet.

Häufig gestellte Fragen (FAQ)
Klassische Entwicklungsumgebungen wie XAMPP installieren globale Software-Versionen direkt auf deinem Betriebssystem. Das führt zwangsläufig zu Versionskonflikten, wenn du an mehreren Projekten arbeitest. Zudem bilden sie das Zusammenspiel von PHP, einer spezifischen Node.js-Laufzeit und Nginx als Reverse Proxy nicht realitätsnah ab. Docker isoliert all diese Prozesse in Containern, die exakt der späteren Live-Umgebung entsprechen.
Nein, WSL2 ist auch unter Windows 10 (ab Version 1903) vollumfänglich verfügbar. Windows 11 bietet jedoch ein leicht verbessertes Ressourcen-Management und eine noch nahtlosere Integration des Linux-Dateisystems in den Windows Explorer.
Wenn du auf einem Mac arbeitest, hast du den Vorteil, dass macOS bereits auf UNIX basiert. Du benötigst kein WSL2. Du kannst Docker Desktop für Mac installieren und deine Projekte nativ auf deiner Festplatte belassen. Die I/O-Performance von Docker auf dem Mac wurde in den letzten Versionen (insbesondere mit der VirtioFS-Integration) massiv verbessert, sodass du denselben Code und dieselbe docker-compose.yml nutzen kannst.
Docker kann ressourcenintensiv sein, wenn es nicht richtig konfiguriert ist. In unserem Setup steuern wir den Ressourcenverbrauch gezielt über die .wslconfig-Datei in Windows. Dort legen wir harte Limits (z. B. maximal 8 GB RAM) für den Linux-Kernel fest. Die Container selbst sind auf das absolute Minimum an Diensten reduziert, sodass ausreichend Ressourcen für deine Code-Editoren und Design-Tools verfügbar bleiben.
Dein nächster Schritt: Die API-Entwicklung starten
Die Container sind hochgefahren, das Setup steht. Jetzt erwecken wir das Backend zum Leben. Im nächsten Teil installieren wir Contao in unserer Docker-Umgebung und beginnen direkt mit der Programmierung der API. Wir brechen aus den Standard-Templates aus und schreiben maßgeschneiderte Symfony-Routen, um die Datenbankinhalte als blitzschnelle JSON-Responses bereitzustellen.
Jetzt starten: Teil 3 – Contao Backend & API-Architektur "From Scratch"

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.


