Vinicius Aguiar
DevOps

Docker para devs frontend: de cero al deploy

16 de abr. de 2026 · 10 min de lectura

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.x

Primer 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
.claude

Multi-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 -v

El 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 --build

Variables 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=production

Comandos 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 uso

Deploy 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:latest

Servicios 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_modules en volumes.
  • Imagen demasiado pesada — usar node:20 en lugar de node:20-alpine. Alpine es ~5x más pequeño.
  • Build lento — no está aprovechando cache de layers. Copia package.json antes del código.
  • Puerto no accesible — olvidó el EXPOSE o el mapeo de puerto en docker 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.