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:
-
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.
-
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:grepdel valor deREVALIDATE_SECRETdentro de la imagen lo encontraba en.next/cache/webpack/{server,client}-production/0.pack. Con la imagen publicada en un registry, cualquiera con permiso depulllo extrae. -
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¶
output: "standalone"y copiar solo.next/standalone,.next/staticypublic/(elegida).- Mantener el
node_modulescompleto y excluir únicamente.next/cachedelCOPY. Corta la fuga del secreto pero deja la imagen cerca de 900 MB. - 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
pully por rollback. .next/cachedeja de existir en la imagen, y con él la fuga deREVALIDATE_SECRET. Verificado:grepde los tres secretos server-side devuelve 0 archivos.- La imagen deja de incluir las dependencias de desarrollo.
npm cihace el árbol de dependencias reproducible entre builds del mismo commit.
Negativas / deuda asumida¶
- La validación de entorno depende ahora del
CMD. Quien cambie elCMDo 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_modulesentero. .next/staticypublic/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.