shared, var, pub/media, app/etc richtig einrichten
Zero-Downtime-Deployments scheitern nicht an der Pipeline, sondern an einer falsch vorbereiteten Serverstruktur. Wer shared-Verzeichnisse und Symlinks nicht korrekt einrichtet, verliert bei jedem Release entweder Mediendateien oder die Konfiguration.
Inhaltsverzeichnis
- 1. Warum die Verzeichnisstruktur alles entscheidet
- 2. Die vollständige Grundstruktur auf dem Server
- 3. Shared-Verzeichnisse: Was bleibt, was wechselt
- 4. Server-Init-Skript: einmalig ausführen, dann nie wieder anfassen
- 5. Symlinks im Deploy-Schritt korrekt setzen
- 6. Berechtigungen und Eigentümer korrekt vergeben
- 7. Verify-Job in GitLab: Struktur automatisch prüfen
- 8. Vergleich: flache Struktur vs. Release-Modell
- 9. Zusammenfassung
- 10. Typische Fehler bei der Serverstruktur
- 11. FAQ
1. Warum die Verzeichnisstruktur alles entscheidet
Der häufigste Grund, warum Zero-Downtime-Deployments für Magento in der Praxis scheitern, liegt nicht in der CI/CD-Pipeline, sondern auf dem Server selbst. Teams bauen die Symlink-basierte Release-Struktur auf einem Server auf, der nie für dieses Modell vorbereitet wurde: pub/media liegt direkt im Web-Root, app/etc/env.php ist hartkodiert, var/log wächst in jedem Release-Verzeichnis unkontrolliert. Wenn der Symlink dann auf das neue Release zeigt, fehlen entweder die Mediendateien oder die Konfiguration.
Die Lösung ist eine klare Trennung zwischen Dateien, die mit jedem Release wechseln dürfen, und Dateien, die persistent über alle Releases hinweg bestehen müssen. Diese persistenten Daten leben im shared-Verzeichnis und werden per Symlink in jedes neue Release eingebunden. Das ist kein Magento-spezifisches Konzept, sondern das klassische Capistrano-Deployment-Modell — übertragen auf GitLab CI/CD mit Shell-Skripten.
Entscheidend ist, dass diese Struktur einmalig und vollständig vorbereitet wird, bevor die erste Pipeline läuft. Ein Deployment-Skript, das versucht, fehlende Verzeichnisse nachträglich anzulegen, ist anfällig für Race Conditions und unterschiedliche Zustände zwischen Staging und Production. Das Server-Init-Skript gehört in das Repository, wird einmal ausgeführt und nicht mehr angefasst — außer bei bewussten strukturellen Änderungen.
2. Die vollständige Grundstruktur auf dem Server
Die Zielstruktur auf dem Produktionsserver folgt einem einfachen, aber strikten Muster. Das Root-Verzeichnis der Anwendung enthält drei Bereiche: releases/ für alle historischen und aktuellen Releases, current als Symlink auf das aktive Release und shared/ für alle persistenten Dateien und Verzeichnisse. Der Webserver zeigt auf current/pub — der öffentlich zugängliche Teil von Magento.
Innerhalb von shared/ liegen alle Daten, die nicht Teil des Builds sind: die Magento-Konfiguration app/etc/env.php und app/etc/config.php, der Medienordner pub/media, die Session-Dateien var/session und die Log-Dateien var/log. Diese Struktur stellt sicher, dass ein Rollback auf ein älteres Release keinerlei Auswirkung auf Mediendateien oder Konfiguration hat — der Symlink zeigt einfach auf ein anderes Release-Verzeichnis, die shared-Daten bleiben unberührt.
# Expected server directory structure after init
# /var/www/magento/
# ├── current -> /var/www/magento/releases/v1.4.2 (symlink — updated per deploy)
# ├── releases/
# │ ├── v1.4.0/ (old release — kept for rollback)
# │ ├── v1.4.1/ (old release — kept for rollback)
# │ └── v1.4.2/ (current active release)
# └── shared/
# ├── app/
# │ └── etc/
# │ ├── env.php (persistent: DB, Redis, secrets)
# │ └── config.php (persistent: module list)
# ├── pub/
# │ └── media/ (persistent: uploaded images)
# └── var/
# ├── log/ (persistent: application logs)
# └── session/ (persistent: user sessions)
# Nginx document root points to: /var/www/magento/current/pub
3. Shared-Verzeichnisse: Was bleibt, was wechselt
Die wichtigste Designentscheidung bei der Serverstruktur ist die Unterscheidung zwischen release-spezifischen und persistenten Daten. Release-spezifische Daten sind der Quellcode, die kompilierten PHP-Klassen in generated/, der Frontend-Build in pub/static/ und der Composer-Vendor-Ordner. Diese Daten kommen mit jedem Release neu auf den Server und sind Teil des Build-Artefakts.
Persistente Daten hingegen sind alles, was über Releases hinweg konsistent bleiben muss: Medien, die Nutzer hochgeladen haben, Konfigurationsdateien, die serverspezifische Datenbankverbindungen enthalten, Session-Daten für eingeloggte Nutzer und Log-Dateien für die Nachvollziehbarkeit. Diese Daten dürfen durch einen Release niemals überschrieben oder gelöscht werden. Der Symlink-Mechanismus stellt das sicher: Jedes Release-Verzeichnis enthält nur einen Symlink auf das shared-Verzeichnis, nie die Daten selbst.
4. Server-Init-Skript: einmalig ausführen, dann nie wieder anfassen
Das Server-Init-Skript ist das einzige Skript, das manuell und einmalig auf dem Zielserver ausgeführt wird. Es legt alle benötigten Verzeichnisse an, setzt die richtigen Eigentümer und Berechtigungen und platziert die initiale env.php an der richtigen Stelle. Nach diesem ersten Durchlauf liegt die vollständige Verzeichnisstruktur vor, und alle weiteren Deployments laufen vollständig automatisiert über GitLab CI/CD.
# scripts/server-init.sh — Run ONCE on a new server to prepare the directory structure
#!/usr/bin/env bash
set -euo pipefail
# Configuration — override via environment variables before running
APP_PATH="${APP_PATH:-/var/www/magento}"
APP_USER="${APP_USER:-www-data}"
APP_GROUP="${APP_GROUP:-www-data}"
echo "[INIT] Creating base directory structure at ${APP_PATH}"
mkdir -p "${APP_PATH}/releases"
mkdir -p "${APP_PATH}/shared/app/etc"
mkdir -p "${APP_PATH}/shared/pub/media"
mkdir -p "${APP_PATH}/shared/var/log"
mkdir -p "${APP_PATH}/shared/var/session"
# Set ownership — web server must be able to write to shared directories
chown -R "${APP_USER}:${APP_GROUP}" "${APP_PATH}"
# Protect env.php with restricted permissions — no world-readable secrets
chmod 660 "${APP_PATH}/shared/app/etc" || true
echo "[INIT] Directory structure created successfully"
echo "[INIT] Next step: place env.php at ${APP_PATH}/shared/app/etc/env.php"
echo "[INIT] Then run your first GitLab pipeline with a release tag"
5. Symlinks im Deploy-Schritt korrekt setzen
Der Deploy-Schritt besteht aus drei Operationen: Erstens wird das Build-Artefakt per rsync in das neue Release-Verzeichnis übertragen. Zweitens werden alle shared-Verzeichnisse und -Dateien per Symlink in das neue Release eingebunden. Drittens wird der current-Symlink atomar auf das neue Release umgeschaltet. Die Atomarität des dritten Schritts ist entscheidend: Der Befehl ln -sfn erzeugt den neuen Symlink und ersetzt den alten in einer einzigen, unteilbaren Operation — der Webserver sieht zu keinem Zeitpunkt einen inkonsistenten Zustand.
Die Reihenfolge ist wichtig: Symlinks auf shared-Verzeichnisse müssen gesetzt sein, bevor der current-Symlink umgeschaltet wird. Andernfalls könnte ein Request, der das neue Release bereits nutzt, auf fehlende Konfigurationsdateien oder Medienordner treffen. Dieses kurze Fenster reicht aus, um Fehler zu produzieren, die schwer zu reproduzieren und diagnostizieren sind.
deploy:production:
stage: deploy
environment:
name: production
url: https://shop.example.com
script:
# Step 1 — Transfer build artifact to new release directory
- |
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "mkdir -p '${DEPLOY_PATH}/releases/${CI_COMMIT_TAG}'"
- rsync -az --delete --exclude='.git' \
./ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/releases/${CI_COMMIT_TAG}/"
# Step 2 — Link shared files before switching current symlink
- |
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash -s <<'SSH'
set -euo pipefail
RELEASE="${DEPLOY_PATH}/releases/${CI_COMMIT_TAG}"
SHARED="${DEPLOY_PATH}/shared"
# Remove placeholder directories created by rsync
rm -rf "${RELEASE}/app/etc" "${RELEASE}/pub/media" "${RELEASE}/var"
mkdir -p "${RELEASE}/app" "${RELEASE}/pub" "${RELEASE}/var"
# Create symlinks to shared persistent data
ln -sfn "${SHARED}/app/etc" "${RELEASE}/app/etc"
ln -sfn "${SHARED}/pub/media" "${RELEASE}/pub/media"
ln -sfn "${SHARED}/var/log" "${RELEASE}/var/log"
ln -sfn "${SHARED}/var/session" "${RELEASE}/var/session"
# Step 3 — Atomic switch of current symlink (zero downtime)
ln -sfn "${RELEASE}" "${DEPLOY_PATH}/current"
echo "[OK] Switched to release ${CI_COMMIT_TAG}"
SSH
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
when: manual
6. Berechtigungen und Eigentümer korrekt vergeben
Berechtigungsprobleme sind die zweithäufigste Ursache für fehlgeschlagene Magento-Deployments nach der fehlerhaften Verzeichnisstruktur. Der Webserver-Prozess muss in der Lage sein, in pub/media zu schreiben, Sessions in var/session anzulegen und in var/log zu protokollieren. Gleichzeitig dürfen Konfigurationsdateien wie app/etc/env.php nicht für alle lesbar sein, weil sie Datenbankpasswörter und API-Schlüssel enthalten.
Die empfohlene Konfiguration ist: Alle Dateien im Release-Verzeichnis gehören dem Deploy-User, der die Pipeline ausführt. Der Webserver-User (zum Beispiel www-data) ist Mitglied der Gruppe des Deploy-Users. Verzeichnisse wie pub/media haben die Berechtigung 775, damit der Webserver schreiben kann. Die env.php hat 640, damit nur Root und die zugehörige Gruppe lesen können. Diese Konfiguration muss in das Init-Skript und in den Deploy-Job eingebaut sein — nichts davon darf manuell "gefixed" werden.
7. Verify-Job in GitLab: Struktur automatisch prüfen
Nach jedem Deployment prüft ein Verify-Job, ob die Serverstruktur korrekt ist. Das ist nicht nur eine Bestätigung, dass das Deployment funktioniert hat — es ist auch ein Frühwarnsystem für strukturelle Abweichungen. Wenn ein Symlink fehlt oder ein Verzeichnis die falschen Berechtigungen hat, schlägt der Verify-Job fehl, bevor der erste echte Nutzer-Request auf das Problem trifft.
verify:server-structure:
stage: verify
script:
- |
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash -s <<'SSH'
set -euo pipefail
CURRENT="${DEPLOY_PATH}/current"
# Verify that current symlink exists and points to a valid release
test -L "${CURRENT}" || { echo "[FAIL] current is not a symlink"; exit 1; }
test -d "${CURRENT}" || { echo "[FAIL] current target does not exist"; exit 1; }
# Verify that shared symlinks are in place
test -L "${CURRENT}/app/etc" || { echo "[FAIL] app/etc symlink missing"; exit 1; }
test -L "${CURRENT}/pub/media" || { echo "[FAIL] pub/media symlink missing"; exit 1; }
test -L "${CURRENT}/var/log" || { echo "[FAIL] var/log symlink missing"; exit 1; }
# Verify env.php exists and is readable
test -f "${DEPLOY_PATH}/shared/app/etc/env.php" \
|| { echo "[FAIL] env.php not found in shared"; exit 1; }
echo "[OK] Server structure verified for $(readlink ${CURRENT})"
SSH
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
when: on_success
8. Vergleich: flache Struktur vs. Release-Modell
Viele Magento-Projekte starten mit einer flachen Serverstruktur: Der Code liegt direkt in /var/www/magento, Deployments überschreiben Dateien direkt per rsync oder git pull. Das funktioniert — bis zum ersten Fehler. Dann zeigt sich, dass kein Rollback möglich ist, weil das vorherige Release nicht mehr existiert, und kein atomares Umschalten möglich ist, weil es keinen Symlink-Mechanismus gibt.
| Merkmal | Flache Struktur | Release-Modell (releases/+shared/) | Vorteil |
|---|---|---|---|
| Rollback | Nicht möglich ohne Backup | Symlink auf altes Release umschalten | Sofortiger Rollback in Sekunden |
| Zero Downtime | Nicht erreichbar | Atomarer Symlink-Wechsel | Kein Ausfallzeitraum |
| Mediendateien | Können beim Deploy überschrieben werden | In shared/ – nie berührt | Datenverlust ausgeschlossen |
| Konfiguration | Manuell gepflegt im Webroot | In shared/app/etc/ – versionsfrei | Sicher und kontrolliert |
| Parallele Deployments | Race Conditions beim Überschreiben | Jedes Release in eigenem Verzeichnis | Keine gegenseitige Beeinflussung |
9. Zusammenfassung
Eine korrekt vorbereitete Serverstruktur ist die Grundlage jedes Zero-Downtime-Deployments für Magento. Das Server-Init-Skript legt releases/, current und shared/ mit allen notwendigen Unterverzeichnissen an. Der Deploy-Job überträgt das Build-Artefakt, setzt Symlinks auf alle shared-Verzeichnisse und schaltet den current-Symlink atomar um. Der Verify-Job prüft nach jedem Deployment, ob die Struktur korrekt ist.
Wer diesen Aufbau einmalig sauber erledigt, hat danach kein manuelles Eingreifen mehr nötig: Jeder Release landet in einem eigenen Verzeichnis, persistente Daten bleiben unberührt, Rollbacks sind in Sekunden möglich und die Deployment-Geschichte ist durch die Verzeichnisnamen direkt auf dem Server ablesbar.
Release-Verzeichnisse und Serverstruktur — Das Wichtigste auf einen Blick
Grundstruktur
releases/ + current-Symlink + shared/ — einmalig mit Server-Init-Skript anlegen. Niemals manuell nachpflegen.
Shared-Verzeichnisse
shared/app/etc/, shared/pub/media/, shared/var/log/, shared/var/session/ — persistent über alle Releases.
Deploy-Reihenfolge
1. Artefakt übertragen. 2. Symlinks auf shared setzen. 3. current-Symlink atomar umschalten. Reihenfolge nie verändern.
Verify-Job
Symlinks, env.php-Präsenz und Berechtigungen automatisch prüfen — frühzeitig warnen, bevor Nutzer-Requests betroffen sind.
10. Typische Fehler bei der Serverstruktur
Der klassische Fehler ist das Vergessen von pub/media im shared-Verzeichnis. Das fällt nicht sofort auf — erst wenn ein Nutzer ein Bild hochlädt, das neue Release deployt wird und das Bild im neuen Release-Verzeichnis nicht mehr vorhanden ist. Dann ist klar, dass pub/media nicht als Symlink auf shared/pub/media zeigt, sondern als echtes Verzeichnis im Release-Ordner liegt.
Ein weiterer verbreiteter Fehler ist das Setzen von Symlinks mit absolutem statt relativem Pfad. Wenn sich der Basispfad des Deployments ändert — zum Beispiel bei einer Server-Migration — zeigen alle bestehenden Symlinks auf Pfade, die nicht mehr existieren. Generell empfehlen wir, Symlinks immer mit absoluten Pfaden zu setzen, aber diese Pfade aus Umgebungsvariablen zu beziehen, damit sie bei einer Migration zentral angepasst werden können.
11. FAQ: Release-Verzeichnisse und Serverstruktur für Magento
1Serverstruktur manuell oder automatisch?
2Was passiert mit pub/media beim Rollback?
3Wie viele alte Releases behalten?
4Staging und Production auf demselben Server?
5Wie env.php absichern?
6ln -sfn vs. ln -sf?
ln -sfn für Verzeichnis-Symlinks — verhindert, dass der neue Link in ein Unterverzeichnis des Ziels gesetzt wird. Immer -sfn für den current-Symlink.