Wir betreiben seit März 2026 drei Next.js-Anwendungen auf AWS App Runner, verteilt auf Dev-, Staging- und Produktionskonten, für eine Kundenplattform mit echten Nutzern und echtem Geld, das dabei fließt. Sechs Monate reichen aus, um an die scharfen Kanten zu stoßen. Dies ist die Bewertung, die wir uns gewünscht hätten, bevor wir uns dafür entschieden haben.
Kurzfassung: App Runner ist die richtige Wahl für ein kleines Team, das Container auf AWS haben möchte, ohne einen ALB, ein VPC-Layout und den Lebenszyklus von ECS-Task-Definitionen selbst zu verwalten. Es ist die falsche Wahl, wenn Sie feine Kontrolle über das Netzwerk, lang laufende Requests oder ein Deployment-Verhalten benötigen, das über „dieses Image gegen jenes austauschen" hinausgeht. Wir würden uns für dieses Projekt wieder dafür entscheiden, mit den unten stehenden Vorbehalten gleich auf der ersten Seite des Runbooks.
Zwischen welchen Optionen wir gewählt haben
Die Plattform war ursprünglich für Amplify Hosting geplant. Als wir die Infrastruktur in CDK neu aufgebaut haben, standen drei Optionen zur Auswahl:
- ECS Fargate hinter einem ALB. Der Branchenstandard. Volle Kontrolle, volle Verkabelung: VPC, Subnetze, Security Groups, Target Groups, Listener-Regeln, Task-Definitionen, Service-Auto-Scaling. Wir haben das in einer separaten Anleitung behandelt.
- Amplify Hosting. Zero-Ops für Next.js, aber es will die Git-Integration und den Build selbst besitzen, und seine serverseitige Runtime ist eine Black Box, die man nicht anpassen kann.
- App Runner. Ein Image zu ECR pushen, eine HTTPS-URL bekommen. Kein ALB, kein VPC für den öffentlichen Pfad nötig, natives Auto-Scaling, eine CloudFormation-Ressource, die man wie jede andere aus CDK heraus verwalten kann.
App Runner gewann bei der operativen Angriffsfläche. Für ein Team von zwei oder drei Personen, die auch den Anwendungscode schreiben, ist jedes Stück Netzwerk, das man nicht selbst betreibt, ein Stück, das man um 23 Uhr nicht debuggen muss.
Das Gute
HTTPS und eine URL von Anfang an. Jeder Service bekommt einen *.awsapprunner.com-Hostnamen mit einem verwalteten Zertifikat. Eigene Domains sind ein CNAME plus ein Validierungseintrag. Für die App-Hostnamen haben wir ACM überhaupt nicht angefasst.
Die CloudFormation-Ressource ist vollständig. AWS::AppRunner::Service legt die Image-ID, die Instanzgröße, die Auto-Scaling-Konfiguration, den Health-Check, Umgebungsvariablen und Secrets offen. Das bedeutet, dass das Rollout innerhalb von CloudFormation stattfindet, wenn sich der Image-Tag ändert, was einen subtilen Vorteil hat: Die Ausführungsrolle von CloudFormation besitzt die Berechtigung iam:PassRole, die der menschliche Deploy-Nutzer nicht haben muss. Gleichzeitige Deploys werden auf Stack-Ebene serialisiert, sodass zwei Entwickler sich nicht gegenseitig das Rollout überschreiben können.
Die Secrets-Injektion ist richtig gelöst. runtimeEnvironmentSecrets holt Werte beim Container-Start aus dem Secrets Manager und gewährt der Instanzrolle GetSecretValue genau für diese ARNs. Sensible Werte landen nie im Template. Wir nutzen das für die Datenbank-Zugangsdaten, API-Tokens von Drittanbietern und den privaten Schlüssel für Web-Push.
Auto-Scaling ist auf die beste Art langweilig. Die Standardkonfiguration (min. 1, max. 25, 100 gleichzeitige Requests pro Instanz) war für interne Apps völlig ausreichend. Für die öffentliche App haben wir ein Minimum von zwei warmen Instanzen gesetzt, was den Traffic am Launch-Tag ohne kaltes Skalieren abgefedert hat.
Die Instanzgröße zu ändern ist eine einzige Zeile. Als sich herausstellte, dass die serverseitige Ertragsberechnung der Finance-App CPU-gebundenes JavaScript war und eine halbe vCPU eine Seite 20 Sekunden brauchen ließ, haben wir sie in config.ts auf 1 vCPU / 2 GB umgestellt. Deployen, fertig. Dieselbe Änderung bei ECS ist eine neue Task-Definition-Revision plus ein Service-Update plus Warten.
Das Schlechte
Jeder Punkt hier hat uns mindestens einen Nachmittag gekostet.
Ein 120-Sekunden-Gateway-Timeout, nicht konfigurierbar. App Runner schließt jeden Request, der länger als 120 Sekunden dauert, mit einem 504. Unsere Finance-App hatte ein Datagrid, das, sobald ein Materialisierungsrückstand etwa 250 Zeilen überschritt, auf eine vollständige On-Read-Berechnung zurückfiel und das Limit sprengte. Die Lösung lag bei uns (inkrementell materialisieren, eine Health-Route hinzufügen, die „not ready" meldet, während Caches aufgewärmt werden), aber es ist die Art von Einschränkung, die man kennen sollte, bevor man seine API-Routen entwirft. Alles Lang-Laufende gehört in eine Lambda-Funktion oder eine Queue, nicht hinter App Runner.
Ein erneutes Deployment desselben Image-Tags bewirkt nichts. Wir pinnen jeden Service auf ein Image, das mit dem Git-SHA getaggt ist. Wenn man denselben SHA mit anderer Build-Zeit-Konfiguration neu baut, bleibt der Tag-String unverändert, CloudFormation sieht keinen Unterschied bei ImageIdentifier, und App Runner liefert weiter das Image aus, das es bereits gepullt hat — obwohl der Tag in ECR jetzt auf einen anderen Digest zeigt. Das hat uns erwischt, als wir ein Feature-Flag über den CDK-Kontext umgelegt haben, ohne eine Code-Änderung. Die Regel lautet jetzt: Jedes Deployment bewegt den SHA. Die Config-Änderung committen.
Build-Zeit- versus Runtime-Umgebung ist Ihr Problem. Next.js bettet NEXT_PUBLIC_*-Variablen zur Build-Zeit ein. Laufzeit-Umgebungsvariablen am App-Runner-Service können sie nicht mehr ändern. Unser Deploy-Skript liest die Build-Zeit-Umgebung jetzt aus dem frisch synthetisierten CloudFormation-Template aus und übergibt sie pro Build an CodeBuild, und verifiziert das anschließend, indem es die ausgelieferten JavaScript-Chunks durchsucht. Bevor wir das taten, brauchte eine geänderte öffentliche Variable zwei Deployments, um im Browser anzukommen.
Eine neue Instanz braucht etwa eine Minute. Image pullen, Node booten, Caches aufwärmen. Mit einem TCP-Health-Check geht die Instanz in Rotation, sobald der Port offen ist, was für unsere Finance-App bedeutete, dass der erste Request kalt und langsam bedient wurde. Die Umstellung auf einen HTTP-Health-Check auf einer eigenen /api/health-Route, die bis zum Ende des Aufwärmens 503 zurückgibt, hat das behoben. Das sollte man von Anfang an so machen.
Der Standard sind TCP-Health-Checks. Siehe oben. Das ist eine Falle für alles, was beim Start Caches aufwärmt.
Keine GitHub Actions? Dann auch kein App-Runner-Deployment über die Konsole. Da CloudFormation den Service besitzt, ist der „Deploy"-Button in der Konsole faktisch tabu: Jede manuelle Änderung wird vom nächsten Stack-Update überschrieben. Wir mussten unser eigenes Deploy-Skript bauen für den Fall, dass CI nicht verfügbar war (eine Zeit lang aus Abrechnungsgründen). Es ist ein 300-Zeilen-Shell-Skript mit einem auf den Remote-Branch gepinnten Worktree, einem git archive-Upload nach S3, parallelen CodeBuild-Jobs und einem CDK-Deploy. Es funktioniert, aber wir müssen es selbst pflegen.
Die Pipeline, die daraus entstand
Docker-Builds laufen in CodeBuild statt auf Laptops, zum einen weil plattformübergreifende Builds auf Apple Silicon schmerzhaft sind, zum anderen weil das den Ablauf „Image für App Runner bauen" innerhalb des Kontos hält, mit einer IAM-Rolle, die nur in die ECR-Repositories dieses Kontos pushen kann. Drei Builds laufen parallel. Synth läuft vor den Builds, weil ein defektes Template in Sekunden fehlschlagen sollte, nicht erst nach fünfzehn Minuten Docker.
Kosten
Wir können die Rechnung des Kunden nicht veröffentlichen, aber wir können ihre Form beschreiben. App Runner berechnet den bereitgestellten Instanzspeicher, solange eine Instanz existiert (warm, im Leerlauf), und vCPU nur, während aktiv Requests verarbeitet werden. Für interne Apps mit Mindestgröße und einer Instanz ist das ein Rundungsfehler. Für die öffentliche App mit zwei warmen Instanzen zu 2 vCPU / 4 GB ist es der größte einzelne Posten in der Compute-Rechnung, größer als Lambda und Aurora zusammen, aber immer noch weniger als ein äquivalentes ECS-Fargate-Setup, sobald man die Stundengebühr des ALB und das NAT-Gateway hinzurechnet, das man für private Subnetze bräuchte.
Der ehrliche Vergleich: App Runner ist pro vCPU-Stunde etwas teurer als Fargate und pro Entwicklerstunde deutlich günstiger.
Fazit
| Punkte | Anmerkung | |
|---|---|---|
| Setup und CDK-Unterstützung | 5/5 | Vollständige CloudFormation-Ressource, Secrets sauber gelöst |
| Day-2-Betrieb | 4/5 | Ruhig; der Health-Check-Standard ist das Einzige, das wir ändern würden |
| Deployment-Modell | 3/5 | Image-Tag-Pinning ist deterministisch, aber die Same-Tag-Falle ist unerbittlich |
| Request-Beschränkungen | 2/5 | 120-Sekunden-Timeout ist hart; darauf einplanen |
| Kosten | 4/5 | Pro vCPU teurer als Fargate, pro Entwickler deutlich günstiger |
| Würden wir es wieder wählen | Ja | Für ein kleines Team, das Next.js auf AWS ausliefert |
Wenn Ihre Workload eine Webanwendung mit Requests unter ein paar Sekunden ist, Ihr Team klein ist und Sie lieber Produktcode als Netzwerkcode schreiben, ist App Runner einer der Dienste mit dem besten Preis-Leistungs-Verhältnis, die AWS hat. Wenn Sie Websockets mit langer Lebensdauer, Requests über zwei Minuten oder ausschließlich private Eingänge mit feingranularer Kontrolle brauchen, gehen Sie direkt zu ECS Fargate und akzeptieren Sie die Verkabelung.
Brauchen Sie Hilfe bei der Entscheidung oder bei der Migration einer bestehenden App? Wir können helfen — wir haben beide Richtungen schon gemacht.