Saltar a contenido

ADR-0005 · La imagen de producción usa el output standalone de Next

  • Estado: aceptado — reemplaza al ADR-0003
  • Decisores: equipo TI Compulandia (hquintero)
  • Fecha de la decisión: 2026-08-26

Contexto y problema

El ADR-0003 decidió copiar el node_modules completo del builder a la etapa de runtime, para garantizar que el contenedor tuviera exactamente las dependencias con las que se compiló. Listó next build --output standalone como opción 3 y la descartó. Esa decisión se toma ahora en un contexto distinto, por tres razones nuevas:

  1. Se va a publicar la imagen en un registry. El plan del pipeline de despliegue empuja la imagen a GHCR en cada commit. Mientras la imagen no salía del host, su tamaño era un problema de disco; publicada, es ancho de banda en cada build y en cada rollback.

  2. La imagen filtra un secreto. La etapa de runtime hacía COPY --from=builder /app/.next ./.next, que arrastra .next/cache. Ese cache de webpack contiene el snapshot de entorno del build: grep del valor de REVALIDATE_SECRET dentro de la imagen lo encontraba en .next/cache/webpack/{server,client}-production/0.pack. Con la imagen publicada en un registry, cualquiera con permiso de pull lo extrae.

  3. El ADR-0004 dejó obsoleta la mitad del ADR-0003. Aquel documento justificaba npm frente a Yarn 3; desde el ADR-0004 npm es el único gestor del repositorio y esa comparación ya no describe ninguna decisión vigente.

Medido sobre la imagen del 26/08/2026: 1.28 GB, de los cuales node_modules 820 MB —incluidas todas las dependencias de desarrollo— y .next 428 MB.

Opciones consideradas

  1. output: "standalone" y copiar solo .next/standalone, .next/static y public/ (elegida).
  2. Mantener el node_modules completo y excluir únicamente .next/cache del COPY. Corta la fuga del secreto pero deja la imagen cerca de 900 MB.
  3. Mantener el statu quo y aceptar el peso y la fuga como deuda registrada.

Decisión

next.config.js declara output: "standalone". La etapa de runtime copia el servidor autocontenido que emite Next —con un node_modules que trae solo las dependencias que el build realmente usa— más .next/static y public/, que quedan fuera del output. El contenedor arranca con node server.js en lugar de npm start.

La etapa de construcción pasa a npm ci sobre node:22-alpine, alineando la imagen con el engines: node >= 22 que fijó el ADR-0004.

Consecuencia operativa: la validación de entorno se movió al arranque

server.js del standalone lleva la configuración embebida como JSON literal y nunca carga next.config.js. Eso significa que check-env-variables.js, que hasta ahora corría al inicio de next.config.js y abortaba si faltaba NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, dejaría de ejecutarse al arrancar el contenedor.

Para no perder esa validación, el archivo pasa a ser ejecutable directo (node check-env-variables.js) y el CMD lo corre antes de levantar el servidor. Como ansi-colors no forma parte del grafo de la app, Next no lo traza al standalone: el script tolera su ausencia y en el contenedor imprime el mismo mensaje sin color.

Consecuencias

Positivas

  • 1.28 GB → 264 MB. Menos ancho de banda por build, por pull y por rollback.
  • .next/cache deja de existir en la imagen, y con él la fuga de REVALIDATE_SECRET. Verificado: grep de los tres secretos server-side devuelve 0 archivos.
  • La imagen deja de incluir las dependencias de desarrollo.
  • npm ci hace el árbol de dependencias reproducible entre builds del mismo commit.

Negativas / deuda asumida

  • La validación de entorno depende ahora del CMD. Quien cambie el CMD o corra la imagen con otro comando se saltea el chequeo. Antes venía incrustada en el arranque de Next.
  • El standalone confía en el análisis de dependencias de Next. Si un módulo se carga de forma dinámica y el tracing no lo detecta, falta en runtime y no en build. No se observó ningún caso, pero el modo de falla existe y no existía copiando node_modules entero.
  • .next/static y public/ hay que copiarlos a mano: quedan fuera del output y olvidarlos produce un sitio sin estilos ni imágenes, con el servidor respondiendo 200.

Verificación

Sobre la imagen construida el 26/08/2026:

Comprobación Resultado
Tamaño 264 MB
grep de los secretos server-side en /app 0 archivos
.next/cache en la imagen no existe
Arranque sin NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY exit 1 con el mensaje de siempre
Puerto dentro del contenedor :8000
.next/static y public/ servidos 200 y 200
docker compose stop 284 ms (el exec del CMD deja pasar SIGTERM)

Con el backend Medusa disponible, sobre un build limpio (--no-cache):

Comprobación Resultado
Prerender de rutas dinámicas 687 páginas; categories y collections marcadas ● (SSG)
/, /py, /py/store, /py/categories/…, /py/account 200
HTML de /py 150 KB, <title> correcto, sin Application error
Hoja de estilos de .next/static 200
Contenido del CMS Payload en la home 154 imágenes servidas; sin el fallback de «homepage no encontrada»
Optimizador de imágenes (/_next/image) 200 image/png — sharp queda trazado al standalone

El 307 inicial no es un error

La primera petición a /py responde 307 hacia /py: el middleware redirige a la misma URL una vez para sembrar la cookie _medusa_cache_id, y con la cookie puesta renderiza. Un curl sin cookie jar entra en un bucle de redirecciones que parece una falla y no lo es.