Es war ein Freitagabend. Jemand hatte eine neue Version der Anwendung via FTP hochgeladen – ein paar Dateien, wie immer. Nur diesmal fehlte eine. Die Seite war offline, der Fehler nicht sofort klar, und das letzte funktionierende Deployment? Irgendwo auf einer lokalen Festplatte, vermutlich. Diese Geschichte kenne ich nicht nur einmal. Hier ist, wie ich mit einem Kunden systematisch den Weg raus gebaut habe.
Die Ausgangssituation
Das Projekt: eine PHP-Anwendung für Auftragserfassung und Rechnungsstellung, gewachsen über etwa zehn Jahre. Keine Versionskontrolle. Deployment bedeutete: Filezilla öffnen, geänderte Dateien auswählen, hochladen, hoffen. Wer welche Version hatte, war unklar. Zwei Entwickler arbeiteten gelegentlich am Code – jeder mit seiner eigenen lokalen Kopie, synchronisiert durch gegenseitiges Überschreiben.
Der Auslöser für die Modernisierung war nicht mangelnder Wille, sondern ein konkreter Schaden: Ein vergessener Upload einer geänderten Datenbankklasse führte zu einem Produktionsausfall von knapp vier Stunden. Die Wiederherstellung war mühsam, weil niemand wusste, welche Dateiversion auf dem Server die letzte funktionierende war.
Phase 1: Git einführen
Bevor irgendetwas automatisiert werden kann, braucht man eine Quelle der Wahrheit. Der erste Schritt war
schlicht: git init, alle vorhandenen Dateien committen, Repository auf GitLab anlegen, beide
Entwickler einrichten. Kein Branching-Modell, kein Workflow – nur die Grundlage.
Das allein hat schon geholfen. Man konnte erstmals nachvollziehen, wer was wann geändert hat. Der psychologische Effekt war nicht zu unterschätzen: Das Deployment fühlte sich weniger wie Roulette an.
Phase 2: GitLab CI – erst nur Syntax-Check
Bevor ich anfange, Deployment zu automatisieren, will ich sicherstellen, dass zumindest offensichtliche Fehler vor dem Upload auffliegen. Eine einfache Pipeline prüft zunächst nur die PHP-Syntax:
stages:
- lint
php-syntax-check:
stage: lint
image: php:8.5-cli
script:
- find . -name "*.php" -not -path "./vendor/*" | xargs -I{} php -l {}
Kein Framework, kein Composer, keine Tests – nur php -l. Das klingt trivial, hat aber in den
ersten zwei Wochen bereits drei Syntaxfehler abgefangen, die sonst live gegangen wären.
Phase 3: Deployment mit rsync statt FTP
Der FTP-Upload wurde durch einen rsync-Schritt in der Pipeline ersetzt. Der Server bekommt einen
Deploy-SSH-Key, der nur Schreibrechte auf das Web-Verzeichnis hat. Die .gitlab-ci.yml:
stages:
- lint
- deploy
php-syntax-check:
stage: lint
image: php:8.2-cli
script:
- find . -name "*.php" -not -path "./vendor/*" | xargs -I{} php -l {}
deploy:
stage: deploy
image: alpine:latest
only:
- main
before_script:
- apk add --no-cache rsync openssh-client
- eval $(ssh-agent -s)
- echo "$DEPLOY_KEY" | ssh-add -
script:
- rsync -avz --delete
--exclude='.git'
--exclude='*.env'
./ deploy@meinserver.de:/var/www/html/
Das --delete-Flag sorgt dafür, dass nicht mehr vorhandene Dateien auch auf dem Server entfernt
werden – das war vorher nie der Fall und hat zu einem Friedhof veralteter Skripte geführt.
Der erste Rollback nach Einführung der Pipeline dauerte zwei Minuten: git revert, push,
fertig. Vorher wäre das Stunden gewesen – wenn überhaupt möglich.
Fazit
Die Migration von FTP zu CI/CD muss nicht in einem großen Sprung passieren. Git zuerst, dann Qualitätssicherung, dann automatisiertes Deployment – jede Phase liefert für sich bereits messbaren Mehrwert. Der technische Aufwand für die gesamte Umstellung lag bei diesem Projekt unter zwei Arbeitstagen. Was länger gedauert hat, war das Vertrauen aufzubauen – dass der Prozess zuverlässiger ist als das menschliche Gedächtnis beim Freitagabend-Deployment.