Autoalojar Next.js y qué deja de funcionar
Next.js corre donde corra Node. Cuatro funciones las da la plataforma y no el framework, y saber cuáles son separa una migración tranquila de una con sorpresas.
"¿Podemos correr esto en nuestra propia infraestructura?" tiene una respuesta
corta y otra larga. La corta es sí: next start es un servidor de Node y
funciona en una VM, en un contenedor, detrás de tu balanceador, en tu región.
La larga es que cuatro cosas que la gente toma por funciones de Next.js las da en realidad la plataforma de hosting, y son justo las cuatro que sorprenden a los equipos una semana después de la migración.
El build que de verdad se puede desplegar
El build por defecto te deja un directorio .next que necesita al lado todo
el árbol de node_modules. Para un contenedor eso son cientos de megas de
dependencias que no ejecutas.
// next.config.ts
export default {
output: 'standalone',
};Esto rastrea los módulos que el servidor alcanza de verdad, los copia a
.next/standalone y añade un server.js que arranca sin gestor de paquetes.
En las bases de código que hemos movido, la imagen pasa de ~1,2 GB a ~180 MB.
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
# Los assets estáticos y public/ NO están dentro de standalone. Copiarlos es
# el paso que se olvida, y el síntoma es un sitio sin CSS.
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]Dos cosas de ese fichero conviene decirlas claras. .next/static y public/
no forman parte de la salida standalone y hay que copiarlos aparte: si te los
saltas, el despliegue sirve HTML sin CSS y sin imágenes. Y npm run build
corre al construir la imagen, así que toda variable de entorno que tu código
lea a nivel de módulo tiene que estar entonces, no solo en tiempo de
ejecución.
Las cuatro cosas que hacía la plataforma
Optimización de imágenes. next/image necesita algo que redimensione y
recodifique. En una plataforma eso es un servicio; en tu servidor es sharp,
en tu proceso y en tu CPU.
npm install sharpInstálalo y se usa solo. Si tu tráfico convierte ese coste de CPU en algo
real, apunta images.loader a un CDN que redimensione, o pregenera los
tamaños en el build. Lo que no puedes hacer es dejar
images.unoptimized: true en la configuración y olvidarlo: eso manda el
fichero original a cada visitante y deshace en silencio la razón por la que
usabas el componente.
ISR y la caché de revalidación. En un contenedor único funciona: la caché
está en disco local. En tres contenedores detrás de un balanceador no: cada
uno tiene su disco, revalidateTag limpia la caché de la instancia que
recibió la llamada, y las otras dos siguen sirviendo la página vieja hasta que
revaliden por su cuenta. La solución es un cache handler compartido:
// next.config.ts
export default {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // desactiva el LRU en proceso; Redis es la fuente
};Sirve cualquier handler con Redis detrás. Lo importante es entender por qué hace falta, porque si no, "la página se actualizó para unos usuarios y para otros no" parece un fallo imposible. Qué hace cada caché conviene tenerlo claro: las cuatro capas y cuál te ha mordido.
Middleware en el edge. Tu middleware ya no corre en cien ubicaciones antes de que la petición llegue a un servidor; corre en tu servidor, en tu única región. El código escrito asumiendo que es casi gratis -una consulta de geolocalización, una asignación A/B- está ahora en el camino crítico de cada petición. Ese coste es real y conviene medirlo en vez de suponerlo.
Streaming a través de tu proxy. Suspense y loading.tsx envían la
respuesta a trozos. Un nginx delante con la configuración por defecto
almacena toda la respuesta antes de mandarla, lo que convierte el streaming en
una versión más lenta de no hacer streaming, y sin ningún error que te avise.
location / {
proxy_pass http://app:3000;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection "";
}Lo que ganas
No es solo coste, aunque a cierta escala la diferencia es grande. Tienes tus datos en una jurisdicción que elegiste, que para un cliente con requisito de residencia de datos turco o europeo no es negociable. Puedes correr en la misma red que tu base de datos, lo que quita un viaje de ida y vuelta a cada consulta. Y tienes un despliegue que nadie puede cambiarte por debajo con una decisión de precios ajena.
Lo que cuesta
Ahora la disponibilidad tiene dueño. Los despliegues de vista previa por pull request, los rollbacks instantáneos y un CDN global son trabajo real de reconstruir, y un equipo que nunca ha operado infraestructura se pasará el primer mes redescubriendo por qué existen esas funciones.
Nuestra regla: si la aplicación vive en una región, si el equipo ya opera servicios, o si hay un requisito de residencia de datos, autoalojar es directamente lo correcto. Si es un sitio de marketing con dos desarrolladores y tráfico global, la plataforma sale más barata que el sueldo de quien mantiene la alternativa.
En cualquier caso, decide por esos motivos y no por un benchmark. El framework corre igual en los dos sitios: lo que se mueve son las cuatro funciones de arriba.
