Por que Docker para Next.js
Docker resuelve el clasico problema de "funciona en mi maquina" empaquetando tu aplicacion Next.js junto con todo su entorno de ejecucion en un contenedor portable. Ya sea que ejecutes tu aplicacion en el portatil de un desarrollador, en un pipeline de CI/CD o en un servidor de produccion, el comportamiento es identico porque el contenedor incluye cada dependencia, configuracion y libreria del sistema que tu aplicacion necesita.
Aunque plataformas como Vercel proporcionan despliegues sin configuracion para Next.js, Docker te da control total sobre tu infraestructura. Esto es esencial cuando necesitas:
- Auto-alojar en tu propia infraestructura: Desplegar en AWS, Google Cloud, DigitalOcean o servidores propios
- Garantizar reproducibilidad: Asegurar que el mismo artefacto de compilacion se ejecuta en cada entorno
- Orquestar multiples servicios: Ejecutar tu app Next.js junto a bases de datos, caches y workers en segundo plano
- Cumplir requisitos de compliance: Mantener control sobre donde se almacenan y procesan tus datos
- Integrar con CI/CD existente: Encajar en pipelines basados en Docker con herramientas como GitHub Actions, GitLab CI o Jenkins
- Escalar horizontalmente: Desplegar multiples replicas de contenedores detras de un balanceador de carga usando Kubernetes o Docker Swarm
Fundamentos del Dockerfile
Un Dockerfile es un archivo de texto con instrucciones que Docker utiliza para construir una imagen. Cada instruccion crea una capa en la imagen, y Docker cachea estas capas para acelerar compilaciones posteriores. Comprender las instrucciones clave es esencial para crear contenedores Next.js eficientes.
# Dockerfile simple de una sola etapa para Next.js
FROM node:20-alpine
WORKDIR /app
# Copiar archivos de paquete primero para mejor cacheo de capas
COPY package.json package-lock.json ./
# Instalar dependencias
RUN npm ci
# Copiar el resto del codigo de la aplicacion
COPY . .
# Compilar la aplicacion Next.js
RUN npm run build
# Exponer el puerto en el que escucha Next.js
EXPOSE 3000
# Establecer el comando por defecto
CMD ["npm", "start"]Instrucciones clave del Dockerfile explicadas:
- FROM: Establece la imagen base —
node:20-alpinees una imagen minima de Node.js basada en Alpine Linux - WORKDIR: Establece el directorio de trabajo dentro del contenedor — todos los comandos posteriores se ejecutan desde esta ruta
- COPY: Copia archivos del host al interior de la imagen del contenedor
- RUN: Ejecuta un comando durante el proceso de compilacion — se usa para instalar dependencias y compilar
- EXPOSE: Documenta en que puerto escucha el contenedor (no publica el puerto realmente)
- CMD: Especifica el comando por defecto a ejecutar cuando el contenedor arranca
Importante: Siempre copia
package.jsonypackage-lock.jsonantes de copiar el resto del codigo fuente. Docker cachea cada capa, y dado que las dependencias cambian con menos frecuencia que el codigo fuente, este patron evita reinstalar todas las dependencias en cada compilacion.
Compilaciones Multi-Etapa para Imagenes Optimizadas
Un Dockerfile de una sola etapa incluye todas las herramientas de compilacion, dependencias de desarrollo y codigo fuente en la imagen final. Las compilaciones multi-etapa resuelven esto usando etapas separadas para instalar dependencias, compilar y ejecutar. La imagen final solo contiene lo necesario en tiempo de ejecucion, reduciendo drasticamente su tamano.
# Etapa 1: Instalar dependencias
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && cp -R node_modules /prod_node_modules && npm ci
# Etapa 2: Compilar la aplicacion
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Desactivar telemetria de Next.js durante la compilacion
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Etapa 3: Runner de produccion
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Crear un usuario no-root por seguridad
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
# Copiar solo los archivos necesarios de la etapa de compilacion
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Establecer la propiedad correcta
RUN chown -R nextjs:nodejs /app
# Cambiar al usuario no-root
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]Este enfoque de tres etapas produce una imagen final que es tipicamente un 80-90% mas pequena que una compilacion de una sola etapa. La imagen de produccion contiene solo el runtime de Node.js, tu aplicacion compilada y las dependencias de produccion.
Desglose de etapas
- Etapa deps: Instala tanto dependencias de produccion como de desarrollo. Copia las dependencias solo de produccion por separado para usar en la etapa final
- Etapa builder: Copia el codigo fuente y todas las dependencias, luego ejecuta el proceso de compilacion de Next.js para producir la salida optimizada
- Etapa runner: Comienza desde una imagen Alpine limpia, copia solo la salida standalone, archivos estaticos y assets publicos. Se ejecuta como usuario no-root por seguridad
Modo de Salida Standalone
El modo de salida standalone de Next.js es la clave para crear imagenes Docker minimas. Cuando
se activa, Next.js rastrea todos los modulos importados y crea una salida autocontenida que
incluye solo los archivos necesarios para ejecutar la aplicacion — sin necesidad del directorio
node_modules.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
// Opcional: reducir el tamano de imagen aun mas desactivando
// la optimizacion de imagenes si la manejas externamente (ej. via CDN)
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.example.com",
},
],
},
};
export default nextConfig;
Con la salida standalone activada, la compilacion produce un directorio .next/standalone
que contiene un archivo server.js minimo y solo los modulos de Node.js que tu
aplicacion realmente importa. Esto tipicamente reduce el tamano del despliegue de varios cientos
de megabytes a menos de 50MB.
Nota: La salida standalone no incluye la carpeta
public/ni el directorio.next/static. Debes copiarlos manualmente en tu Dockerfile, como se muestra en el ejemplo de compilacion multi-etapa anterior.
El archivo .dockerignore
Un archivo .dockerignore indica a Docker que archivos excluir al copiar el
contexto de compilacion en el contenedor. Esto acelera las compilaciones y previene que
archivos sensibles o innecesarios terminen en tu imagen.
# .dockerignore
node_modules
.next
.git
.gitignore
*.md
LICENSE
.env
.env.*
.vscode
.idea
.DS_Store
Thumbs.db
docker-compose*.yml
Dockerfile*
.dockerignore
npm-debug.log*
yarn-debug.log*
yarn-error.log*
coverage
.nyc_output
__tests__
*.test.ts
*.test.tsx
*.spec.ts
*.spec.tsx- node_modules: Las dependencias se instalan dentro del contenedor — nunca copies los
node_modulesdel host - .next: La salida de compilacion se genera dentro del contenedor durante
RUN npm run build - .git: El historial de Git es innecesario en el contenedor y puede ser muy grande
- Archivos .env: Las variables de entorno deben inyectarse en tiempo de ejecucion, nunca incorporarse en la imagen
- Archivos de test: Los tests no pertenecen a las imagenes de produccion
Docker Compose para Desarrollo Local
Docker Compose te permite definir y ejecutar configuraciones multi-contenedor con un solo comando. Para desarrollo local, puedes ejecutar tu aplicacion Next.js junto a una base de datos PostgreSQL, cache Redis y cualquier otro servicio del que dependa tu app.
# docker-compose.yml
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
- /app/.next
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
- REDIS_URL=redis://redis:6379
- NEXT_PUBLIC_ENABLE_CONTACT_FORM=true
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
volumes:
postgres_data:
redis_data:Dockerfile de desarrollo
# Dockerfile.dev — optimizado para desarrollo con hot reload
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]Caracteristicas clave de esta configuracion de Docker Compose:
- Montaje de volumenes: El montaje
.:/apphabilita el hot reloading — los cambios en tu host se reflejan en el contenedor inmediatamente - Volumenes anonimos:
/app/node_modulesy/app/.nextse excluyen del montaje del host para que el contenedor use sus propias versiones - Dependencias de servicios: El
depends_onconcondition: service_healthyasegura que la base de datos este lista antes de que la app arranque - Health checks: PostgreSQL incluye un health check que verifica que la base de datos acepta conexiones
- Datos persistentes: Los volumenes nombrados (
postgres_data,redis_data) persisten datos entre reinicios del contenedor
# Iniciar todos los servicios
docker compose up -d
# Ver logs
docker compose logs -f app
# Detener todos los servicios
docker compose down
# Detener y eliminar volumenes (resetear datos)
docker compose down -v
# Recompilar despues de cambios en dependencias
docker compose up -d --buildVariables de Entorno y Docker Secrets
Las variables de entorno en Docker pueden pasarse en tiempo de compilacion o de ejecucion. Para aplicaciones Next.js, es critico entender la diferencia entre variables de tiempo de compilacion y de ejecucion.
# Variables de tiempo de compilacion (disponibles durante npm run build)
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_ENABLE_CONTACT_FORM
# Variables de tiempo de ejecucion (disponibles cuando el contenedor esta corriendo)
ENV NODE_ENV=production
ENV PORT=3000
Las variables con prefijo NEXT_PUBLIC_ se incrustan en el JavaScript del lado
del cliente durante la compilacion. Esto significa que deben estar disponibles como argumentos
de compilacion, no solo como variables de entorno en tiempo de ejecucion.
# Compilar con variables de entorno publicas
docker build --build-arg NEXT_PUBLIC_API_URL=https://api.example.com --build-arg NEXT_PUBLIC_ENABLE_CONTACT_FORM=true -t myapp:latest .
# Ejecutar con variables de entorno del lado del servidor
docker run -d -p 3000:3000 -e MAILJET_API_KEY=tu_clave -e MAILJET_API_SECRET=tu_secreto -e MAILJET_SENDER_EMAIL=hello@raymartin.es -e DATABASE_URL=postgresql://user:pass@host:5432/db myapp:latestSeguridad: Nunca uses
ENVpara secretos en tu Dockerfile. Los valores establecidos conENVse incorporan en las capas de la imagen y pueden ser extraidos. Usadocker run -eo Docker secrets para valores sensibles.
Docker Secrets con Compose
# docker-compose.prod.yml
version: "3.9"
services:
app:
image: myapp:latest
ports:
- "3000:3000"
secrets:
- mailjet_api_key
- mailjet_api_secret
- db_url
environment:
- MAILJET_API_KEY_FILE=/run/secrets/mailjet_api_key
- MAILJET_API_SECRET_FILE=/run/secrets/mailjet_api_secret
- DATABASE_URL_FILE=/run/secrets/db_url
secrets:
mailjet_api_key:
file: ./secrets/mailjet_api_key.txt
mailjet_api_secret:
file: ./secrets/mailjet_api_secret.txt
db_url:
file: ./secrets/db_url.txtHealth Checks
Los health checks permiten a Docker y a los orquestadores de contenedores saber si tu aplicacion esta funcionando correctamente. Si un health check falla, el contenedor puede reiniciarse o reemplazarse automaticamente.
# Agregar health check al Dockerfile
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1Crea una ruta API simple de health check en tu aplicacion Next.js:
// app/api/health/route.ts
import { NextResponse } from "next/server";
export async function GET() {
try {
// Opcionalmente verificar conectividad con la base de datos
// await db.query("SELECT 1");
return NextResponse.json(
{
status: "healthy",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.APP_VERSION || "desconocido",
},
{ status: 200 }
);
} catch (error) {
return NextResponse.json(
{
status: "unhealthy",
error: error instanceof Error ? error.message : "Error desconocido",
},
{ status: 503 }
);
}
}
export const dynamic = "force-dynamic";- interval: Con que frecuencia ejecutar el health check (30 segundos es un buen valor por defecto)
- timeout: Tiempo maximo de espera para una respuesta antes de considerar el check como fallido
- start-period: Periodo de gracia despues del inicio del contenedor durante el cual los fallos no se contabilizan — da tiempo a tu app para inicializarse
- retries: Numero de fallos consecutivos necesarios antes de marcar el contenedor como no saludable
Compilacion y Push a un Container Registry
Un container registry almacena tus imagenes Docker para que puedan ser descargadas por los destinos de despliegue. Los registros mas populares incluyen Docker Hub, GitHub Container Registry (GHCR), AWS ECR y Google Artifact Registry.
# Compilar la imagen con una etiqueta
docker build -t ghcr.io/raymartin/myapp:latest .
docker build -t ghcr.io/raymartin/myapp:v1.2.3 .
# Autenticarse en GitHub Container Registry
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
# Subir la imagen
docker push ghcr.io/raymartin/myapp:latest
docker push ghcr.io/raymartin/myapp:v1.2.3Compilaciones automatizadas con GitHub Actions
# .github/workflows/docker-build.yml
name: Compilar y Subir Imagen Docker
on:
push:
branches: [main]
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout del repositorio
uses: actions/checkout@v4
- name: Configurar Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Iniciar sesion en GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extraer metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix=
- name: Compilar y subir
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NEXT_PUBLIC_ENABLE_CONTACT_FORM=trueDespliegue en Plataformas Cloud
AWS ECS (Elastic Container Service)
{
"family": "nextjs-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"containerDefinitions": [
{
"name": "nextjs",
"image": "123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest",
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"environment": [
{ "name": "NODE_ENV", "value": "production" },
{ "name": "PORT", "value": "3000" }
],
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:db-url"
}
],
"healthCheck": {
"command": ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 15
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/nextjs-app",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}Google Cloud Run
# Compilar y subir a Google Artifact Registry
gcloud builds submit --tag gcr.io/my-project/myapp:latest
# Desplegar en Cloud Run
gcloud run deploy myapp --image gcr.io/my-project/myapp:latest --platform managed --region us-central1 --port 3000 --memory 512Mi --cpu 1 --min-instances 0 --max-instances 10 --set-env-vars NODE_ENV=production --set-secrets DATABASE_URL=db-url:latest,MAILJET_API_KEY=mailjet-key:latest --allow-unauthenticatedDigitalOcean App Platform
# .do/app.yaml
name: nextjs-app
region: nyc
services:
- name: web
dockerfile_path: Dockerfile
github:
repo: raymartin/myapp
branch: main
deploy_on_push: true
http_port: 3000
instance_count: 2
instance_size_slug: professional-xs
health_check:
http_path: /api/health
initial_delay_seconds: 15
period_seconds: 30
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
type: SECRET
value: "${db.DATABASE_URL}"
databases:
- name: db
engine: PG
version: "16"
size: db-s-1vcpu-1gb
num_nodes: 1Optimizacion para Produccion
Imagenes Alpine para tamano minimo
Las imagenes de Node.js basadas en Alpine son significativamente mas pequenas que las imagenes
basadas en Debian por defecto. La imagen node:20-alpine tiene aproximadamente 50MB
comparada con los 350MB de node:20.
# Comparar tamanos de imagen
docker images --format "table {{.Repository}} {{.Tag}} {{.Size}}"
# REPOSITORY TAG SIZE
# myapp debian 1.2GB
# myapp alpine 180MB
# myapp alpine-standalone 85MBEstrategias de cacheo de capas
# Orden optimo de capas para eficiencia del cache
FROM node:20-alpine AS deps
WORKDIR /app
# 1. Copiar solo archivos de paquete (cambian raramente)
COPY package.json package-lock.json ./
RUN npm ci
# 2. Copiar archivos de configuracion (cambian ocasionalmente)
COPY next.config.ts tsconfig.json tailwind.config.ts postcss.config.js ./
# 3. Copiar codigo fuente (cambia frecuentemente)
COPY app/ ./app/
COPY components/ ./components/
COPY content/ ./content/
COPY hooks/ ./hooks/
COPY messages/ ./messages/
COPY public/ ./public/
COPY routes/ ./routes/
COPY styles/ ./styles/
COPY utils/ ./utils/
COPY middleware.ts i18n.ts environment.d.ts ./
RUN npm run buildEscaneo de seguridad
# Escanear imagen en busca de vulnerabilidades con Docker Scout
docker scout cves myapp:latest
# Escanear con Trivy (codigo abierto)
trivy image myapp:latest
# Escanear con Snyk
snyk container test myapp:latestMejores practicas de seguridad para contenedores Docker en produccion:
- Ejecutar como no-root: Siempre crea y cambia a un usuario no-root en tu Dockerfile
- Usar etiquetas especificas: Fija tu imagen base a una version especifica como
node:20.11-alpineen lugar denode:20-alpine - Escanear regularmente: Integra el escaneo de vulnerabilidades en tu pipeline de CI/CD
- Minimizar superficie de ataque: Usa compilaciones multi-etapa para excluir herramientas de compilacion de la imagen final
- Actualizar imagenes base: Recompila regularmente con imagenes base actualizadas para incorporar parches de seguridad
- Sistema de archivos de solo lectura: Monta el sistema de archivos raiz como solo lectura cuando sea posible usando
--read-only - Sin secretos en imagenes: Nunca almacenes credenciales, claves API o tokens en las capas de la imagen
Consejo profesional: Combina compilaciones multi-etapa con el modo de salida standalone para obtener la imagen de produccion mas pequena posible. Una imagen Docker de Next.js bien optimizada puede tener menos de 100MB, lo que significa despliegues mas rapidos, menores costes de almacenamiento y tiempos de arranque de contenedor mas rapidos. Ejecuta
docker imagesdespues de cada optimizacion para medir el impacto de tus cambios.
Docker te da propiedad total de tu pipeline de despliegue. Combinando compilaciones multi-etapa, salida standalone, health checks y mejores practicas de seguridad, puedes desplegar aplicaciones Next.js en cualquier infraestructura con confianza. Ya sea que elijas AWS, Google Cloud, DigitalOcean o tus propios servidores, la aplicacion containerizada se comporta de forma identica en todas partes.