Docker para Next.js: De Desarrollo a Producción con Multi-Stage Build
guía paso a paso para dockerizar tu proyecto Next.js. Multi-stage build, standalone output, Docker Compose para dev, y optimización de imagen de 2GB a menos de 200MB.
Docker para Next.js: De Desarrollo a Producción con Multi-Stage Build
Docker para tu proyecto Next.js es de esas herramientas que una vez que configuras, no entiendes como deployabas antes. En vez de depender de la configuración de cada servidor, empaquetas tu app en un contenedor que corre igual en tu maquina, en CI, y en producción.
El problema es que un Dockerfile mal configurado genera imágenes de 2GB o más. Con la configuración correcta -- multi-stage build y standalone output -- esa misma imagen baja a menos de 200MB. Esta guía te lleva paso a paso desde cero.
por qué Docker para tu proyecto Next.js
Si deployeas en Vercel, probablemente no necesitas Docker. Vercel maneja todo por ti. Pero Docker es la respuesta cuando:
- Deployeas en tu propio servidor o VPS (DigitalOcean, Railway, AWS EC2)
- Usas Kubernetes para orquestar múltiples servicios
- Tu proyecto tiene dependencias de sistema que necesitas controlar (como sharp para optimizar imágenes)
- Necesitas el mismo entorno en desarrollo, CI/CD y producción
- Tu empresa tiene infraestructura propia y no usa servicios managed
según el Stack Overflow Developer Survey 2025, Docker tuvo el mayor crecimiento de adopción de cualquier tecnologia: +17 puntos en un solo año. Ya no es opcional -- es parte del stack moderno.
Requisitos previos
- Node.js 18+ instalado
- Docker Desktop instalado (docker.com)
- Un proyecto Next.js funcionando localmente
- Conocimiento básico de terminal
Si no tienes un proyecto, puedes crear uno rápido:
npx create-next-app@latest mi-app --typescript --tailwind --app
cd mi-appConfigurar Next.js para standalone output
El primer paso es decirle a Next.js que genere una versión standalone de tu app. Esto es lo que hace posible que la imagen Docker sea liviana.
Abre tu next.config.ts y agrega la opción output:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;qué hace output: "standalone"? Cuando corres next build, Next.js analiza las dependencias que tu app realmente usa y genera una carpeta .next/standalone/ con solo esas dependencias. En vez de copiar 500MB+ de node_modules, standalone incluye solo lo necesario (~30MB).
Esto es crítico para Docker. Sin standalone, tu imagen copia TODOS los node_modules al contenedor. Con standalone, copia una fracción.
El Dockerfile optimizado (multi-stage build)
Este es el Dockerfile completo. después explico cada línea:
# Etapa 1: Instalar dependencias
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# Etapa 2: Build de la aplicación
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Etapa 3: Runtime de producción
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Usuario no-root por seguridad
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copiar solo lo necesario del build
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]qué hace cada línea del Dockerfile
Etapa 1: Dependencias
FROM node:22-alpine AS depsUsa Node.js 22 sobre Alpine Linux. Alpine pesa ~5MB en vez de los ~900MB de la imagen completa de Node. El AS deps le da nombre a esta etapa para referenciarla después.
COPY package.json package-lock.json ./
RUN npm ci --ignore-scriptsCopia SOLO los archivos de dependencias primero. Esto aprovecha el layer caching de Docker: si tus dependencias no cambian, Docker reutiliza esta capa sin reinstalar nada. npm ci es más rápido y determinista que npm install porque usa el lockfile exacto.
Etapa 2: Build
FROM node:22-alpine AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run buildEmpieza una imagen limpia, copia los node_modules de la etapa anterior, luego copia todo el código fuente y corre el build. El resultado es la carpeta .next/ con tu app compilada.
Etapa 3: Runtime (la imagen final)
Esta es la imagen que realmente se deploya. No tiene node_modules completos, no tiene código fuente, no tiene dependencias de desarrollo:
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjsCrea un usuario dedicado. Nunca corras contenedores como root en producción. Si alguien logra explotar una vulnerabilidad en tu app, el dano esta limitado a los permisos de este usuario.
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/staticCopia solo tres cosas del build:
/public-- tus archivos estáticos (imágenes, favicon, etc.).next/standalone-- el servidor mínimo con solo las dependencias necesarias.next/static-- los assets generados (JS/CSS bundles)
El --chown=nextjs:nodejs asigna los archivos al usuario no-root.
CMD ["node", "server.js"]El standalone output genera un server.js en la raiz. Este archivo ES tu servidor de producción. No necesitas next start ni ningún otro comando.
El .dockerignore que todos olvidan
Sin .dockerignore, Docker copia TODO al contexto del build. Incluyendo node_modules, .next, .git y cualquier otra cosa en tu proyecto. Esto hace el build más lento y la imagen más pesada.
Crea un .dockerignore en la raiz:
node_modules
.next
.git
.gitignore
*.md
docker-compose*.yml
.env
.env.*Importante: nunca incluyas archivos .env en tu imagen Docker. Las variables de entorno se pasan al contenedor en runtime, no en build time. Si necesitas variables durante el build (como NEXT_PUBLIC_*), pasalas como build args.
Si manejas API keys o secrets en tu proyecto, Asegúrate de que no esten expuestos en tu repo. Herramientas como datahogo escanean tu repositorio y detectan credenciales filtradas automáticamente.
Docker Compose para desarrollo local
Para desarrollo, no quieres multi-stage build ni standalone. Quieres hot reload y acceso rápido. Este docker-compose.yml levanta tu app con todo lo que necesitas:
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
db:
image: postgres:17-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: miapp
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:Y el Dockerfile.dev para desarrollo:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]Con docker compose up levantas tu app Next.js y una base de datos PostgreSQL en segundos. Los volumes montan tu código local en el contenedor, así que los cambios se reflejan al instante con hot reload.
Build y run: de imagen a contenedor
Build de la imagen
docker build -t mi-app-nextjs .La primera vez toma unos minutos. Las siguientes son mucho más rapidas gracias al layer caching.
Verificar el tamaño
docker images mi-app-nextjsCon la configuración de esta guía, tu imagen debería estar entre 150MB y 200MB. Si ves más de 500MB, algo salio mal -- probablemente falta el output: "standalone" en next.config.ts.
Correr el contenedor
docker run -p 3000:3000 --env-file .env.production mi-app-nextjsTu app esta corriendo en http://localhost:3000. El --env-file inyecta las variables de entorno en runtime, sin que queden en la imagen.
Comparativa de tamaño
| Configuración | tamaño de imagen |
|---|---|
| Sin optimizar (COPY node_modules completo) | ~2.1 GB |
| Con Alpine (sin standalone) | ~800 MB |
| Con Alpine + standalone | ~180 MB |
| Con Alpine + standalone + .dockerignore correcto | ~150 MB |
La diferencia no es solo de disco. Una imagen más liviana significa deploys más rapidos, cold starts más cortos, y menos costos de almacenamiento en tu registry.
Errores comunes y como resolverlos
"Module not found" después del build
Standalone a veces no detecta dependencias que se importan de forma dinámica. Agrega esas dependencias a serverExternalPackages en tu config:
// next.config.ts
const nextConfig: NextConfig = {
output: "standalone",
serverExternalPackages: ["sharp", "bcrypt"],
};La carpeta public no se copia
Standalone no incluye /public ni .next/static automáticamente. Por eso los copiamos explicitamente en el Dockerfile. Si te faltan assets, verifica que esas líneas COPY esten presentes.
Variables de entorno no disponibles
Las variables NEXT_PUBLIC_* se inyectan en build time, no en runtime. Si necesitas que esten disponibles en la imagen, pasalas como build args:
# En el Dockerfile, etapa builder
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URLdocker build --build-arg NEXT_PUBLIC_API_URL=https://api.ejemplo.com -t mi-app .Las variables del servidor (sin NEXT_PUBLIC_) si funcionan en runtime con --env-file.
El contenedor corre como root
Si olvidaste agregar el usuario no-root, tu contenedor corre como root. Esto es un riesgo de seguridad. Verifica con:
docker exec -it <container-id> whoamidebería mostrar nextjs, no root.
Siguiente paso: automatizar con CI/CD
Ya tienes tu app dockerizada. El siguiente paso logico es automatizar el build y deploy con cada push. Si usas GitHub Actions, tengo una guía completa de CI/CD con GitHub Actions para Next.js que cubre exactamente eso.
también te puede servir la guía de variables de entorno en Next.js y Vercel para manejar correctamente los secrets entre ambientes.
Si estas evaluando donde deployar tu contenedor, la guía de deploy en Vercel te ayuda a comparar opciones.
Preguntas frecuentes
¿Cómo dockerizar una aplicación Next.js?
Necesitas tres cosas: configurar output: "standalone" en next.config.ts, crear un Dockerfile con multi-stage build (dependencias, build, runtime), y un .dockerignore. Con esto generas una imagen optimizada lista para producción.
¿Qué es output standalone en Next.js?
Es una configuración que genera una versión mínima de tu app con solo las dependencias necesarias. En vez de copiar todos los node_modules (500MB+), standalone incluye solo lo que tu app realmente importa (~30MB). Lo activas con output: "standalone" en next.config.ts.
¿Cuánto pesa una imagen Docker de Next.js optimizada?
Con multi-stage build y standalone output, una imagen típica queda entre 150MB y 200MB. Sin optimizar, la misma app puede pesar más de 2GB. La diferencia impacta directamente en tiempos de deploy y costos de hosting.
¿Docker o Vercel para deployar Next.js?
Depende de tu caso. Vercel es más simple si tu proyecto es estandar y no necesitas control total de la infraestructura. Docker tiene sentido cuando corres en tu propio servidor, usas Kubernetes, tienes requisitos especificos de red o seguridad, o necesitas el mismo entorno en todos tus ambientes.
¿Cómo uso Docker Compose con Next.js y una base de datos?
Creas un docker-compose.yml con dos servicios: tu app Next.js y tu base de datos. Docker Compose levanta ambos contenedores en la misma red, y tu app se conecta a la base de datos usando el nombre del servicio como hostname. En esta guía hay un ejemplo completo con PostgreSQL.
Preguntas frecuentes
¿Cómo dockerizar una aplicación Next.js?
Necesitas tres cosas: configurar output standalone en next.config.ts, crear un Dockerfile con multi-stage build (dependencias, build, runtime), y un .dockerignore. Con esto generas una imagen optimizada lista para producción.
¿Qué es output standalone en Next.js?
Es una configuración de Next.js que genera una versión mínima de tu aplicación con solo las dependencias necesarias para correr. En vez de copiar todos los node_modules (500MB+), standalone incluye solo lo que tu app realmente importa (~30MB).
¿Cuánto pesa una imagen Docker de Next.js optimizada?
Con multi-stage build y standalone output, una imagen típica de Next.js queda entre 150MB y 200MB. Sin optimizar, la misma app puede pesar más de 2GB. La diferencia es enorme en tiempos de deploy y costos de hosting.
¿Docker o Vercel para deployar Next.js?
Si tu proyecto es estandar y no necesitas control total, Vercel es más simple. Docker tiene sentido cuándo necesitas control sobre la infraestructura, corres en tu propio servidor, usas Kubernetes, o tienes requisitos especificos de red o seguridad.
¿Cómo uso Docker Compose con Next.js y una base de datos?
Creas un docker-compose.yml con dos servicios: tu app Next.js y tu base de datos (PostgreSQL, por ejemplo). Docker Compose levanta ambos contenedores en la misma red, y tu app se conecta a la base de datos por el nombre del servicio.
Articulos relacionados
Next.js 16: guía de Migración y Novedades
Migra tu proyecto de Next.js 15 a 16. Novedades principales, breaking changes, y pasos para actualizar sin romper tu app.
Testing en Next.js con Vitest y Playwright
Configura testing en tu proyecto Next.js. Unit tests con Vitest, E2E con Playwright, y como integrarlos en tu pipeline de CI/CD.
Tailwind CSS 4: Migración desde v3
Migra tu proyecto de Tailwind CSS 3 a 4. Cambios principales, nuevo sistema de configuración, CSS-first config y como actualizar sin romper tu app.