Dockerizar Next.js: Los desafíos ocultos

Tenía un mes queriendo escribir este artículo. Hay una gran diferencia entre un proyecto pequeño en Vercel y una aplicación real en producción. En Vercel, todo es fácil y se siente color de rosa porque la complejidad está oculta.
Cuando tu proyecto crece, necesitas más control. Pasas de una configuración simple a una infraestructura grande con múltiples pods y reglas de escalado. Yo uso Google Cloud para mis proyectos, pero estos retos son los mismos si usas AWS o Azure.
Este cambio representa una gran transformación en la arquitectura. En Vercel, la plataforma gestiona el sistema por ti. Fuera de Vercel, tú eres el arquitecto. En este artículo, quiero compartir el primer tropiezo que tuve al dockerizar Next.js. Nunca pensé que me daría problemas, pero así fue: las variables de entorno.
Arquitectura en Vercel vs. Arquitectura autogestionada
Cuando usas Vercel, la plataforma te da una "caja negra" que maneja tu compilación, tu despliegue, tu SSL y tu CDN. Cuando pasas a tu propia infraestructura (como Docker en GCP), necesitas reemplazar cada pieza de esa caja.
graph TD
subgraph "The Vercel Way (Managed)"
A[Git Push] --> B[Vercel Build Engine]
B --> C[Vercel Edge Network]
C --> D[User]
end
subgraph "The Self-Managed Way (Docker)"
E[Git Push] --> F[CI/CD Pipeline\nGitHub Actions/GitLab]
F -->|Build Image| G[Container Registry\nArtifact Registry/ECR]
G -->|Deploy| H[Cloud Hosting\nCloud Run/App Runner]
I[Secret Manager] -.->|Runtime Secrets| H
H --> J[Load Balancer / CDN]
J --> K[User]
endPara que esta arquitectura funcione, la parte más crítica es el pipeline de compilación (Build Pipeline). Aquí es donde tu código se convierte en una imagen de Docker, y es justo donde la mayoría de los desarrolladores se topan con su primer gran problema: las variables de entorno.
El muro técnico: Variables de entorno
Antes de armar nuestra arquitectura, debemos entender cómo maneja Next.js las variables de entorno. Las separa en dos grupos distintos: variables en tiempo de compilación (Build-Time Variables) y variables en tiempo de ejecución (Runtime Variables). En Vercel, esta conexión es transparente: agregas tus variables en el panel y funcionan en ambos casos. En tu propia arquitectura, debes manejar esto manualmente en tu Dockerfile y en tu CI/CD.
Variables en tiempo de compilación (NEXT_PUBLIC_)
Las variables que comienzan con NEXT_PUBLIC_ quedan incrustadas en el bundle final de JavaScript cuando ejecutas npm run build. Son visibles para el navegador.
Ejemplo: URLs de APIs o IDs de analíticas.
Si olvidas agregar estas variables durante el paso de compilación, el archivo JavaScript final tendrá valores undefined o cadenas vacías. Esto ocurre incluso si intentas agregarlas más tarde al iniciar la aplicación.
Variables en tiempo de ejecución (privadas)
Las variables sin este prefijo son privadas y existen únicamente en el servidor. No se empaquetan durante la compilación; en su lugar, se accede a ellas en el momento de procesar cada petición.
Ejemplo: URLs de bases de datos, tokens de APIs o claves de pago.
El escenario
Imagina una aplicación que necesita consultar datos de una API externa. En desarrollo local, todo funciona sin esfuerzo. Creas un archivo .env.local y Next.js toma los valores de forma automática:
# .env.local
NEXT_PUBLIC_API_URL=https://dummyjson.com/products
API_SECRET_KEY=my-super-secret-key-123NEXT_PUBLIC_API_URL: Es para el frontend. Le indica a tu aplicación desde dónde consultar la información.API_SECRET_KEY: Es una clave privada para la lógica de tu backend.
Cuando ejecutas npm run dev, Next.js lee estas variables y tu aplicación funciona perfectamente. En tu código, accedes a ellas de esta forma:
// Frontend: Esto formará parte del bundle de JS
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// Backend: Esto se quedará únicamente en el servidor
const secret = process.env.API_SECRET_KEY;Esta "magia" sucede porque Next.js está diseñado para hacer sencillo el desarrollo local. Pero tan pronto como pasas a Docker, esta magia desaparece.
Pasar a Docker
Después de probar la aplicación en local, podrías pensar que pasar a Docker es simple. Si npm run dev y npm run build funcionan en tu máquina, la imagen de Docker también debería funcionar.
Creé mi primer Dockerfile de esta forma:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build # ❌ ¡Faltan las variables NEXT_PUBLIC_!
CMD ["npm", "start"]Construí la imagen, inicié el contenedor y encontré mi primer tropiezo. La aplicación estaba corriendo, pero las variables del frontend no estaban. Esto pasa porque Next.js necesita las variables NEXT_PUBLIC_ durante el proceso de compilación. En mi Dockerfile simple, el comando npm run build no tenía acceso a estas variables.
¿El resultado? Los archivos compilados se crearon con valores undefined.
La solución: ARG y ENV
Para solucionar esto, necesitamos pasar estas variables durante la fase de compilación usando ARG y ENV de Docker.
ARG(Build Argument): Es una variable temporal que se usa únicamente mientras se construye la imagen.ENV(Environment Variable): Es una variable permanente dentro del contenedor.
Esta es la forma correcta de escribir el Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
# 1. Capturar los argumentos en tiempo de compilación
ARG NEXT_PUBLIC_API_URL
# 2. Asignar a ENV para el proceso de compilación
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
CMD ["npm", "start"]Ahora puedes construir la imagen con los valores correctos:
docker build --build-arg NEXT_PUBLIC_API_URL="https://api.com" -t my-app .¿Qué pasa con las variables privadas?
Las variables privadas (sin el prefijo NEXT_PUBLIC_) son diferentes. Nunca debes ponerlas en el Dockerfile. Estas variables se usan cuando el servidor está corriendo, no cuando construyes la imagen.
Puedes inyectarlas al momento de iniciar el contenedor:
docker run -e API_SECRET_KEY="my-secret" my-appConsideraciones de seguridad
Cuando llevas tu aplicación a un entorno de producción, la seguridad es muy importante. En Vercel, agregas tus secretos en el panel y se encriptan de forma automática.
En tu propia infraestructura, podrías pensar en usar el flag -e API_SECRET_KEY=... al iniciar Docker. Esta no es una buena idea para un proyecto real. No es seguro ni escalable. Escribir tus contraseñas en texto plano en la terminal es un riesgo grande.
En su lugar, deberías usar un Secret Manager. Los grandes proveedores cloud como Google Cloud, AWS y Azure ofrecen servicios para gestionar tus secretos de forma segura.
Cuando tu contenedor arranca, el proveedor cloud inyecta estos secretos directamente en el entorno. Esta es una forma de trabajar mucho mejor, ya que tus secretos nunca quedan guardados en los logs de tu CI/CD, en el historial de la terminal ni en las capas de tu imagen de Docker. Esto mantiene tu aplicación protegida a medida que escala a múltiples pods y configuraciones complejas.
Resumen
Ahora que nuestra imagen de Docker está lista, podemos desplegarla en plataformas serverless modernas como Google Cloud Run. Si quieres un recorrido completo sobre precios, dominios personalizados y configuración de Cloud Build, revisa mi guía paso a paso sobre cómo migrar de Vercel a Google Cloud Run.
En un entorno de producción real, podrías usar una configuración más compleja con muchos pods y Kubernetes. Quise enfocarme en este primer tropiezo porque es la razón más común por la que fallan los despliegues de Next.js.
¿Listo para el siguiente paso? Lee mi guía sobre Next.js en Google Cloud Run: caché de producción, secretos y cero arranques en frío para manejar la gestión de secretos y edge caching.
Espero que esta guía te sea útil cuando migres tu aplicación desde Vercel hacia un entorno cloud real.
¡Happy coding!
Foto de Travis Fish en Unsplash
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Preguntas Frecuentes
¿Cuál es el principal desafío al dockerizar una aplicación de Next.js fuera de Vercel?
El principal desafío es que Next.js maneja las variables de entorno de forma diferente en tiempo de compilación y en tiempo de ejecución, y Vercel se encarga de esto automáticamente en segundo plano. Cuando construyes una imagen de Docker por tu cuenta, debes proporcionar de forma explícita las variables con el prefijo NEXT_PUBLIC_ durante el paso de compilación porque se incrustan en el bundle de JavaScript, mientras que las variables privadas del servidor deben inyectarse al iniciar el contenedor.
¿Cuál es la diferencia entre las variables de entorno en tiempo de compilación y en tiempo de ejecución en Next.js?
Las variables en tiempo de compilación tienen el prefijo NEXT_PUBLIC_ y se incrustan en el bundle de JavaScript del cliente durante el paso npm run build. Si faltan en la compilación, el bundle contendrá valores undefined. Las variables en tiempo de ejecución no tienen prefijo, existen únicamente en el servidor y se leen cada vez que se procesa una petición, por lo que pueden suministrarse cuando el contenedor inicia sin tener que reconstruir la imagen.
¿Cómo paso variables de entorno a un contenedor Docker de Next.js durante el paso de compilación?
En tu Dockerfile, usa ARG para declarar la variable y ENV para asignarla, y luego pasa el valor mediante el flag --build-arg al ejecutar docker build. En un pipeline de CI/CD, leerías los valores desde tu gestor de secretos o del entorno del pipeline y los enviarías como argumentos de compilación para que estén disponibles cuando se empaquete el código del cliente.
¿Por qué mi variable NEXT_PUBLIC_ aparece como undefined al desplegar Next.js en Docker?
Esto ocurre porque las variables NEXT_PUBLIC_ deben estar presentes en el momento en que ejecutas el comando de compilación, no solo cuando inicia el contenedor. Si las pasas únicamente como variables de entorno en tiempo de ejecución mediante docker run o en un manifiesto de despliegue de Kubernetes, Next.js no las verá durante el empaquetado y el código del cliente contendrá undefined. Necesitas pasarlas como argumentos de compilación de Docker al crear la imagen.
¿Qué diferencias de arquitectura debo esperar al pasar de Vercel a un hosting con Docker autogestionado?
En Vercel, la compilación, el CDN, el SSL y el enrutamiento edge se gestionan automáticamente por ti. Cuando pasas a Docker en plataformas como Google Cloud o AWS, te haces responsable de cada una de esas capas por separado. Necesitas un pipeline de CI/CD para compilar y subir tu imagen, un registro de contenedores para almacenarla, un servicio de hosting para ejecutarla, un balanceador de carga para el enrutamiento del tráfico y un gestor de secretos para inyectar la configuración sensible en tiempo de ejecución.
Artículos Relacionados
De Vercel Next.js a Google Cloud Run: Guía de costos y arquitectura
Por qué migré mis proyectos de Next.js de Vercel a Google Cloud Run, cómo funciona la compilación standalone con Docker y cómo mantenerse en el nivel gratuito.
Next.js en Google Cloud Run: Caché en producción, secretos y cero cold starts
Una guía práctica para llevar Next.js a producción en Google Cloud Run: configura el caché de archivos estáticos con Cloud CDN, monta variables de entorno con Secret Manager, reduce los cold starts y automatiza tus despliegues con Cloud Build.
Cómo empezar con Docker: Guía paso a paso para principiantes
Saber desplegar aplicaciones con Docker es una habilidad muy demandada hoy en día. Esta guía te enseñará desde cero a instalarlo, compilar imágenes, montar volúmenes y orquestar servicios con Docker Compose.
Mi newsletter caía en Spam: cómo solucioné la entregabilidad gratis con Cloudflare
Un amigo me avisó que mis correos caían en su carpeta de Spam. Así fue como solucioné mi entregabilidad usando Cloudflare Email Routing y autenticación de dominio personalizado por $0.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.