¿Qué Node.js compila tu app en Amplify? El log no lo dice

Un log de compilación de Amplify sin ninguna línea con la versión de Node, junto a los tres runtimes que Amplify Hosting admite y el comando nvm que ahora imprime v24.21.0 en cada build

Estaba cerrando una incidencia de higiene de repositorio en una app pequeña de Next.js 16 con backend Amplify Gen 2. Una tarea decía: declara engines.node con la versión mayor de Node con la que se compila el proyecto y comprueba que la imagen de build de Amplify la cumple. Fácil. Abro el último log de build de staging, busco la línea con la versión de Node, apunto el número.

No hay ninguna línea con la versión de Node. El log tiene 298 líneas: clonado, caché, npm ci, una columna de npm warn deprecated, ampx pipeline-deploy, next build, listo. Ninguna dice qué Node ejecutó nada de eso. Llevaba un año desplegando en Amplify Hosting sin saberlo.

TL;DR

  1. La imagen de build por defecto de Amplify Hosting (Amazon Linux 2023) trae Node 18 y 20 y usa uno de ellos salvo que digas lo contrario. El log no imprime cuál.
  2. En una app Next.js con compute, el runtime SSR es la versión mayor de Node que compiló la app. El valor por defecto silencioso decide tu runtime de producción, y Lambda retiró su runtime nodejs20.x el 30 de abril de 2026.
  3. Amplify Hosting lista hoy Node 20, 22 y 24 como runtimes SSR admitidos. Fija el más nuevo en .nvmrc y deja que amplify.yml ejecute nvm install && nvm use && node --version antes de cada fase.
  4. Mantén engines.node como suelo y @types/node en la misma versión mayor, y sube los tres juntos cuando Amplify liste una más nueva.
  5. Prefiere nvm en el repositorio antes que las Live package updates de la consola. La versión pertenece al control de versiones, y latest rompe el build de todos modos.

Lo que el log sí dice, y lo que no

Un log de build cuenta muchas cosas por accidente. El mío mostraba npm warn deprecated glob@7.2.3, glob@10.5.0, glob@11.1.0: tres generaciones de un paquete, ninguna de Node. Mostraba npm ci terminando, Turbopack compilando en 16 segundos, páginas estáticas prerenderizadas. No mostraba node -v, porque nada en el build por defecto lo ejecuta y el runner de Amplify no anuncia el runtime que te entrega.

Así que el número tuvo que salir de la documentación. Tres hechos, de tres páginas distintas de AWS:

  • Cuando Amplify pasó su imagen de build por defecto a Amazon Linux 2023 en 2024, el anuncio decía que Node 18 y 20 vienen preinstalados, con nvm disponible para cualquier versión más nueva.
  • La página de funciones SSR dice que cuando Amplify compila y despliega una app Next.js con compute, usa la versión de runtime de Node.js que coincide con la versión mayor de Node.js usada para compilar la app. Si no especificas una, Amplify usa la versión que esté usando el contenedor de build.
  • La misma página lista los runtimes que Hosting sigue admitiendo: Node 20, 22 y 24. Node 14, 16 y 18 ya no.

En conjunto: mi app se compilaba casi con seguridad en Node 20, porque Next.js 16 rechaza cualquier versión anterior a 20.9 y la imagen no ofrece nada más nuevo por defecto. Eso convertía la función SSR de producción en una Lambda nodejs20.x, sobre un runtime que la propia Lambda había retirado seis meses antes. Nada estaba roto. Nada me habría avisado cuando se rompiera.

Veintisiete apps, cero pins

Antes de tocar la app comprobé cómo lo manejaba el resto de mis proyectos, ya que todos usan el mismo stack. La API de Amplify lista cada app de la cuenta con su build spec y sus variables de entorno, que es donde las Live package updates de la consola terminan como una cadena JSON _LIVE_UPDATES.

Veintisiete apps. Ninguna fijaba Node, ni en el build spec ni en Live package updates. Unas cuantas antiguas tenían _CUSTOM_IMAGE apuntando a la imagen de Amazon Linux 2, que es otro problema. Cada app Next.js 16 de la familia, incluidas las dos sobre las que más escribo en este sitio, se compilaba con lo que la imagen eligiera. La tarea de higiene que había tomado para una app era un hueco de toda la familia.

Dos formas de fijar la versión, una de ellas en el repo

Amplify ofrece dos mecanismos. La consola tiene Live package updates, donde eliges un paquete, por ejemplo Node.js, y una versión; el runner de build la instala antes de ejecutar tus comandos. La documentación avisa de que elegir latest para Node rompe el build, así que pones una versión mayor exacta. Funciona, y vive fuera del repositorio, en un ajuste que nadie revisa en un diff.

El otro mecanismo es el que cualquier desarrollador de Linux ya conoce. La imagen de build trae nvm, así que una línea en amplify.yml puede instalar cualquier versión que nombres. Es la que usé, con un refinamiento: el archivo no nombra ninguna versión.

version: 1
backend:
  phases:
    build:
      commands:
        # Fija Node según .nvmrc: la imagen por defecto trae Node 18/20 y el
        # runtime SSR sigue a la versión mayor que compiló la app.
        - nvm install && nvm use && node --version
        - npm ci --cache .npm --prefer-offline
        - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
  phases:
    preBuild:
      commands:
        - nvm install && nvm use && node --version
        - npm ci --cache .npm --prefer-offline
    build:
      commands:
        - nvm use
        - npm run build

nvm install sin argumento lee .nvmrc, y .nvmrc tiene una sola línea:

24

Tres detalles son deliberados:

  • node --version dentro del comando. La siguiente persona que abra un log de build tendrá la respuesta que yo no tenía. La línea ahora dice Now using node v24.21.0 (npm v11.19.0), seguida de v24.21.0, tanto en la fase de backend como en la de frontend.
  • nvm use otra vez en la fase build del frontend. Amplify ejecuta las fases en un mismo shell, así que la selección de preBuild se mantiene, pero un comando de un segundo que hace la fase autodescriptiva es más barato que descubrir lo contrario.
  • La versión aparece en exactamente un archivo. package.json dice "engines": { "node": ">=24" }, un suelo y no un pin, porque mi máquina corre Node 26 y el suelo es lo que npm puede hacer cumplir. @types/node está en ^24 para que los tipos describan el runtime. Cuando Amplify liste Node 26, los tres se mueven juntos y nada más cambia.

El primer build tras el cambio descargó Node 24.21.0 en unos tres segundos por fase, compiló, desplegó, y la función SSR ahora corre en nodejs24.x. La app desplegada no cambió ni un byte de código de aplicación.

Por qué 24 y no 22

Mi primera versión de este arreglo fijaba 22. Era la versión que tenía en la cabeza como "actual", la búsqueda en la documentación que hice solo preguntaba si el valor por defecto de la imagen estaba por debajo de 22, y el propio requisito del tooling de Amplify Gen 2 dice >=22. Todo cierto, y todo un poco atrasado.

La regla que acabé escribiendo en la guía de ingeniería compartida de todos mis proyectos es más simple que un número: fija la versión mayor más nueva que Amplify Hosting liste como runtime SSR admitido, y muévete cuando la lista se mueva. Hoy es 24. Un pin en 22 habría sido correcto exactamente hasta que alguien leyera bien la página de runtimes admitidos, que es el tipo de corrección que caduca en silencio.

Una nota sobre hacer esto con un agente de IA

Un agente de programación con IA hizo la implementación, y cometió el error de arriba: leyó el log de build, no encontró ninguna línea de Node, buscó en la documentación, concluyó que el valor por defecto de la imagen estaba por debajo de 22 y añadió nvm install 22 && nvm use 22 al build spec. Razonable, con fuentes, y más estrecho de lo que la pregunta merecía.

Lo que lo atrapó fue una pregunta de una línea mía: ¿revisaste la documentación con la skill de Amplify, y cómo lo hacemos en los otros proyectos? La respuesta honesta fue no y no. De la segunda pasada salieron el listado de veintisiete apps, la lista de runtimes admitidos y la regla del "más nuevo admitido", y ahí el 22 escrito a mano se convirtió en un nvm install a secas que lee .nvmrc.

La lección para el agente fue a su memoria: antes de cualquier decisión sobre Amplify, Node o el arnés de pruebas, invocar la skill de Amplify y comparar con los proyectos hermanos, y luego presentar las opciones. La lección para mí es que "¿cómo lo hacen los otros proyectos?" es la pregunta de revisión más barata que tengo.

Reglas que generalizan

  • Un valor por defecto silencioso es una decisión que alguien tomó por ti. Si una plataforma elige un runtime, averigua cuál y escríbelo donde un diff pueda verlo.
  • Haz que el log de build responda la pregunta. Una línea node --version no cuesta nada y acaba con la arqueología.
  • Un solo archivo es dueño de la versión. .nvmrc lo leen nvm, la mayoría de los editores y las personas. Todo lo demás deriva de él o declara un suelo.
  • Fija según la lista de la plataforma, no según un número que recuerdes. Las listas admitidas se mueven; los números recordados no.
  • Revisa toda la familia antes de arreglar a un miembro. El hueco que encontré en una app estaba en las veintisiete.

Referencias