Docker pour Next.js
Utilisez-le pour l'image de l'application. Les faits relatifs au standalone Next.js se trouvent dans nextjs-production (references/self-hosting.md) ; ce qui exécute l'image dans Kubernetes est helm-nextjs ; comment le CI construit et pousse l'image est github-actions.
Le Dockerfile du template dans apps/<app>/ est la source de vérité ; references/Dockerfile.md le contient en entier avec des notes. Construisez à partir de la racine du dépôt : docker build -f apps/<app>/Dockerfile -t <app> .
Règles
-
Cinq étapes :
base→pruner→deps→builder→runner.base: Node + pnpm, rien d'autre.pruner:turbo prune <app> --dockersur l'ensemble du dépôt, ne laissant que l'application et ses dépendances d'espace de travail.deps: copie seulementout/json/(manifestes) etout/pnpm-lock.yaml, puispnpm fetch+pnpm install --frozen-lockfile --offline. Cette couche est réutilisée jusqu'à ce qu'un manifeste ou le fichier de verrouillage change, donc une modification du code source ne déclenche pas une réinstallation.builder: copieout/full/(sources),pnpm exec turbo run build --filter=<app>.runner: une image Node fraîche avec seulement la sortie standalone. Pourquoi : les modifications du code source ne reconstruisent que les deux dernières étapes, et le runner n'a ni pnpm, ni sources, ni dépendances de développement.
-
Épinglez l'image Node par version exacte et empreinte (digest), en faisant correspondre
.node-version.node:26.12.0-alpine3.24@sha256:6f08…dans un seulARG NODE_IMAGE, utilisé à la fois parbaseetrunner. L'empreinte est l'index multi-arch, donc amd64 et arm64 se résolvent tous les deux. Pourquoi Alpine plutôt que Distroless : Distroless Node ne publie que des tags flottants (latest,nonroot), donc la version exacte de Node ne peut pas être mise en correspondance avec.node-version, et il n'a pas dewgetpour le HEALTHCHECK. Alpine est légère, s'épingle exactement, etsharplivre des binaires musl préconstruits, donc l'optimisation d'image fonctionne sans paquets supplémentaires. -
Installez pnpm à partir du binaire de la version GitHub, vérifié par SHA-256. Jamais
npm i -g pnpm, jamais Corepack, jamais uncurl | shnon vérifié. UnARG PNPM_VERSIONégal àpackageManager, plus une somme de contrôle par architecture pourpnpm-linux-{x64,arm64}-musl.tar.gz, sélectionnée parTARGETARCH. Pour mettre à jour : changez les trois ARGs, en prenant les valeurs sha256 des empreintes des assets de la version (gh release view v<version> --repo pnpm/pnpm --json assets), et mettez à jourpackageManagerdans le même commit. Solution de repli si le format de publication change : basez les étapespruner/deps/buildersurghcr.io/pnpm/pnpm:<version>(pas de Node, ajoutez-le) plutôt que d'inventer une nouvelle méthode d'installation. -
Le runner copie exactement trois choses.
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>/publicLe mode standalone n'inclut pas
staticoupublic; sans eux, l'application ne sert aucun JS ou CSS. Dans le monorepo, le serveur estapps/<app>/server.js.CMD ["node", "apps/<app>/server.js"], forme exec, donc Node reçoit SIGTERM directement. -
Exécutez avec uid/gid 1001, les fichiers appartenant à root. Créez
nextjs:nodejs(1001:1001), laissez les fichiers copiés appartenir à root (l'application peut lire mais pas modifier son propre code), etchownseulementapps/<app>/.next/cache.USER 1001:1001, numérique, afin que Kubernetes puisse vérifierrunAsNonRootsans résoudre les noms. -
L'image doit fonctionner avec un système de fichiers racine en lecture seule. Les seules écritures à l'exécution vont dans
.next/cache(optimisation d'image, cache ISR/fetch). Kubernetes y monte unemptyDir; localement, utilisez--tmpfs. Testez-le :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> -
Définissez
NODE_ENV=production,HOSTNAME=0.0.0.0,PORT=3000,NEXT_TELEMETRY_DISABLED=1dans le runner. SansHOSTNAME=0.0.0.0, le serveur standalone se lie au nom d'hôte du conteneur et les sondes échouent. -
HEALTHCHECKcible/api/healthavecwgetde busybox. C'est pourdocker run/Compose ; Kubernetes l'ignore et utilise les sondes du chart sur le même chemin. -
Les arguments de construction (build args) sont uniquement pour les valeurs intégrées à la construction :
NEXT_PUBLIC_*etNEXT_DEPLOYMENT_ID.NEXT_PUBLIC_*est compilé dans le JS, donc il doit être unARG+ENVdansbuilder, et il est identique dans tous les environnements où l'image s'exécute. Tout ce qui est spécifique à l'environnement est une variable d'environnement d'exécution provenant du chart. Pour en ajouter un :ARG NEXT_PUBLIC_APP_NAME ENV NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME}… plus
build-argsdans les deux workflows etNEXT_PUBLIC_*est déjà dans l'envde turbo. -
Les secrets ne vont jamais dans un
ARG,ENVouCOPY. Ils resteraient dans l'historique de l'image. Utilisez un montage de secret BuildKit, qui n'existe que pour ceRUN:RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN pnpm fetch # registry privéL'étape de construction du template déclare déjà le secret optionnel
next_server_actions_keyde cette façon. -
Mettez en cache le store pnpm avec un cache mount BuildKit.
--mount=type=cache,id=pnpm-store,target=/pnpm/storesurfetch/install/dlx, avecpnpm_config_store_dir=/pnpm/store. Le store survit entre les constructions sur le même builder sans atterrir dans une couche. En CI, la mise en cache de couche esttype=gha(voirgithub-actions). -
.dockerignoreà la racine du dépôt maintient le contexte petit et les secrets à l'écart. Il exclutnode_modules,.next,.turbo,dist,coverage,.env*,.git,.github,charts. Un contexte qui inclutnode_modulesde Windows ou macOS casse les paquets natifs dans l'image Linux. -
Multi-architecture est
linux/amd64,linux/arm64, construit par le CI. Localement, construisez pour votre propre architecture. Chaque étape est neutre en termes d'architecture à l'exception du téléchargement de pnpm, qui choisit sa somme de contrôle à partir deTARGETARCH. -
Étiquettes : le Dockerfile définit
org.opencontainers.image.titleetlicenses; le CI ajoutesource,revision,version,createdviadocker/metadata-action. Ne codez pas en dur une étiquette de version. -
Gardez les étapes exemptes de hacks spécifiques à l'application. Pas de
apk adddans le runner (l'application n'a pas besoin d'une boîte à outils shell), pas denpm, pas denode_modulescopiés depuis l'hôte. Si l'application a besoin d'un outil natif à l'exécution, justifiez-le dans un commentaire.
Débogage
| Symptôme | Cause |
|---|---|
extends: … doesn't resolve pendant la construction | Un fichier au niveau racine dont l'application a besoin n'était pas dans l'arbre élagué : déplacez-le dans un package d'espace de travail |
ERR_PNPM_OUTDATED_LOCKFILE dans deps | Fichier de verrouillage non commité après un changement de manifeste |
ERR_PNPM_IGNORED_BUILDS | Une nouvelle dépendance veut un script d'installation : décidez dans allowBuilds |
| L'application démarre, pas de CSS/JS | .next/static ou public pas copié dans apps/<app>/ |
| La vérification de santé échoue, les logs sont bons | HOSTNAME n'est pas 0.0.0.0 |
EROFS à l'exécution | Quelque chose écrit en dehors de .next/cache ; trouvez-le, ne supprimez pas --read-only |
NEXT_PUBLIC_X vide | Non passé comme argument de construction (c'est cuit à la construction) |
Vérification
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>
Sources
- 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