Developer Tools

Docker para Desarrolladores: Contenedores, Compose y Flujos de Trabajo Reales

Una guía práctica sobre los conceptos de Docker, buenas prácticas con Dockerfile y el uso de Docker Compose para ejecutar entornos de desarrollo con múltiples servicios.

9 min de lectura

Sala de servidores con iluminación azul

"Funciona en mi máquina" es la frase que ha provocado miles de fallos en producción. Docker resuelve esto empaquetando tu aplicación y todas sus dependencias en un contenedor: un entorno aislado y reproducible que se ejecuta de forma idéntica en cualquier lugar, desde tu portátil hasta producción.

Contenedores vs. máquinas virtuales

Máquina Virtual Contenedor
Aislamiento SO completo A nivel de proceso
Tiempo de arranque Minutos Segundos
Tamaño GBs MBs
Overhead Alto (hipervisor) Casi nulo
Caso de uso Aislamiento completo de SO Empaquetado de apps

Los contenedores comparten el kernel del host pero aíslan el sistema de archivos, los procesos y la red. Por eso son tan ligeros.

Conceptos fundamentales de Docker

Image — Un plano de solo lectura para un contenedor. Piensa en él como la definición de una clase.

Container — Una instancia en ejecución de una imagen. Piensa en él como un objeto creado a partir de la clase.

Registry — Un sistema de almacenamiento y distribución de imágenes. Docker Hub es el público por defecto; GitHub Container Registry y AWS ECR son alternativas habituales.

Volume — Almacenamiento persistente que sobrevive a los reinicios del contenedor. Los datos escritos dentro de un contenedor sin un volumen se pierden cuando el contenedor se detiene.

Cómo escribir un buen Dockerfile

# Usa una versión específica — nunca "latest" en producción
FROM node:20-alpine

# Establece el directorio de trabajo
WORKDIR /app

# Copia primero los archivos de dependencias (aprovecha la caché de capas)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Copia el código de la aplicación
COPY . .

# Compila la app
RUN npm run build

# Ejecuta como usuario no root por seguridad
USER node

# Documenta el puerto (no lo publica)
EXPOSE 3000

# Usa la forma exec para un manejo correcto de señales
CMD ["node", "server.js"]

Caché de capas: la clave para compilaciones rápidas

Cada instrucción en un Dockerfile crea una capa. Docker almacena las capas en caché: si nada ha cambiado en una capa o en sus predecesoras, Docker reutiliza la caché.

Copia los archivos de dependencias antes que el código de la aplicación. Las dependencias cambian con mucha menos frecuencia que tu código. Si copias todo de una vez, un cambio de una línea en index.js invalida la caché de instalación de dependencias y fuerza un npm install completo.

Compilaciones multi-etapa

Usa compilaciones multi-etapa para mantener las imágenes de producción ligeras:

# Etapa 1: Compilación
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Etapa 2: Producción (sin dependencias de desarrollo, sin código fuente)
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]

La imagen final contiene únicamente la salida compilada, no tu código fuente, archivos de pruebas ni dependencias de desarrollo.

Docker Compose para el desarrollo local

Docker Compose orquesta aplicaciones con múltiples contenedores. Un único docker-compose.yml define toda tu pila local:

version: "3.9"

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - .:/app
      - /app/node_modules

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Genera el esqueleto de tu pila con nuestro Docker Compose Generator — elige tus servicios y obtén un docker-compose.yml listo para producción en segundos.

Comandos esenciales de Compose

# Inicia todos los servicios en segundo plano
docker compose up -d

# Ver logs (modo seguimiento)
docker compose logs -f app

# Ejecutar un comando dentro de un contenedor en ejecución
docker compose exec app sh

# Detener y eliminar contenedores (conserva los volúmenes)
docker compose down

# Detener, eliminar contenedores Y volúmenes
docker compose down -v

# Reconstruir imágenes tras cambios en el Dockerfile
docker compose up -d --build

Variables de entorno y secretos

Nunca escribas credenciales directamente en tu Dockerfile o en el archivo de Compose. Usa un archivo .env:

# .env (¡añádelo a .gitignore!)
DATABASE_URL=postgresql://user:pass@db:5432/myapp
REDIS_URL=redis://redis:6379
JWT_SECRET=your-super-secret-key

Compose carga automáticamente el archivo .env desde el directorio del proyecto. Referencia las variables con ${VARIABLE_NAME} en tu YAML.

Usa nuestro Env Generator para generar archivos .env con valores predeterminados razonables y plantillas .env.example para tu equipo.

Redes en Docker

Compose crea una red predeterminada para tu pila. Los servicios pueden comunicarse entre sí usando el nombre del servicio como hostname — por eso la app usa db como host de la base de datos, no localhost.

app → se conecta a "db:5432"
app → se conecta a "redis:6379"

Solo los puertos mapeados explícitamente con ports: son accesibles desde la máquina host.

Health checks y orden de arranque

depends_on solo espera a que un contenedor arranque, no a que esté listo. Un contenedor de base de datos arranca en segundos, pero Postgres puede tardar unos segundos más en aceptar conexiones. Usa healthcheck + condition: service_healthy para esperar hasta que el servicio esté realmente disponible.

Consideraciones para producción

  1. Usa etiquetas de imagen específicasnode:20.11.1-alpine, no node:latest.
  2. Analiza las imágenes en busca de vulnerabilidadesdocker scout cves myapp:latest.
  3. Establece límites de recursos — evita que un solo contenedor deje sin recursos a los demás.
  4. Usa sistemas de archivos de solo lectura donde sea posible — read_only: true en Compose.
  5. Nunca ejecutes como root — añade USER node o equivalente.
  6. Configura Nginx como proxy inverso frente a tu app — usa nuestro Nginx Config Generator para obtener una configuración probada en producción.

Referencia rápida

# Imágenes
docker images                    # listar imágenes locales
docker pull nginx:alpine         # descargar imagen
docker rmi myimage               # eliminar imagen

# Contenedores
docker ps                        # contenedores en ejecución
docker ps -a                     # todos los contenedores
docker stop <id>                 # detener de forma controlada
docker rm <id>                   # eliminar contenedor detenido
docker logs <id> -f              # transmitir logs en tiempo real

# Limpieza
docker system prune              # eliminar todo lo que no se usa
docker volume prune              # eliminar volúmenes sin uso

Docker elimina toda una categoría de errores relacionados con el entorno y hace que incorporar nuevos desarrolladores sea increíblemente sencillo. Domina los fundamentos aquí y tendrás una base sólida para Kubernetes, pipelines de CI/CD y despliegues en la nube.