Desplegar una app fullstack de Next.js 16 en AWS Amplify Gen 2 (SSR): seis fallos y cómo evitarlos
Conseguir que una app de Next.js 16 con un backend de Amplify Gen 2 desplegara en el cómputo SSR de Amplify Hosting costó seis compilaciones fallidas. Cada fallo aparecía una capa más abajo que el anterior, así que los logs resultaron engañosos hasta el final. Este artículo recorre la cadena completa — síntoma, causa raíz, solución — más una lista de verificación para llegar directo a una compilación en verde.
Stack: Next.js 16 (App Router, Turbopack), Amplify Gen 2 (defineBackend, Cognito + AppSync/DynamoDB + S3), transcodificación de audio en el servidor con ffmpeg.
Lista de verificación rápida
Si vas a desplegar Next.js 16 + Amplify Gen 2 SSR, haz esto por adelantado:
- Verifica el lock con
npm ci, nunca solo connpm install. Amplify compila connpm ci, que es estricto.npm installpuede dejar el lock en un estado quenpm cirechaza, sin avisar. - Cuenta con que el límite de 220 MB de cómputo SSR te va a golpear. El soporte gestionado de Next.js en Amplify apunta a las versiones 12–15; en la 16 empaqueta el paquete
nextcompleto y revienta el límite. Mantén los binarios grandes (ffmpeg, navegadores headless, etc.) fuera del bundle de hosting. - No intentes esquivar el handler de Next con un framework
Web Computing. Amplify detecta Next y rechaza la ruta genérica de la especificación de despliegue. - Si necesitas un binario nativo grande en tiempo de ejecución, ponlo en una Lambda + layer, no en el servidor de Next. Construye el layer con bundling en el host (sin Docker) e instala
xzconsudodurante la compilación. - Haz coincidir los runtimes compatibles del layer con el runtime real de la función (Amplify ahora usa
nodejs22.xpor defecto en las funciones).
Fallo 1 — npm ci falla: lockfile desincronizado
Síntoma (log de compilación):
npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and
package-lock.json ... are in sync.
npm error Invalid: lock file's semver@7.7.1 does not satisfy semver@7.8.5
Causa. Una dependencia transitiva de semver viene empaquetada dentro de @aws-amplify/data-construct y @aws-amplify/graphql-api-construct fijada en 7.7.1, mientras que otra parte de su subárbol requiere ^7.8.x. npm install tolera la inconsistencia (e incluso la reintroduce); npm ci, que es lo que ejecuta Amplify, la rechaza.
Solución. Regenera el lock para que la resolución sea internamente consistente y luego verifica con npm ci en local:
npm install --package-lock-only # recomputes a consistent tree
npm ci # must exit 0 — this is the real test
git add package-lock.json && git commit -m "fix: resync package-lock"
Detalles a tener en cuenta.
- Los
overridesdenpmno arreglan esto — la copia obsoleta está empaquetada dentro del tarball de la dependencia, así que los overrides no la alcanzan. - Ejecutar un
npm installnormal después (por ejemplo, para añadir una dependencia) puede revertir el arreglo. Vuelve a ejecutarnpm cisiempre antes de hacer push.
Fallo 2 — la salida de la compilación excede el límite de 220 MB de SSR
Síntoma:
CustomerError: The size of the build output (249282439) exceeds the max
allowed size of 230686720 bytes.
Causa. Amplify Hosting limita el bundle de cómputo SSR a 220 MB. Su empaquetado gestionado de Next.js soporta oficialmente Next 12–15; en la 16 recurre a enviar el paquete next completo (~173 MB), y nuestro binario ffmpeg-static (~77 MB) lo empujó hasta ~249 MB.
Lo que no funciona:
- Podar las dependencias de desarrollo (
npm prune --omit=devenamplify.yml). El tamaño que Amplify mide sale del file trace de Next, no de tunode_modulesreal, así que podar dev deps no cambia nada. - Salida standalone + la especificación de despliegue (ver Fallo 3).
Lo que sí funciona: sacar el binario grande del bundle de hosting por completo (ver La solución real, más abajo). Para confirmar qué hay realmente en el bundle trazado, suma los archivos referenciados por los archivos de traza .nft.json dentro de .next — eso es lo que Amplify envía.
Fallo 3 — el desvío por la especificación de despliegue (un callejón sin salida en Next)
Idea tentadora: compilar la salida standalone (que reduce next a ~16 MB), organizarla según la especificación de despliegue de Amplify (una carpeta .amplify-hosting con compute, static y un deploy-manifest.json) y sacar el framework de la rama de Next para que Amplify la despliegue tal cual:
aws amplify update-branch --app-id <id> --branch-name main \
--framework 'Web Computing' --region <region>
Dos muros, en este orden:
CustomerError: Can't find required-server-files.json in build output directory
y luego, en cuanto cambias el framework:
CustomerError: It looks like you are attempting to deploy a Next.js SSR app,
but your app's framework looks wrong. Please update your app's framework to
'Next.js - SSR' ...
Causa. La imagen de compilación de Amplify detecta Next.js y fuerza su handler nativo. Next.js - SSR valida un .next en crudo (empaquetado nativo, de vuelta al Fallo 2). Web Computing se rechaza porque ve Next. No hay configuración que permita a una app de Next usar la ruta genérica de la especificación. No te metas por ahí. Devuelve el framework a su sitio:
aws amplify update-branch --app-id <id> --branch-name main \
--framework 'Next.js - SSR' --region <region>
La solución real: ffmpeg en un layer de Lambda
Dado que el framework queda atado al Next nativo, la única forma de bajar del límite es sacar el binario grande del bundle de hosting. Mueve el trabajo a una función Lambda que lleve ffmpeg como un layer con versión fijada, e invócala desde la ruta (aquí mediante una mutación de AppSync autorizada como invitado sobre S3). Resultado: los node_modules trazados bajaron de 117 MB a 41 MB; el bundle nativo quedó en ~173 MB, cómodamente por debajo de los 220 MB.
Pieza clave — construir el layer en tiempo de synth, en el host (sin Docker), porque la imagen de compilación de Amplify no tiene demonio de Docker:
// amplify/backend.ts (abridged)
const FFMPEG_TARBALL_URL =
'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz'
const FFMPEG_TARBALL_SHA256 = 'abda8d77…' // pin + fail closed on drift
const ffmpegLayer = new LayerVersion(scope, 'FfmpegLayer', {
compatibleRuntimes: [Runtime.NODEJS_20_X, Runtime.NODEJS_22_X], // see Failure 6
compatibleArchitectures: [Architecture.X86_64],
code: Code.fromAsset(join(amplifyDir, 'layers/ffmpeg'), {
assetHashType: AssetHashType.CUSTOM,
assetHash: FFMPEG_TARBALL_SHA256,
bundling: {
image: DockerImage.fromRegistry('public.ecr.aws/amazonlinux/amazonlinux:2023'),
local: { tryBundle: bundleFfmpegLocally }, // curl + tar on the host
command: ['bash', '-c', '/* docker fallback */'],
},
}),
})
lambda.addLayers(ffmpegLayer)
Fija el binario contra un SHA-256 registrado para que un cambio upstream haga fallar la compilación en lugar de enviar en silencio un binario distinto. La compilación estática es agnóstica del runtime y no depende de glibc, así que funciona sobre el sistema de archivos de Lambda de Amazon Linux 2023.
El mismo principio aplica a cualquier cosa pesada que el servidor necesite en tiempo de ejecución (Chromium headless, toolchains de imágenes, runtimes de ML): mantenla fuera del cómputo de hosting de Next.
Fallo 4 — falta xz en la imagen de compilación
Síntoma (durante el synth de CDK / bundling del layer):
tar (child): xz: Cannot exec: No such file or directory
Causa. La compilación estática de ffmpeg se distribuye como un .tar.xz; tar -xJf necesita la utilidad xz, que la imagen AL2023 de Amplify no incluye.
Solución. Instálala en la fase de compilación del backend, antes de que pipeline-deploy ejecute el bundling:
# amplify.yml
backend:
phases:
build:
commands:
- (command -v xz >/dev/null 2>&1) || sudo dnf install -y xz || sudo yum install -y xz
- npm ci --cache .npm --prefer-offline
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
Fallo 5 — el usuario de compilación no es root
Síntoma. La instalación de xz del Fallo 4 falla con "has to be run with superuser privileges."
Causa. El usuario de compilación de Amplify no es root.
Solución. Usa sudo — las imágenes de compilación de Amplify lo incluyen. Por eso el comando del Fallo 4 se escribe como sudo dnf install -y xz || sudo yum install -y xz.
Fallo 6 — incompatibilidad entre layer y runtime
Síntoma (ensamblado de CDK, tras un synth correcto):
[IncompatibleLayerRuntime] This lambda function uses a runtime that is
incompatible with this layer (nodejs22.x is not in [nodejs20.x])
Causa. Amplify Gen 2 ahora usa nodejs22.x por defecto en las funciones, pero el layer declaraba únicamente nodejs20.x.
Solución. Declara ambos runtimes en el layer (al binario estático le da igual), o fija el runtime de la función para que coincida:
compatibleRuntimes: [Runtime.NODEJS_20_X, Runtime.NODEJS_22_X]
Cómo evitar toda la cadena la próxima vez
- Trata el límite de 220 MB de SSR como una restricción de diseño. Antes de desplegar, suma los archivos de traza de
.next. Todo lo que sea grande y binario pertenece a un layer de Lambda, no al servidor de Next. - Prefiere una versión de Next soportada por Amplify (12–15) si puedes. En la 16 estás fuera del empaquetado gestionado y heredas el sobrepeso del "paquete
nextcompleto". npm cies tu barrera antes del push. Detecta la desincronización del lockfile queinstall,buildytestse saltan sin inmutarse.- El bundling del layer tiene que hacerse en el host (no hay Docker en la imagen de compilación de Amplify) y puede necesitar herramientas extra (
xz) instaladas consudo. - Fija los binarios por SHA-256 y falla en cerrado. Si no, una URL "latest" que se mueve acabará enviándote una compilación sorpresa meses después.
- Mantén el framework de la rama como
Next.js - SSR. La especificación genérica de despliegue no es una vía de escape para las apps de Next.
La escalera de fallos (cada compilación bajó un peldaño más)
- Compilación 1 — llegó a la instalación; falló por la desincronización del lockfile en
npm ci. - Compilación 2 — llegó a la compilación; 249 MB sobre el límite de 220 MB.
- Compilación 3 — llegó a la compilación; especificación de despliegue rechazada (
required-server-files.jsony después el framework). - Compilación 4 — llegó al synth; faltaba
xz. - Compilación 5 — llegó al synth;
dnfnecesita root. - Compilación 6 — llegó al ensamblado; layer
nodejs20.xfrente a funciónnodejs22.x. - Compilación 7 — despliegue en verde.