Docker para Next.js
Úsalo para la imagen de la aplicación. Los detalles de standalone de Next.js están en nextjs-production (references/self-hosting.md); qué ejecuta la imagen en Kubernetes está en helm-nextjs; cómo CI la compila y la sube está en github-actions.
El apps/<app>/Dockerfile de la plantilla es la fuente de verdad; references/Dockerfile.md lo contiene completo con notas. Compila desde la raíz del repositorio: docker build -f apps/<app>/Dockerfile -t <app> .
Reglas
-
Cinco etapas:
base→pruner→deps→builder→runner.base: Node + pnpm, nada más.pruner:turbo prune <app> --dockersobre el repositorio completo, dejando solo la aplicación y sus dependencias del espacio de trabajo.deps: copia soloout/json/(manifiestos) yout/pnpm-lock.yaml, luegopnpm fetch+pnpm install --frozen-lockfile --offline. Esta capa se reutiliza hasta que cambie un manifiesto o el archivo lock, por lo que una edición de código fuente no reinstala.builder: copiaout/full/(código fuente),pnpm exec turbo run build --filter=<app>.runner: una imagen Node nueva con solo el resultado standalone. Por qué: los cambios de código fuente reconstruyen solo las últimas dos etapas, y el runner no tiene pnpm, código fuente ni dependencias de desarrollo.
-
Fijar la imagen de Node por versión exacta y digest, coincidiendo con
.node-version.node:26.12.0-alpine3.24@sha256:6f08…en unARG NODE_IMAGE, usado tanto porbasecomo porrunner. El digest es el índice multi-arquitectura, por lo que tanto amd64 como arm64 se resuelven correctamente. Por qué Alpine en lugar de distroless: distroless Node publica solo etiquetas flotantes (latest,nonroot), por lo que la versión exacta de Node no se puede emparejar con.node-version, y no tienewgetpara el HEALTHCHECK. Alpine es pequeña, fija exactamente, ysharpenvía binarios musl precompilados, por lo que la optimización de imágenes funciona sin paquetes adicionales. -
Instalar pnpm desde el binario de la release de GitHub, verificado por SHA-256. Nunca
npm i -g pnpm, nunca Corepack, nunca uncurl | shsin verificar. UnARG PNPM_VERSIONigual apackageManager, más una suma de verificación por arquitectura parapnpm-linux-{x64,arm64}-musl.tar.gz, seleccionada porTARGETARCH. Para actualizar: cambiar los tres ARGs, tomando los valores sha256 de los digests de los activos de la release (gh release view v<version> --repo pnpm/pnpm --json assets), y actualizarpackageManageren el mismo commit. Plan de respaldo si el formato de la release cambia: basar las etapaspruner/deps/builderenghcr.io/pnpm/pnpm:<version>(sin Node, añadirlo) en lugar de inventar un nuevo método de instalación. -
El runner copia exactamente tres cosas.
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 no incluye
staticopublic; sin ellos la aplicación no sirve JS ni CSS. En el monorrepositorio el servidor esapps/<app>/server.js.CMD ["node", "apps/<app>/server.js"], forma exec, para que Node reciba SIGTERM directamente. -
Ejecutar como uid/gid 1001 con archivos propiedad de root. Crear
nextjs:nodejs(1001:1001), dejar los archivos copiados como propiedad de root (la aplicación puede leer pero no cambiar su propio código), ychownsoloapps/<app>/.next/cache.USER 1001:1001, numérico, para que Kubernetes pueda verificarrunAsNonRootsin resolver nombres. -
La imagen debe funcionar con un sistema de archivos raíz de solo lectura. Las únicas escrituras en tiempo de ejecución van a
.next/cache(optimización de imágenes, caché ISR/fetch). Kubernetes monta unemptyDirallí; localmente usar--tmpfs. Probarlo: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> -
Establecer
NODE_ENV=production,HOSTNAME=0.0.0.0,PORT=3000,NEXT_TELEMETRY_DISABLED=1en el runner. SinHOSTNAME=0.0.0.0el servidor standalone se vincula al hostname del contenedor y las sondas fallan. -
HEALTHCHECKgolpea/api/healthconwgetde busybox. Es paradocker run/Compose; Kubernetes lo ignora y usa las sondas del chart en la misma ruta. -
Los argumentos de compilación son solo para valores inlineados en la compilación:
NEXT_PUBLIC_*yNEXT_DEPLOYMENT_ID.NEXT_PUBLIC_*se compila en el JS, por lo que debe ser unARG+ENVenbuilder, y es idéntico en todos los entornos donde se ejecuta la imagen. Todo lo específico del entorno es una variable de entorno en tiempo de ejecución del chart. Para añadir uno:ARG NEXT_PUBLIC_APP_NAME ENV NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME}…además de
build-argsen ambos flujos de trabajo yNEXT_PUBLIC_*ya está enenvde turbo. -
Los secretos nunca van en un
ARG,ENVoCOPY. Permanecerían en el historial de la imagen. Usar un montaje secreto de BuildKit, que existe solo para eseRUN:RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN pnpm fetch # registro privadoEl paso de compilación de la plantilla ya declara el secreto opcional
next_server_actions_keyde esta manera. -
Cachear el store de pnpm con un montaje de caché de BuildKit.
--mount=type=cache,id=pnpm-store,target=/pnpm/storeenfetch/install/dlx, conpnpm_config_store_dir=/pnpm/store. El store sobrevive entre compilaciones en el mismo builder sin terminar en una capa. En CI, el caché de capas estype=gha(vergithub-actions). -
.dockerignoreen la raíz del repositorio mantiene el contexto pequeño y los secretos fuera. Excluyenode_modules,.next,.turbo,dist,coverage,.env*,.git,.github,charts. Un contexto que incluyanode_modulesde Windows o macOS rompe los paquetes nativos en la imagen Linux. -
Multi-arquitectura es
linux/amd64,linux/arm64, compilado por CI. Localmente, compila tu propia arquitectura. Cada etapa es neutral en cuanto a arquitectura excepto la descarga de pnpm, que elige su suma de verificación segúnTARGETARCH. -
Etiquetas: el Dockerfile establece
org.opencontainers.image.titleylicenses; CI añadesource,revision,version,createda través dedocker/metadata-action. No codificar una etiqueta de versión. -
Mantener las etapas libres de hacks específicos de la aplicación. Sin
apk adden el runner (la aplicación no necesita un conjunto de herramientas de shell), sinnpm, sinnode_modulescopiados del host. Si la aplicación necesita una herramienta nativa en tiempo de ejecución, justificarlo en un comentario.
Depuración
| Síntoma | Causa |
|---|---|
extends: … doesn't resolve durante la compilación | Un archivo de nivel raíz que la aplicación necesita no estaba en el árbol podado: moverlo a un paquete del espacio de trabajo |
ERR_PNPM_OUTDATED_LOCKFILE en deps | Lockfile no confirmado después de un cambio de manifiesto |
ERR_PNPM_IGNORED_BUILDS | Una nueva dependencia quiere un script de instalación: decidir en allowBuilds |
| La aplicación inicia, sin CSS/JS | .next/static o public no copiados a apps/<app>/ |
| La verificación de salud falla, los registros están bien | HOSTNAME no es 0.0.0.0 |
EROFS en tiempo de ejecución | Algo escribe fuera de .next/cache; encontrarlo, no eliminar --read-only |
NEXT_PUBLIC_X vacío | No se pasó como argumento de compilación (se hornea en la compilación) |
Verificar
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 después de ~30s
docker rm -f <app>
Fuentes
- 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