Docker für Next.js
Verwenden für das App-Image. Next-Seite-Standalone-Fakten befinden sich in nextjs-production (references/self-hosting.md); was das Image in Kubernetes ausführt, ist helm-nextjs; wie CI es baut und pusht, ist github-actions.
Das apps/<app>/Dockerfile der Vorlage ist die Quelle der Wahrheit; references/Dockerfile.md enthält es vollständig mit Anmerkungen. Bauen Sie vom Repository-Root aus: docker build -f apps/<app>/Dockerfile -t <app> .
Regeln
-
Fünf Stufen:
base→pruner→deps→builder→runner.base: Node + pnpm, nichts weiter.pruner:turbo prune <app> --dockerauf dem vollständigen Repository, sodass nur die App und ihre Workspace-Abhängigkeiten übrig bleiben.deps: Nurout/json/(Manifeste) undout/pnpm-lock.yamlkopieren, dannpnpm fetch+pnpm install --frozen-lockfile --offline. Diese Schicht wird wiederverwendet, bis sich ein Manifest oder die Lockfile ändert, sodass eine Quellcode-Änderung keine Neuinstallation auslöst.builder:out/full/(Quellen) kopieren,pnpm exec turbo run build --filter=<app>.runner: Ein frisches Node-Image mit nur dem Standalone-Output. Warum: Quellcode-Änderungen bauen nur die letzten beiden Stufen neu, und der Runner hat kein pnpm, keine Quellen und keine Dev-Abhängigkeiten.
-
Pinne das Node-Image auf exakte Version und Digest, abgestimmt auf
.node-version.node:26.12.0-alpine3.24@sha256:6f08…in einemARG NODE_IMAGE, verwendet vonbaseundrunner. Der Digest ist der Multi-Arch-Index, sodass sowohl amd64 als auch arm64 aufgelöst werden. Warum Alpine statt distroless: Distroless Node veröffentlicht nur fließende Tags (latest,nonroot), sodass die exakte Node-Version nicht mit.node-versionabgeglichen werden kann, und es hat keinwgetfür den HEALTHCHECK. Alpine ist klein, pinnt exakt undsharpliefert vorgebaute musl-Binärdateien, sodass die Bildoptimierung ohne zusätzliche Pakete funktioniert. -
Installiere pnpm aus dem GitHub-Release-Binary, verifiziert durch SHA-256. Niemals
npm i -g pnpm, niemals Corepack, niemals ein unverifiziertescurl | sh. EinARG PNPM_VERSIONgleichpackageManager, plus ein Prüfsumme pro Architektur fürpnpm-linux-{x64,arm64}-musl.tar.gz, ausgewählt durchTARGETARCH. Zum Aktualisieren: Ändern Sie die drei ARGs, entnehmen Sie die sha256-Werte aus den Asset-Digests des Releases (gh release view v<version> --repo pnpm/pnpm --json assets), und aktualisieren SiepackageManagerim selben Commit. Fallback, falls sich das Release-Format ändert: Basieren Sie diepruner/deps/builder-Stufen aufghcr.io/pnpm/pnpm:<version>(kein Node, fügen Sie es hinzu), anstatt eine neue Installationsmethode zu erfinden. -
Der Runner kopiert genau drei Dinge.
COPY --from=builder /repo/apps/<app>/.next/standalone ./ COPY --from=builder /repo/apps/<app>/.next/static ./apps/<app>/.next/static COPY --from=builder /repo/apps/<app>/public ./apps/<app>/publicStandalone enthält weder
staticnochpublic; ohne sie liefert die App kein JS oder CSS. Im Monorepo ist der Serverapps/<app>/server.js.CMD ["node", "apps/<app>/server.js"], Exec-Form, sodass Node SIGTERM direkt erhält. -
Als uid/gid 1001 mit Dateien im Besitz von root ausführen.
nextjs:nodejs(1001:1001) erstellen, die kopierten Dateien im Root-Besitz lassen (die App kann lesen, aber ihren eigenen Code nicht ändern) und nurapps/<app>/.next/cachemitchownbearbeiten.USER 1001:1001, numerisch, damit KubernetesrunAsNonRootohne Namensauflösung überprüfen kann. -
Das Image muss mit einem schreibgeschützten Root-Dateisystem funktionieren. Die einzigen Laufzeitschreibvorgänge gehen an
.next/cache(Bildoptimierung, ISR/Fetch-Cache). Kubernetes mountet dort einemptyDir; lokal verwenden Sie--tmpfs. Testen Sie es:docker run --rm -p 3000:3000 --read-only \ --tmpfs /app/apps/<app>/.next/cache:uid=1001,gid=1001 \ --cap-drop ALL --security-opt no-new-privileges <app> -
Setze
NODE_ENV=production,HOSTNAME=0.0.0.0,PORT=3000,NEXT_TELEMETRY_DISABLED=1im Runner. OhneHOSTNAME=0.0.0.0bindet sich der Standalone-Server an den Container-Hostnamen und Probes schlagen fehl. -
HEALTHCHECKgreift auf/api/healthmit busyboxwgetzu. Es ist fürdocker run/Compose; Kubernetes ignoriert es und verwendet die Probes des Charts auf demselben Pfad. -
Build-Args sind nur für Werte, die zur Build-Zeit eingebettet werden:
NEXT_PUBLIC_*undNEXT_DEPLOYMENT_ID.NEXT_PUBLIC_*wird in das JS kompiliert, daher muss es einARG+ENVimbuildersein, und es ist in jeder Umgebung, in der das Image läuft, identisch. Alles, was umgebungsspezifisch ist, ist eine Laufzeit-Umgebungsvariable aus dem Chart. Um eine hinzuzufügen:ARG NEXT_PUBLIC_APP_NAME ENV NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME}… plus
build-argsin beiden Workflows undNEXT_PUBLIC_*ist bereits in turbosenv. -
Geheimnisse dürfen nie in einem
ARG,ENVoderCOPYlanden. Sie blieben in der Image-Historie. Verwenden Sie einen BuildKit-Secret-Mount, der nur für diesesRUNexistiert:RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN pnpm fetch # privates RegistryDer Build-Schritt der Vorlage deklariert bereits das optionale
next_server_actions_key-Secret auf diese Weise. -
Cache für den pnpm-Store mit einem BuildKit-Cache-Mount.
--mount=type=cache,id=pnpm-store,target=/pnpm/storebeifetch/install/dlx, mitpnpm_config_store_dir=/pnpm/store. Der Store überlebt zwischen Builds auf demselben Builder, ohne in einer Schicht zu landen. In CI ist Layer-Cachingtype=gha(siehegithub-actions). -
.dockerignoreim Repository-Root hält den Kontext klein und Geheimnisse draußen. Es schließtnode_modules,.next,.turbo,dist,coverage,.env*,.git,.github,chartsaus. Ein Kontext, dernode_modulesvon Windows oder macOS enthält, zerbricht native Pakete im Linux-Image. -
Multi-Arch ist
linux/amd64,linux/arm64, gebaut von CI. Lokal bauen Sie Ihre eigene Architektur. Jede Stufe ist architekturneutral, außer dem pnpm-Download, der seine Prüfsumme vonTARGETARCHwählt. -
Labels: Das Dockerfile setzt
org.opencontainers.image.titleundlicenses; CI fügtsource,revision,version,createddurchdocker/metadata-actionhinzu. Härten Sie kein Version-Label fest. -
Halten Sie Stufen frei von App-spezifischen Hacks. Kein
apk addim Runner (die App benötigt kein Shell-Toolset), keinnpm, keinenode_modulesvom Host kopiert. Wenn die App ein natives Tool zur Laufzeit benötigt, begründen Sie dies in einem Kommentar.
Fehlerbehebung
| Symptom | Ursache |
|---|---|
extends: … doesn't resolve während des Builds | Eine Datei auf Root-Ebene, die die App benötigt, war nicht im beschnittenen Baum: Verschieben Sie sie in ein Workspace-Paket |
ERR_PNPM_OUTDATED_LOCKFILE in deps | Lockfile nach einer Manifeständerung nicht committet |
ERR_PNPM_IGNORED_BUILDS | Eine neue Abhängigkeit möchte ein Installationsskript: Entscheiden Sie in allowBuilds |
| App startet, kein CSS/JS | .next/static oder public nicht nach apps/<app>/ kopiert |
| Health-Check schlägt fehl, Logs okay | HOSTNAME nicht 0.0.0.0 |
EROFS zur Laufzeit | Etwas schreibt außerhalb von .next/cache; finden Sie es, entfernen Sie nicht --read-only |
NEXT_PUBLIC_X leer | Nicht als Build-Arg übergeben (es wird zur Build-Zeit eingebacken) |
Verifizieren
docker build -f apps/<app>/Dockerfile -t <app>:local --build-arg NEXT_DEPLOYMENT_ID=local .
docker run -d --name <app> -p 3000:3000 --read-only --tmpfs /app/apps/<app>/.next/cache:uid=1001,gid=1001 --cap-drop ALL <app>:local
curl -fsS localhost:3000/api/health
docker exec <app> id # uid=1001(nextjs)
docker inspect --format '{{.State.Health.Status}}' <app> # healthy after ~30s
docker rm -f <app>
Quellen
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://turborepo.dev/docs/guides/tools/docker
- https://pnpm.io/docker
- https://pnpm.io/cli/fetch
- https://github.com/pnpm/pnpm/releases
- https://docs.docker.com/build/building/best-practices/
- https://docs.docker.com/build/building/secrets/
- https://docs.docker.com/build/cache/optimize/#use-cache-mounts
- https://docs.docker.com/reference/dockerfile/#healthcheck
- https://hub.docker.com/_/node
- https://github.com/opencontainers/image-spec/blob/main/annotations.md