
Netlify hat nichts falsch gemacht. Trotzdem wollte ich wissen, wie echte Kubernetes-Kontrolle sich anfühlt. 30 Tage OpenShift Developer Sandbox, eine schnellere Website – und die ehrliche Antwort warum wir trotzdem geblieben sind.
Warum OpenShift – und warum wieder weg?
Netlify hat nichts falsch gemacht. Railway auch nicht. Trotzdem wollte ich wissen, wie es sich anfühlt, wenn man selbst die Kontrolle hat – kein "Push and Deploy", kein Magic-Deployment, keine Abstraktion auf Abstraktion. OpenShift Developer Sandbox ist kostenlos, läuft auf Kubernetes, und es hat mich einen halben Tag gekostet, bis die erste Next.js-Seite oben war. Dieser Artikel ist das, was ich vorher gebraucht hätte.
Der nächste Schritt: die Spring Boot Microservices von Railway nach OpenShift migrieren. Aber zuerst musste das Frontend laufen.
Die nginx-Falle: Kein root auf OpenShift
Der erste Versuch war ein Standard-nginx-Container. OpenShift hat ihn sofort abgeschossen. Kein hilfreicher Fehler, nur CrashLoopBackOff. Die Ursache: OpenShift erlaubt per Default keine root-Container. nginx läuft auf Port 80, und Port 80 braucht root. Das ist auf normalen Servern kein Problem – auf OpenShift ist es ein Showstopper.
OpenShift Security Context Constraint (SCC) Standard nginx (Port 80, root) ───────────────────────────── Container startet → OpenShift prüft SCC → User 0 (root) verboten → CrashLoopBackOff ✗ nginxinc/nginx-unprivileged (Port 8080, non-root) ────────────────────────────────────────────────── Container startet → OpenShift prüft SCC → Non-root User ✓ → Pod läuft ✓
⚠ CrashLoopBackOff beim ersten Deploy
Problem: Standard nginx läuft als root (Port 80) – OpenShift verbietet root-Container per Default.
Fix: nginxinc/nginx-unprivileged:alpine nutzen. Läuft auf Port 8080, kein root nötig.
# Falsch – schlägt fehl auf OpenShift:
FROM nginx:alpine
# Richtig – rootless, Port 8080:
FROM nginxinc/nginx-unprivileged:alpinePrivates GitHub Repo einbinden
OpenShift kann nicht einfach auf ein privates GitHub-Repo zugreifen. Ein Personal Access Token reicht nicht – das Secret muss außerdem annotiert werden, damit OpenShift es automatisch der richtigen URL zuordnet.
# 1. Secret mit GitHub Personal Access Token anlegen
oc create secret generic github-secret \
--from-literal=username=<github-username> \
--from-literal=password=<github-PAT> \
--type=kubernetes.io/basic-auth
# 2. Secret annotieren – OpenShift nutzt es automatisch für die URL
oc annotate secret github-secret \
"build.openshift.io/source-secret-match-uri-1=https://github.com/<username>/*"
# 3. Builder-ServiceAccount bekommt Zugriff
oc secret link builder github-secret✓ Secret annotieren ist Pflicht
Ein GitHub-Secret ohne Annotation greift nicht automatisch. Die Annotation build.openshift.io/source-secret-match-uri-1 verbindet das Secret mit der richtigen Git-URL – erst dann klappt der Clone.
Dockerfile für Next.js: Multi-Stage und standalone
Ein normales Dockerfile würde ein riesiges Image produzieren – node_modules sind hunderte Megabyte. Multi-Stage Build löst das: Die Build-Stage kompiliert, die Runtime-Stage kopiert nur das Ergebnis.

Das Schlüsselwort ist output: "standalone" in der next.config.ts. Next.js bündelt dann nur die tatsächlich importierten Dependencies in .next/standalone – kein node_modules im finalen Image nötig.
# next.config.ts – eine Zeile entscheidet alles:
output: "standalone"
# Das Ergebnis: .next/standalone enthält nur was wirklich gebraucht wird.
# Kein node_modules im finalen Image – spart ~800MB.✓ output: standalone spart ~800MB
Ohne standalone kopiert das Dockerfile das gesamte node_modules-Verzeichnis ins finale Image. Mit standalone reicht der .next/standalone-Ordner – Next.js hat alle nötigen Dependencies bereits eingebettet.
Die NEXT_PUBLIC_* Falle: Build-Zeit vs. Runtime
Das war der teuerste Fehler in Zeiteinheiten. Next.js unterscheidet strikt zwischen Variablen, die zur Build-Zeit eingefroren werden, und Variablen, die zur Laufzeit ausgewertet werden.
NEXT_PUBLIC_* – die Falle
Build-Zeit Runtime
────────────────────────── ──────────────────────────
NEXT_PUBLIC_SITE_URL BLOG_BASE_URL
NEXT_PUBLIC_BLOG_BASE AUTH_BASE_URL
NEXT_PUBLIC_CLOUDINARY_* CLOUDINARY_API_SECRET
NEXT_PUBLIC_GA_* CLOUDINARY_API_KEY
→ Werden in den JS-Bundle → Werden beim Start des
eingefroren (unveränderbar) Servers ausgelesen
Falsch gesetzt → leere Falsch gesetzt → localhost
Variablen im Frontend als Proxy-Ziel⚠ NEXT_PUBLIC_* war leer im Frontend
Problem: Die Variablen wurden als Runtime-Secrets gesetzt – aber Next.js inlinet sie zur Build-Zeit. Im Bundle standen leere Strings.
Fix: NEXT_PUBLIC_* als buildArgs in die BuildConfig eintragen. Server-seitige Secrets (API Keys) weiterhin als Runtime-Secret.
# BuildConfig – NEXT_PUBLIC_* als buildArgs:
spec:
strategy:
type: Docker
dockerStrategy:
buildArgs:
- name: NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME
value: dein-cloud-name
- name: NEXT_PUBLIC_SITE_URL
value: https://zelkulon.com
# Runtime Vars – Server-seitige Secrets:
oc create secret generic zelkulon-secrets --from-env-file=.env.local
oc set env deployment/zelkulon-homepage --from=secret/zelkulon-secretsBuildConfig statt oc new-app
oc new-app --strategy=docker schlägt häufig fehl wenn OpenShift die Git-URL nicht direkt verifizieren kann. Die sauberere Lösung: BuildConfig manuell per YAML anlegen.
⚠ InvalidOutputReference beim Build-Start
Problem: oc new-app legt keinen ImageStream an – ohne ImageStream kann das Build-Ergebnis nicht gespeichert werden.
Fix: Vor dem ersten Build: oc create imagestream zelkulon-homepage
# ImageStream zuerst anlegen – sonst: InvalidOutputReference
oc create imagestream zelkulon-homepage
# BuildConfig per YAML
cat <<EOF | oc apply -f -
apiVersion: build.openshift.io/v1
kind: BuildConfig
metadata:
name: zelkulon-homepage
spec:
source:
type: Git
git:
uri: https://github.com/<username>/zelkulon-homepage.git
sourceSecret:
name: github-secret
strategy:
type: Docker
output:
to:
kind: ImageStreamTag
name: zelkulon-homepage:latest
EOF
# Build starten und live beobachten (dauert 5–7 Minuten)
oc start-build zelkulon-homepage --followEin Build dauert 5–7 Minuten – Next.js TypeCheck und Production Build brauchen ihre Zeit. Mit npm cache würde es schneller gehen, aber für die Sandbox reicht es.
HTTPS automatisch mit Edge Route
Was mich am meisten überrascht hat: TLS ist kein manueller Schritt. OpenShift übernimmt die Zertifikatsverwaltung vollständig – man erstellt eine Edge Route, und HTTPS funktioniert sofort.
# App aus ImageStream deployen
oc new-app --image-stream=zelkulon-homepage:latest --name=zelkulon-homepage
# HTTPS Route mit Edge Termination – TLS übernimmt OpenShift automatisch
oc create route edge zelkulon-homepage \
--service=zelkulon-homepage \
--port=3000
# URL abrufen
oc get routes✓ OpenShift übernimmt TLS komplett
Edge Termination bedeutet: TLS wird an der OpenShift-Grenze terminiert. Kein Zertifikat kaufen, kein Let's Encrypt einrichten, kein manueller Renewal. Einfach oc create route edge und fertig.
OpenShift vs. Netlify vs. Railway
| Merkmal | OpenShift | Netlify | Railway |
|---|---|---|---|
| Kontrolle über Infrastruktur | ✅ Vollständig | ✅ Abstrakt | ✅ Abstrakt |
| Einstiegshürde | ⚠ Komplex | ✅ Push & Deploy | ✅ Push & Deploy |
| HTTPS | ✅ Automatisch (Edge) | ✅ Automatisch | ✅ Automatisch |
| Root-Container | ❌ Kein root | ✅ Kein Problem | ✅ Kein Problem |
| Kosten | 30 Tage Sandbox | Kostenlos (kommerziell) | $5 Starter |
| Performance | 🚀 Sehr schnell | ✅ Gut | ✅ Gut |
Die Tabelle zeigt die ehrliche Einschätzung nach 30 Tagen Sandbox-Betrieb. OpenShift ist kein Netlify-Ersatz für kleine Teams.
Fazit: Schneller, aber nicht für jeden
OpenShift ist schneller. Das ist keine Theorie – nach 30 Tagen Sandbox lädt die Seite spürbar flotter, die Bilder sind sofort da. Kubernetes-Scheduling, dedizierte Ressourcen, kein Cold-Start-Theater.
Trotzdem bleiben wir bei Netlify und Railway. Nicht weil OpenShift schlechter ist, sondern weil das Sandbox-Tier ausläuft und ein echter OpenShift-Cluster für eine kleine UG schlicht zu teuer ist. Der Performancevorteil rechtfertigt die Kosten aktuell nicht.
- Root-Container funktionieren nicht – nginxinc/nginx-unprivileged ist Pflicht
- NEXT_PUBLIC_* gehören in buildArgs, nicht in Runtime-Secrets
- ImageStream vor dem ersten Build anlegen
- BuildConfig per YAML ist zuverlässiger als oc new-app
- Performance ist messbar besser – aber der Preis passt nicht für kleine UGs
Das hier war Phase 1. In den nächsten Artikeln folgt, wie sich OpenShift mit Spring Boot Microservices schlägt – Service Discovery, persistente Datenbanken, interne Kommunikation. Ob sich der Aufwand lohnt, wird sich zeigen.