Wenn Ihr Frontend einen Server braucht: Eine CI/CD-Pipeline für SSR-Anwendungen
Sie haben gerade ein neues Feature in Ihrer Next.js-App fertiggestellt. Der Build läuft lokal durch. Sie pushen in die Produktion. Doch statt einer funktionierenden Seite sehen Ihre Nutzer nur einen leeren Bildschirm mit einem sich drehenden Ladekreis. Der Serverprozess wurde gestartet, war aber noch nicht bereit, Anfragen zu bearbeiten.
In diesem Moment wird klar: Das Deployment einer serverseitig gerenderten Frontend-Anwendung unterscheidet sich fundamental vom Deployment einer statischen Website. Die Pipeline, die für Ihre React SPA oder eine statische Gatsby-Seite funktioniert hat, reicht hier nicht mehr aus.
Der Kernunterschied: Sie deployen einen Server, keine Dateien
Bei einem statischen Frontend produziert Ihr Build HTML-, CSS- und JavaScript-Dateien. Sie laden diese auf ein CDN oder in einen Storage-Bucket hoch – fertig. Das Deployment ist im Wesentlichen ein Dateikopiervorgang.
Beim Server-Side Rendering (SSR) enthält das Build-Output serverseitigen Code, der als Prozess laufen muss. Frameworks wie Next.js, Nuxt oder Remix erzeugen ein Verzeichnis mit:
- JavaScript-Code, der auf dem Server läuft
- JavaScript-Bundles für den Client
- Statischen Assets wie Bilder und Schriftarten
- Einer Einstiegsdatei (oft
server.js), die die Anwendung startet
Ihre Pipeline muss dieses Output nun als eine kontinuierlich laufende Anwendung behandeln – nicht als eine Sammlung von Dateien, die einfach nur ausgeliefert werden. Das ändert alles daran, wie Sie bauen, testen und deployen.
Schritt 1: Mit dem richtigen Ziel bauen
Der Build-Schritt ähnelt auf den ersten Blick dem eines statischen Frontends. Sie führen npm run build oder den entsprechenden Befehl Ihres Frameworks aus. Aber das Output ist anders – und auch das, was Sie damit tun.
Für SSR muss das Build-Output in etwas verpackt werden, das auf einem Server laufen kann. Wenn Sie Container verwenden, bedeutet das, ein Docker-Image zu erstellen, das Folgendes enthält:
- Den gebauten Server-Code
- Laufzeitabhängigkeiten (Node.js-Version, Systembibliotheken)
- Konfigurationsdateien, die zur Laufzeit benötigt werden
- Das Einstiegsskript
Ihr Dockerfile könnte etwa so aussehen:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY .next ./.next
COPY public ./public
EXPOSE 3000
CMD ["node", ".next/standalone/server.js"]
Der entscheidende Punkt: Sie kopieren nicht den gesamten Quellcode, sondern nur das, was zum Ausführen der Anwendung nötig ist. Das hält das Image klein und reduziert die Angriffsfläche.
Schritt 2: Health Checks sind kein optionales Extra
Hier scheitern viele SSR-Pipelines. Der Container startet, der Prozess läuft, und alle gehen davon aus, dass die Anwendung funktioniert. Aber „Der Prozess läuft“ ist nicht dasselbe wie „Die Anwendung kann Anfragen bedienen“.
Ihre Anwendung kann zwar erfolgreich starten, aber trotzdem keine Seiten rendern, weil:
- Eine Datenbankverbindung zeitlich ausfällt
- Eine externe API nicht erreichbar ist
- Umgebungsvariablen fehlen
- Ein benötigter Dienst noch nicht bereit ist
Fügen Sie Ihrer Anwendung einen Health-Check-Endpunkt hinzu. Üblicherweise liegt dieser unter /health oder /api/health und gibt einen 200-Status zurück, wenn die App tatsächlich Anfragen verarbeiten kann. Ihre Pipeline muss diesen Endpunkt nach dem Deployment aufrufen, bevor sie Traffic auf die neue Version lenkt.
Wenn der Health Check fehlschlägt, stoppen Sie die Pipeline. Lassen Sie nicht zu, dass Nutzer Fehlerseiten oder endlose Ladezustände sehen. Das Team untersucht das Problem, behebt es und führt die Pipeline erneut aus.
Schritt 3: Wählen Sie Ihre Deployment-Strategie
Es gibt zwei gängige Wege, SSR-Anwendungen zu deployen: direkt auf einen Server oder in Container. Beide haben unterschiedliche Auswirkungen auf Ihre Pipeline.
Das folgende Flussdiagramm zeigt die vollständige SSR-Pipeline vom Build bis zum Deployment mit dem kritischen Health-Check-Entscheidungspunkt.
Direktes Server-Deployment
Sie kopieren das Build-Output auf einen Server, stoppen den alten Prozess und starten den neuen. Die entscheidende Frage ist, wie Sie den Übergang gestalten.
Wenn Sie den alten Prozess sofort beenden, schlagen alle gerade bearbeiteten Anfragen fehl. Nutzer sehen mitten in der Aktion Fehler. Die Lösung ist ein Graceful Shutdown: Der alte Server nimmt keine neuen Anfragen mehr an, beendet aber die bereits laufenden. Sobald diese abgeschlossen sind, beendet sich der Prozess sauber. Dann startet der neue Server und beginnt, Traffic anzunehmen.
Ihr Pipeline-Skript muss diese Übergabe koordinieren. Das ist machbar, erfordert aber sorgfältige Skripterstellung und Überwachung.
Container-Deployment
Container geben Ihnen mehr Kontrolle. Die Pipeline baut ein neues Docker-Image, pusht es in eine Registry und deployed es auf Ihrer Container-Orchestrierungsplattform.
Hier ist ein minimales Dockerfile, das die gebaute SSR-Anwendung für das Container-Deployment paketiert:
FROM node:20-alpine
WORKDIR /app
# Produktionsabhängigkeiten kopieren
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Gebauten Server und Client-Assets kopieren
COPY .next ./.next
COPY public ./public
# Port freigeben, auf dem die App lauscht
EXPOSE 3000
# Server starten
CMD ["node", ".next/standalone/server.js"]
Mit Kubernetes wird daraus ein Rolling Update:
- Ein neuer Pod startet mit dem neuen Image
- Der Pod führt seinen Health Check aus
- Sobald er gesund ist, wird der Traffic schrittweise auf den neuen Pod umgeleitet
- Alte Pods werden beendet, nachdem sie ihre aktuellen Anfragen abgeschlossen haben
Kubernetes kümmert sich automatisch um Graceful Shutdown und Traffic-Umleitung. Ihre Pipeline muss lediglich das Deployment-Manifest mit dem neuen Image-Tag aktualisieren und anwenden.
Schritt 4: Tracken, was läuft
Nach dem Deployment sollte Ihre Pipeline aufzeichnen, welche Version läuft. Speichern Sie den Commit-Hash, den Image-Tag oder den Deployment-Zeitstempel an einem zugänglichen Ort. Diese Information ist unverzichtbar, wenn Sie:
- Auf eine vorherige Version zurückrollen müssen
- Untersuchen müssen, welches Deployment einen Fehler eingeführt hat
- Leistungsprobleme mit bestimmten Releases korrelieren müssen
Ein einfacher Ansatz: Taggen Sie Ihre Docker-Images mit dem Commit-Hash und speichern Sie die Zuordnung in einer Datenbank oder einer einfachen Textdatei. Ihre Monitoring-Tools können dann beim Alarmieren auf diese Daten verweisen.
Praxis-Checkliste für Ihre SSR-Pipeline
Bevor Sie Ihre Pipeline als produktionsreif bezeichnen, überprüfen Sie diese Punkte:
- Build-Output ist in ein deployierbares Artefakt verpackt (Docker-Image oder Server-Paket)
- Health-Check-Endpunkt existiert und liefert einen aussagekräftigen Status
- Pipeline wartet auf erfolgreichen Health Check, bevor sie Traffic umleitet
- Pipeline stoppt und alarmiert, wenn der Health Check fehlschlägt
- Graceful Shutdown ist konfiguriert (alter Prozess beendet laufende Anfragen)
- Rolling-Update-Strategie ist getestet (keine Ausfallzeit während des Deployments)
- Versionsinformationen werden nach dem Deployment gespeichert und sind zugänglich
- Rollback-Prozess ist dokumentiert und getestet
Das Fazit
Ein SSR-Frontend ist keine statische Website. Es ist eine Serveranwendung, die zufällig HTML rendert. Behandeln Sie Ihre Pipeline entsprechend: Bauen Sie für die Laufzeit, verifizieren Sie mit Health Checks, deployen Sie mit Zero-Downtime-Strategien, und wissen Sie immer, welche Version Ihre Nutzer bedient. Wenn Sie diese Grundlagen richtig umsetzen, sehen Ihre Nutzer nie den leeren Bildschirm. Sie sehen nur eine schnelle, funktionierende Seite.