Von Code bis Prod: Wie wir unsere Delivery-Pipeline neu aufgestellt haben
Im Zuge des Neoground-Relaunchs und mit Blick auf Charm v4 haben wir den Weg unserer Webanwendungen vom Repository bis in Produktion grundlegend neu aufgebaut: mit isolierter CI, reproduzierbaren Build-Umgebungen, verifizierten Artefakten und kontrollierten Deployments über Compass.

Eine Deployment-Pipeline wird leicht unterschätzt.
Auf den ersten Blick wirkt das Deployment einer Webanwendung wie ein überschaubarer Vorgang: den aktuellen Code abrufen, Abhängigkeiten installieren, Assets bauen, einige anwendungsspezifische Befehle ausführen und die neue Version bereitstellen.
Dieses Modell kann erstaunlich gut funktionieren.
Es kann aber ebenso dazu führen, dass sich Verantwortlichkeiten an Stellen ansammeln, an die sie langfristig nicht gehören.
Im Zuge des vollständigen Relaunchs der Neoground-Website und unserer laufenden Arbeit an Charm v4 haben wir deshalb grundlegend überprüft, wie unsere Anwendungen heute vom Source Repository bis in Produktion gelangen.
Entstanden ist ein neues Delivery-Modell, das einem einfachen Prinzip folgt:
Produktion sollte vorbereitete Software ausführen – nicht erst herstellen.
Aus diesem Grund trennen wir Build, Artifact Handling und Deployment heute bewusst voneinander. Jede dieser Ebenen hat eigene Aufgaben, eigene Berechtigungen und eine klar definierte Vertrauensgrenze.
Die erste Anwendung, die diese neue Pipeline vollständig durchlaufen hat, war unsere neue Unternehmenswebsite.
Das bisherige Modell
Auch unser bisheriger Deployment-Prozess war bereits automatisiert.
Ein Webhook konnte ein Update auf dem Zielsystem auslösen, worauf die üblichen Build-Schritte der Anwendung folgten:
Source Repository
↓
Deployment-Trigger
↓
Produktionssystem
↓
Composer
↓
Frontend-Abhängigkeiten
↓
Asset-Build
↓
anwendungsspezifische Deployment-Schritte
Für kleinere Systeme ist dieser Ansatz pragmatisch und unkompliziert zu betreiben.
Der Nachteil: Das Produktionssystem übernimmt zwei Rollen gleichzeitig:
- es ist Runtime-Umgebung;
- und zugleich Build-Umgebung.
Damit benötigt es Zugriff auf Package Registries, Frontend-Tooling, Dependency Resolver und verschiedene Build-Abhängigkeiten, die nach dem Start der Anwendung auf einem Produktionsserver eigentlich keinen Zweck mehr erfüllen.
Zugleich hängt ein Deployment teilweise vom konkreten Zustand dieses Servers ab.
Mit der zunehmenden Reife unserer Anwendungen, Infrastruktur und internen Werkzeuge wurde immer deutlicher, dass dies für uns nicht mehr die richtige Systemgrenze war.
Den Build aus der Produktion herauslösen
Das neue Modell trennt Source, Build und Deployment:
Source
↓
CI
↓
Produktionsartefakt
↓
Artifact Management
↓
kontrolliertes Deployment
Ein Produktionsartefakt ist damit kein ausgecheckter Quellcode mehr, der auf dem Zielsystem erst zusammengesetzt werden muss.
Es enthält die Anwendung bereits in genau dem Zustand, den wir deployen möchten.
Für eine typische Charm-basierte Website übernimmt die CI unter anderem folgende Aufgaben:
- Auschecken exakt der vorgesehenen Source-Revision;
- Setzen der vorgesehenen Produktionsumgebung;
- Installation der Composer-Abhängigkeiten ohne Development Packages;
- immutable Installation der Frontend-Abhängigkeiten;
- Kompilierung der Sass- und JavaScript-Assets;
- Entfernen ausschließlich für den Build benötigter Abhängigkeiten;
- Validierung des resultierenden Application Trees;
- Erzeugung eines komprimierten Release-Artefakts.
Das Zielsystem muss diese Arbeiten nicht mehr selbst durchführen.
CI braucht eine eigene Vertrauensgrenze
Eine architektonische Anforderung war für uns früh klar: Der CI Runner sollte nicht direkt auf einem wichtigen Anwendungs- oder Infrastrukturserver ausgeführt werden.
CI-Systeme führen ihrem Wesen nach Anweisungen aus, die aus Repositories stammen.
Sobald dabei Container-Tooling ins Spiel kommt, werden die damit verbundenen Privilegien besonders relevant. Ein Workflow mit weitreichender Kontrolle über eine Docker-Umgebung kann erhebliche Befugnisse gegenüber seinem Host erlangen.
Unsere Build-Umgebung ist deshalb bewusst von der Produktionsinfrastruktur isoliert.
In unserer heutigen Größenordnung schafft eine dedizierte virtuelle Maschine diese Grenze effizient:
Forgejo
↓
dedizierte Runner-Umgebung
↓
Docker
↓
ephemerer Build-Container
Die konkrete Implementierung kann mit der Auslastung wachsen.
Bei höheren Anforderungen könnte dieselbe Rolle auf eine dedizierte Appliance, eine Virtualisierungsplattform oder eine externe Compute-Instanz verlagert werden, ohne die Workflows der Anwendungen wesentlich verändern zu müssen.
Entscheidend ist nicht, wo genau der Runner ausgeführt wird.
Entscheidend ist, dass Build-Workloads und Produktions-Workloads nicht dieselbe Vertrauensgrenze teilen.
Eine standardisierte Neoground-Build-Umgebung
Der Runner selbst ist dabei noch nicht die eigentliche Build-Umgebung der Anwendung.
Für Charm-basierte Anwendungen haben wir ein wiederverwendbares Container-Image erstellt, das den gemeinsamen Toolchain unserer aktuellen Softwarelandschaft enthält.
Dazu gehören unter anderem:
Debian 13
PHP 8.5
Composer
Node.js 24 LTS
Yarn 4
Valkey-kompatibles PHP-Tooling
Git
OpenSSH
rsync
tar
Zstandard
gängige PHP-Extensions
Das Image stellt eine stabile Werkzeugumgebung bereit.
Das Repository definiert dagegen die projektspezifischen Abläufe.
Dadurch können unterschiedliche Anwendungen dieselbe Build-Grundlage verwenden, ohne Anwendungscode, Konfigurationen oder Zugangsdaten in das Image integrieren zu müssen.
Eine typische Projekt-Pipeline kann entsprechend kompakt bleiben:
checkout
↓
composer install --no-dev
↓
yarn install --immutable
↓
build assets
↓
validate
↓
package
Wie ein Projekt konkret gebaut wird, bleibt Teil des Projekts.
Die Umgebung, in der dieser Build stattfindet, wird standardisiert.
Reproduzierbarkeit macht versteckte Annahmen sichtbar
Einer der Vorteile eines Builds in einer sauberen Umgebung besteht darin, dass bislang implizite Annahmen plötzlich sichtbar werden.
Bei der ersten Migration stellte eine immutable Yarn-Installation beispielsweise unmittelbar fest, dass das Projekt noch Zustände aus einer älteren Yarn-Generation mitführte.
Die bestehende Entwicklungsumgebung konnte mit dieser Historie umgehen.
Eine saubere CI-Umgebung verweigerte dagegen richtigerweise, während eines Produktions-Builds eigenständig den Dependency State umzuschreiben.
Genau dieses Verhalten wollen wir.
Eine Produktions-Pipeline sollte nicht stillschweigend entscheiden, dass sich ihr Abhängigkeitsgraph verändern muss.
Stattdessen beschreibt das Repository nun explizit den erwarteten Zustand des Package Managers, während die CI diesen Zustand überprüft.
Dasselbe Prinzip gilt für den gesamten Build:
Wenn ein Release von etwas abhängt, sollte diese Abhängigkeit sichtbar und reproduzierbar sein.
Vom Repository zum Artefakt
Sobald der Produktionsstand vollständig aufgebaut ist, verpackt die CI ihn in ein komprimiertes Zstandard-Archiv.
Ein Release besteht konzeptionell aus drei Objekten:
application.tar.zst
application.tar.zst.sha256
application.json
Das Archiv enthält die vorbereitete Anwendung.
Die Checksumme bestätigt ihre Integrität.
Strukturierte Metadaten beschreiben das Release.
Diese Metadaten können beispielsweise folgende Informationen enthalten:
{
"project": "example",
"source": {
"commit": "...",
"ref": "main"
},
"artifact": {
"filename": "...",
"size_bytes": 13000000,
"sha256": "..."
},
"build": {
"environment": "Prod",
"php": "8.5.x",
"node": "v24.x",
"yarn": "4.x"
}
}
Das Source Repository bleibt die maßgebliche Historie des Quellcodes.
Das Artefakt beschreibt hingegen, was aus genau diesem Code tatsächlich gebaut wurde.
Diese Unterscheidung gewinnt erheblich an Wert, sobald Releases später nachvollzogen, verglichen, aufbewahrt oder erneut deployt werden sollen.
CI deployt nicht
Eine weitere bewusst gesetzte Grenze: Die CI-Pipeline endet nach der Veröffentlichung.
Das Build-System darf ein Release erzeugen.
Es entscheidet jedoch nicht darüber, ob dieses Release produktiv geschaltet wird.
Stattdessen veröffentlicht die CI potenzielle Releases in einem stark eingeschränkten Ingress-Pfad.
Die hierfür verwendeten Zugangsdaten sind ausschließlich für diesen Zweck vorgesehen und gewähren keinen allgemeinen administrativen Zugriff auf die Zielinfrastruktur.
Konzeptionell:
CI
↓
eingeschränkte Artefakt-Publikation
↓
Inbox
Ab diesem Punkt übernimmt unsere Infrastruktur-Ebene.
Dadurch begrenzen wir die Befugnisse des Build-Systems und halten Deployment-Entscheidungen innerhalb jener Werkzeuge, die tatsächlich für den Betrieb der Infrastruktur verantwortlich sind.
Wo Compass übernimmt
Mit Compass haben wir vor Kurzem unsere Infrastruktur-Management-Ebene vorgestellt, mit der wir die Systeme hinter Neoground-Diensten und -Anwendungen betreiben.
Der neue Artifact Workflow fügt sich unmittelbar in diese Architektur ein.
Sobald ein Release die Artifact-Ingress-Ebene erreicht, können Compass und das zugrunde liegende Provisioning-System den weiteren Lebenszyklus übernehmen:
Artifact Inbox
↓
Validierung
↓
Übernahme der Metadaten
↓
Release-Registrierung
↓
Artifact Storage
↓
Deployment-Auswahl
↓
kontrollierter Rollout
Release-Metadaten können gemeinsam mit dem übrigen Systemzustand gespeichert werden. Eine konkrete Anwendungsversion lässt sich dadurch später über die jeweilige Administrationsoberfläche oder das entsprechende Command-Line-Tool auswählen.
Daraus ergibt sich für uns eine klare Aufgabenteilung:
CI
Verantwortlich für:
- Source Checkout;
- Dependency Resolution;
- Kompilierung;
- Tests und Validierung;
- Erstellung des Artefakts;
- Integritätsmetadaten;
- Veröffentlichung des Artefakts.
Compass und Provisioning
Verantwortlich für:
- Übernahme von Artefakten;
- Release-Inventar;
- Autorisierung von Deployments;
- Auswahl des Zielsystems;
- Deployment auf Dateisystemebene;
- Application Lifecycle Hooks.
Diese Trennung hält den gesamten Delivery-Prozess nachvollziehbar, auch wenn die Systeme darum herum weiter wachsen.
Auch ein Deployment muss die Anwendung verstehen
Ein Artefakt bildet lediglich den definierten Ausgangszustand der Anwendung ab.
Für ein korrektes Deployment muss dennoch klar sein, welche Teile eines laufenden Systems austauschbar und welche persistent sind.
Charm-Anwendungen können persistente anwendungsspezifische Daten, Logs und Runtime-Cache-Strukturen enthalten, die ein Code-Deployment überleben müssen.
Die Provisioning-Ebene überschreibt deshalb nicht schlicht das bestehende Live-Verzeichnis.
Stattdessen arbeitet sie auf Basis eines sauber extrahierten Releases und gleicht diesen Zustand mit der Zielanwendung ab. Persistente Runtime-Daten, die zur konkreten Installation und nicht zum Release gehören, bleiben dabei erhalten.
Konzeptionell:
verifiziertes Artefakt
↓
sauberer Staging Tree
↓
Abgleich mit dem Zielsystem
│
├── persistente Daten erhalten
├── Logs erhalten
└── disposable Cache-Zustand zurücksetzen
↓
Post-Deploy-Lifecycle der Anwendung
An diesem Punkt wird das Framework selbst Teil des Deployment-Vertrags.
Charm übernimmt die anwendungsspezifische Deployment-Logik
Charm stellt über Bob anwendungsbezogene Werkzeuge bereit, darunter projektspezifische Deployment-Kommandos.
Nachdem die Provisioning-Ebene den neuen Anwendungszustand installiert hat, kann die Anwendung selbst jene Operationen durchführen, deren Bedeutung nur sie kennt.
Je nach Projekt können dazu beispielsweise gehören:
- Datenbankmigrationen;
- Generierung von Sitemaps;
- Invalidierung von Application Caches;
- Aktualisierung generierter Metadaten;
- projektspezifische Maintenance-Operationen.
Das Infrastruktursystem muss nicht verstehen, weshalb eine bestimmte Anwendung ihre Sitemap neu aufbauen muss.
Es muss lediglich einen verlässlichen Lifecycle-Punkt bereitstellen, an dem die Anwendung diese Aufgabe selbst ausführen kann.
Auch dieser Ansatz ist Teil der laufenden Modernisierung rund um Charm v4.
Ein Framework besteht nicht nur aus dem Code, der während eines HTTP-Requests ausgeführt wird. Es wirkt ebenso in Entwicklung, Testing, Deployment und Betrieb hinein.
Die neue Release-Pipeline schafft zwischen diesen Ebenen eine deutlich klarere Schnittstelle.
Die neue Neoground-Website als erstes vollständiges Deployment
Das erste Projekt, das unsere neue Pipeline vollständig nutzt, ist die Website, auf der Sie diesen Artikel gerade lesen.
Damit ist die Arbeit an der Deployment-Infrastruktur zugleich Teil eines größeren Meilensteins.
Die Website selbst ist ein vollständiger Neuaufbau:
- ein neues Repository;
- eine neue visuelle Identität;
- ein weitgehend individuell entwickeltes Frontend statt einer generischen Bootstrap-Basis;
- ein strukturierteres Komponenten- und Interaktionssystem;
- eine modernisierte Charm-Anwendung im Hintergrund;
- und nun auch eine vollständig überarbeitete Delivery-Pipeline darum herum.
Ein vollständiger Produktions-Build benötigt aktuell weniger als eine Minute. Darin enthalten sind Dependency Handling, Frontend-Kompilierung, Erstellung des Artefakts, Checksum-Generierung, Erzeugung der Metadaten und Veröffentlichung.
Das daraus entstehende vollständige Website-Artefakt ist zugleich kompakt genug, dass Speicherbedarf und Übertragung operativ kaum ins Gewicht fallen.
Genau solche Ergebnisse bevorzugen wir: mehr technische Disziplin, ohne dafür unnötige Plattformkomplexität einzuführen.
Modern muss nicht kompliziert bedeuten
Für Software Delivery existieren erheblich größere Systeme.
Für die richtigen Anforderungen sind sie vollkommen gerechtfertigt.
Unser Ziel war hier ein anderes.
Wir wollten explizite Vertrauensgrenzen, reproduzierbare Builds, verifizierbare Artefakte und kontrollierte Deployments – ohne eine Infrastruktur einzuführen, deren Komplexität die des eigentlichen Problems übersteigt.
Der resultierende Stack basiert weiterhin auf bemerkenswert gewöhnlichen Komponenten:
Git
Forgejo
Linux
Docker
PHP
Node.js
Composer
Yarn
tar
Zstandard
SHA-256
SSH
rsync
Die entscheidende Ingenieursarbeit liegt nicht darin, möglichst exotische Werkzeuge einzusetzen.
Sie liegt darin, wie diese Komponenten voneinander getrennt werden und wie Autorität zwischen ihnen fließt.
CI darf bauen.
CI darf veröffentlichen.
CI darf Produktion nicht administrieren.
Artefakte lassen sich verifizieren.
Die Infrastruktur entscheidet, was tatsächlich deployt wird.
Und die Anwendungen bleiben für ihre eigene Post-Deployment-Semantik verantwortlich.
Das resultierende System ist vergleichsweise klein – sein Vertrag zwischen den einzelnen Ebenen ist jedoch wesentlich stärker.
Strategische Technologie, konsequent bis zur Umsetzung
Unsere aktuelle Positionierung lautet:
Strategische Technologie, konsequent bis zur Umsetzung.
Dieser Anspruch gilt intern genauso wie für unsere Arbeit mit Kunden.
Architektur ist relevant.
Sicherheitsgrenzen sind relevant.
Operative Einfachheit ist relevant.
Am Ende muss das System aber vor allem funktionieren.
Das Ergebnis ist in diesem Fall bewusst unspektakulär: Ein Entwickler kann eine Änderung pushen, eine reproduzierbare Umgebung baut daraus die Anwendung, ein verifiziertes Release gelangt in die Infrastruktur-Ebene und die gewünschte Version kann anschließend über dieselben Werkzeuge ausgerollt werden, mit denen wir auch den übrigen Systembetrieb steuern.
Die Website ist das sichtbare Resultat.
Die technische Architektur darunter sorgt dafür, dass dieses Resultat verlässlich bleibt.