Si eres dev frontend y nunca usaste Docker, probablemente ya escuchaste que "funciona en mi máquina". Docker resuelve exactamente eso — empaqueta tu aplicación con todas las dependencias en un container que corre igual en cualquier lugar: en tu Mac, en la PC del colega, en CI y en producción. En esta guía, voy de cero al deploy con foco en proyectos frontend reales.
Qué es Docker (sin rodeos)
Docker es una herramienta que crea ambientes aislados (containers) para correr aplicaciones. A diferencia de una máquina virtual, un container es liviano — comparte el kernel del sistema e inicia en segundos.
- Imagen — el "molde". Contiene el sistema operativo base, Node.js, tus dependencias y tu código.
- Container — la "instancia" corriendo de la imagen. Puedes tener varios containers de la misma imagen.
- Dockerfile — la "receta" que define cómo construir la imagen.
- docker-compose — orquesta múltiples containers (app + base de datos + redis) en un solo comando.
Instalación
Instala Docker Desktop para tu sistema operativo. Incluye Docker Engine, Docker CLI y Docker Compose:
- macOS: descarga en docker.com/products/docker-desktop o usa
brew install --cask docker - Windows: descarga Docker Desktop (requiere WSL2)
- Linux: instala vía apt/yum o sigue la documentación oficial
Verifica la instalación:
docker --version
# Docker version 24.x.x
docker compose version
# Docker Compose version v2.x.xPrimer Dockerfile: aplicación Next.js
Vamos a crear un Dockerfile para una aplicación Next.js. Lo más básico posible para entender el concepto:
# Dockerfile
FROM node:20-alpine
WORKDIR /app
# Copiar dependencias primero (cache de layers)
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
# Copiar código
COPY . .
# Build
RUN pnpm build
# Correr
EXPOSE 3000
CMD ["pnpm", "start"]Cada línea es una layer. Docker hace cache de cada layer — si el package.json no cambió, salta la instalación de dependencias. Por eso copiamos las dependencias antes del código.
# Construir la imagen
docker build -t mi-app .
# Correr el container
docker run -p 3000:3000 mi-app
# Acceder: http://localhost:3000.dockerignore: qué no entra en la imagen
Igual que .gitignore, .dockerignore define qué no debe copiarse dentro de la imagen. Esto reduce el tamaño y acelera el build:
# .dockerignore
node_modules
.next
.git
.env
.env.local
README.md
.vscode
.claudeMulti-stage build: imagen de producción optimizada
El Dockerfile básico funciona pero la imagen final es pesada — contiene todas las devDependencies, código fuente y archivos de build. En producción solo necesitas el output del build. Multi-stage build resuelve esto:
# Dockerfile (multi-stage)
# Stage 1: Instalar dependencias
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable && pnpm build
# Stage 3: Producción (solo lo necesario)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Copiar solo lo necesario del build
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]La diferencia es significativa:
- Dockerfile básico: ~1GB (node_modules completo, código fuente, devDependencies)
- Multi-stage: ~150MB (solo Node.js + output del build + archivos estáticos)
Docker Compose: ambiente de dev completo
Para desarrollo local, docker-compose orquesta múltiples servicios. Un escenario real: app Next.js + PostgreSQL + Redis:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
volumes:
- .:/app
- /app/node_modules
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:16-alpine
ports:
- '5432:5432'
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
pgdata:# Subir todo con un comando
docker compose up -d
# Ver logs
docker compose logs -f app
# Tumbar todo
docker compose down
# Tumbar y limpiar volumes (reset de la base)
docker compose down -vEl volumes: - .:/app monta tu código local dentro del container — cambios en el código se reflejan inmediatamente sin rebuild. El - /app/node_modules evita que el node_modules local sobrescriba el del container.
Ambiente de dev vs producción
Es común tener dos Dockerfiles o usar target en compose:
# docker-compose.dev.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- '3000:3000'
volumes:
- .:/app
- /app/node_modules
command: pnpm dev# Dockerfile.dev (simple, sin multi-stage)
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install
COPY . .
EXPOSE 3000
CMD ["pnpm", "dev"]# Dev
docker compose -f docker-compose.dev.yml up
# Producción
docker compose up --buildVariables de entorno
Nunca pongas secrets en el Dockerfile. Usa variables de entorno vía .env o docker-compose:
# .env (no commitear)
DATABASE_URL=postgresql://user:pass@db:5432/myapp
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_API_URL=http://localhost:3000/api# docker-compose.yml
services:
app:
env_file:
- .env
# o individualmente:
environment:
- NODE_ENV=productionComandos esenciales
Los comandos que uso diariamente:
# Imágenes
docker build -t app . # Construir imagen
docker images # Listar imágenes
docker rmi app # Remover imagen
# Containers
docker ps # Containers corriendo
docker ps -a # Todos (incluyendo parados)
docker logs -f <container> # Logs en tiempo real
docker exec -it <container> sh # Entrar al container
docker stop <container> # Parar
docker rm <container> # Remover
# Compose
docker compose up -d # Subir en background
docker compose down # Tumbar
docker compose down -v # Tumbar + limpiar volumes
docker compose logs -f # Logs de todos los servicios
docker compose build --no-cache # Rebuild sin cache
# Limpieza
docker system prune -a # Remover todo lo que no está en usoDeploy en producción
Con la imagen multi-stage lista, el deploy es enviar a un registry y correr en el servidor:
# Build y tag
docker build -t mi-app:latest .
# Push a Docker Hub (o ECR, GCR, etc.)
docker tag mi-app:latest usuario/mi-app:latest
docker push usuario/mi-app:latest
# En el servidor (pull y run)
docker pull usuario/mi-app:latest
docker run -d -p 3000:3000 --env-file .env usuario/mi-app:latestServicios como Render, Railway y Fly.io detectan el Dockerfile automáticamente y hacen build + deploy sin configuración extra.
Errores comunes
- node_modules dentro de la imagen + volume local — el volume sobrescribe el node_modules del container. Solución: agregar
- /app/node_modulesen volumes. - Imagen demasiado pesada — usar
node:20en lugar denode:20-alpine. Alpine es ~5x más pequeño. - Build lento — no está aprovechando cache de layers. Copia
package.jsonantes del código. - Puerto no accesible — olvidó el
EXPOSEo el mapeo de puerto endocker run -p. - Variables de entorno no llegan — en Next.js, variables
NEXT_PUBLIC_*necesitan estar disponibles en build time, no solo en runtime.
Resumen
Docker no es solo para DevOps — es una herramienta de productividad para cualquier dev. Con un Dockerfile y un docker-compose, garantizas que el ambiente es consistente del dev al deploy. La inversión de aprendizaje es pequeña comparada al retorno: menos bugs de ambiente, onboarding más rápido y deploy predecible.
