Zum Inhalt springen

Next.js selbst hosten - und was dabei aufhört zu funktionieren

Next.js läuft überall, wo Node läuft. Vier Funktionen liefert die Plattform statt des Frameworks, und zu wissen welche vier trennt eine ruhige Migration von einer überraschten.

6 Min. Lesezeit

"Können wir das auf eigener Infrastruktur betreiben?" hat eine kurze und eine lange Antwort. Die kurze lautet ja: next start ist ein Node-Server und läuft auf einer VM, in einem Container, hinter Ihrem Load Balancer, in Ihrer Region.

Die lange lautet, dass vier Dinge, die man für Next.js-Funktionen hält, tatsächlich von der Hosting-Plattform kommen - und genau diese vier überraschen Teams eine Woche nach der Migration.

Der Build, der wirklich deploybar ist

Der Standard-Build hinterlässt ein .next-Verzeichnis, das den kompletten node_modules-Baum daneben braucht. Für einen Container sind das Hunderte Megabyte Abhängigkeiten, die Sie nicht ausführen.

// next.config.ts
export default {
  output: 'standalone',
};

Das verfolgt die Module, die der Server tatsächlich erreicht, kopiert sie nach .next/standalone und legt eine server.js dazu, die ohne Paketmanager startet. In den Codebasen, die wir umgezogen haben, ging das Image von ~1,2 GB auf ~180 MB.

FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
 
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
 
FROM node:22-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
# Statische Assets und public/ liegen NICHT in standalone. Sie zu kopieren ist
# der Schritt, den man übersieht - das Symptom ist eine Seite ohne CSS.
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Zwei Dinge an dieser Datei gehören klar gesagt. .next/static und public/ sind nicht Teil der Standalone-Ausgabe und müssen separat kopiert werden - fehlt das, liefert das Deployment HTML ohne CSS und ohne Bilder. Und npm run build läuft zur Image-Bauzeit, also muss jede Umgebungsvariable, die Ihr Code auf Modulebene liest, dann vorhanden sein und nicht erst zur Laufzeit.

Die vier Dinge, die die Plattform erledigt hat

Bildoptimierung. next/image braucht etwas, das skaliert und neu kodiert. Auf einer Plattform ist das ein Dienst; auf Ihrem Server ist es sharp, in Ihrem Prozess, auf Ihrer CPU.

npm install sharp

Installiert wird es automatisch benutzt. Wenn Ihr Traffic diese CPU-Kosten real macht, richten Sie images.loader auf ein CDN, das skaliert, oder erzeugen Sie die Größen zur Bauzeit vor. Was Sie nicht tun dürfen, ist images.unoptimized: true in der Konfiguration stehen zu lassen und zu vergessen - das schickt die Originaldatei an jeden Besucher und hebt still den Grund auf, aus dem Sie die Komponente benutzt haben.

ISR und der Revalidierungs-Cache. Auf einem einzelnen Container funktioniert das: Der Cache liegt auf der lokalen Platte. Auf drei Containern hinter einem Load Balancer nicht - jeder hat seine eigene Platte, revalidateTag leert den Cache auf der Instanz, die den Aufruf bekommen hat, und die anderen beiden liefern die alte Seite weiter, bis sie zufällig selbst revalidieren. Die Lösung ist ein gemeinsamer Cache-Handler:

// next.config.ts
export default {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0, // In-Process-LRU aus; Redis ist die Quelle
};

Jeder Redis-gestützte Handler tut es. Wichtig ist zu verstehen, warum er nötig ist, denn "die Seite hat sich für manche Nutzer aktualisiert und für andere nicht" sieht sonst nach einem unmöglichen Fehler aus. Welcher Cache was tut, lohnt sich nachzulesen: die vier Ebenen und welche Sie erwischt hat.

Middleware an der Edge. Ihre Middleware läuft nicht mehr an hundert Orten, bevor die Anfrage einen Server erreicht; sie läuft in Ihrem Server, in Ihrer einen Region. Code, der in der Annahme geschrieben wurde, das sei fast kostenlos - ein Geo-Lookup, eine A/B-Zuweisung -, liegt jetzt im kritischen Pfad jeder Anfrage. Das sind echte Kosten, und sie gehören gemessen statt angenommen.

Streaming durch Ihren Proxy. Suspense und loading.tsx streamen die Antwort in Stücken. Ein nginx davor mit Standardeinstellungen puffert die ganze Antwort, bevor er sie sendet - damit wird Streaming zu einer langsameren Variante von Nicht-Streaming, ohne dass eine Fehlermeldung es Ihnen sagt.

location / {
  proxy_pass http://app:3000;
  proxy_buffering off;
  proxy_http_version 1.1;
  proxy_set_header Connection "";
}

Was Sie gewinnen

Nicht nur Kosten, obwohl der Unterschied in der Größe erheblich ist. Sie bekommen Ihre Daten in einer Jurisdiktion, die Sie gewählt haben - für einen Kunden mit türkischer oder EU-Anforderung an die Datenhaltung nicht verhandelbar. Sie können im selben Netz wie Ihre Datenbank laufen, was jedem Query einen Roundtrip spart. Und Sie bekommen ein Deployment, das niemand durch eine fremde Preisentscheidung unter Ihnen verändert.

Was es kostet

Jetzt gehört die Verfügbarkeit jemandem. Preview-Deployments pro Pull Request, sofortige Rollbacks und ein globales CDN sind echte Arbeit im Nachbau, und ein Team, das nie Infrastruktur betrieben hat, verbringt den ersten Monat damit, wiederzuentdecken, warum es diese Funktionen gibt.

Unsere Faustregel: Läuft die Anwendung in einer Region, hat das Team schon Dienste im Betrieb, oder gibt es eine Anforderung an die Datenhaltung, ist Selbst-Hosting schlicht richtig. Ist es eine Marketing-Seite mit zwei Entwicklern und globalem Traffic, ist die Plattform billiger als das Gehalt der Person, die die Alternative pflegt.

So oder so: Treffen Sie die Entscheidung auf dieser Grundlage und nicht anhand eines Benchmarks. Das Framework läuft an beiden Orten gleich - es sind die vier Funktionen oben, die sich verschieben.

Zurück zu allen Artikeln