Saltar al contenido principal

Docker para Desarrolladores: Guía Práctica sin Teoría Innecesaria (2026)

Aprende Docker desde cero con ejemplos reales: contenedores, Dockerfile, docker-compose y deploy. Sin rodeos, solo lo que necesitas para desarrollo y producción.

Fran Cobos 7 min de lectura 1303 palabras

Tabla de contenidos

Llevas 3 horas intentando instalar PostgreSQL 16 en tu máquina. En Windows te pide un servicio, en Mac se pelea con Homebrew, y tu compañero tiene la versión 14 porque “a él le funciona así”.

Con Docker, es un comando:

docker run -d --name postgres -e POSTGRES_PASSWORD=secret -p 5432:5432 postgres:16

PostgreSQL 16 corriendo en 5 segundos. Misma versión para todo el equipo. Sin instalar nada en tu sistema.

Lo mínimo que necesitas saber

Imagen vs Contenedor

  • Imagen: La receta. Un paquete con el sistema operativo, dependencias y tu código. Es inmutable.
  • Contenedor: El plato cocinado. Una instancia ejecutándose de esa imagen. Puedes tener varios contenedores de la misma imagen.
# Descargar una imagen
docker pull node:22-alpine

# Crear y ejecutar un contenedor desde la imagen
docker run -it node:22-alpine node -e "console.log('Hola Docker')"

Comandos que vas a usar el 90% del tiempo

# Ver contenedores corriendo
docker ps

# Ver TODOS (incluidos los parados)
docker ps -a

# Parar un contenedor
docker stop nombre_o_id

# Eliminar un contenedor
docker rm nombre_o_id

# Ver logs
docker logs nombre_o_id

# Entrar en un contenedor
docker exec -it nombre_o_id sh

Tu primer Dockerfile

Un Dockerfile define cómo construir la imagen de tu aplicación. Este es para una app Node.js:

# Imagen base ligera
FROM node:22-alpine

# Directorio de trabajo dentro del contenedor
WORKDIR /app

# Copiar solo package.json primero (aprovecha caché de capas)
COPY package*.json ./
RUN npm ci --only=production

# Copiar el resto del código
COPY . .

# Puerto que expone la app
EXPOSE 3000

# Comando para arrancar
CMD ["node", "src/index.js"]

Por qué COPY package*.json va primero

Docker cachea cada instrucción como una “capa”. Si copias primero package.json y luego haces npm ci, Docker solo reinstala dependencias cuando cambias package.json. Si cambias un archivo de código, salta directamente al COPY . .el build tarda segundos en vez de minutos.

Construir y ejecutar

# Construir la imagen
docker build -t mi-api .

# Ejecutar el contenedor
docker run -d -p 3000:3000 --name mi-api mi-api

# Verificar que funciona
curl http://localhost:3000

.dockerignore — No copies basura al contenedor

Crea un .dockerignore en la raíz del proyecto:

node_modules
npm-debug.log
.git
.env
dist
*.md

Sin esto, Docker copia node_modules (que puede pesar 500MB+) dentro del contenedor y luego los reinstala con npm ci. Doble desperdicio.

Docker Compose — Varios servicios con un comando

Tu API necesita base de datos, Redis para caché y tal vez Elasticsearch. Sin Docker Compose, necesitas 3 terminales con 3 comandos. Con Compose, un solo archivo:

# docker-compose.yml
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

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

volumes:
  pgdata:

Comandos de Compose

# Levantar todo (en segundo plano)
docker compose up -d

# Ver logs de todos los servicios
docker compose logs -f

# Parar todo
docker compose down

# Parar y eliminar volúmenes (⚠️ borra datos de DB)
docker compose down -v

Truco: depends_on solo espera a que el contenedor arranque, NO a que el servicio esté listo. Para esperar a que PostgreSQL acepte conexiones, usa un health check.

Health checks

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    build: .
    depends_on:
      db:
        condition: service_healthy

Ahora api no arranca hasta que PostgreSQL esté realmente listo para recibir conexiones.

Dockerfile para producción (multi-stage)

El Dockerfile básico funciona, pero incluye herramientas de build (npm, compiladores) que no necesitas en producción. Multi-stage build lo soluciona:

# --- Stage 1: Build ---
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# --- Stage 2: Production ---
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]

¿Qué cambia?

AspectoSingle-stageMulti-stage
Tamaño imagen~400MB~150MB
Herramientas de buildIncluidasExcluidas
Superficie de ataqueMayorMenor
USER nodeNoSí (no corre como root)

Importante: Nunca corras contenedores como root en producción. La línea USER node usa el usuario node que viene incluido en las imágenes oficiales de Node.js.

Ejemplo real: API NestJS + PostgreSQL + Redis

Este es un docker-compose.yml de un proyecto real, similar al que usamos en el caso real de Atrapaclientes:

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    ports:
      - "3000:3000"
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASS}
    volumes:
      - redisdata:/data
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Notas clave:

  • env_file: .env carga variables de entorno. Nunca hardcodees contraseñas en el compose.
  • restart: unless-stopped hace que los contenedores se reinicien si caen (excepto si los paras manualmente).
  • Los volúmenes (pgdata, redisdata) persisten datos entre reinicios del contenedor.

Errores típicos y cómo evitarlos

1. “Port already in use”

# Algo ya está usando el puerto 5432
docker compose up -d
# Error: port is already allocated

# Solución: encuentra qué lo usa
# Windows
netstat -ano | findstr :5432

# Linux/Mac
lsof -i :5432

2. Cambios en el código no se reflejan

En desarrollo, monta tu código como volumen para que los cambios se apliquen en caliente:

  api:
    build: .
    volumes:
      - ./src:/app/src  # Monta el código fuente
    command: npm run dev  # Usa nodemon o tsx --watch

3. La imagen pesa demasiado

# Ver el tamaño de tus imágenes
docker images

# Usa alpine como base (mucho más ligera)
FROM node:22-alpine    # ~130MB
# vs
FROM node:22           # ~1.1GB

4. “Cannot connect to database” al arrancar

Tu API arranca antes que la DB esté lista. Usa health checks (explicado arriba) o añade retry en tu código:

// Retry de conexión a la DB
async function connectWithRetry(retries = 5, delay = 3000) {
  for (let i = 0; i < retries; i++) {
    try {
      await dataSource.initialize();
      console.log('DB conectada');
      return;
    } catch (err) {
      console.log(`Intento ${i + 1}/${retries} fallido, reintentando...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error('No se pudo conectar a la DB');
}

Cheatsheet: Comandos Docker esenciales

# --- Imágenes ---
docker build -t nombre .          # Construir imagen
docker images                      # Listar imágenes
docker rmi nombre                  # Eliminar imagen
docker image prune -a             # Limpiar imágenes sin usar

# --- Contenedores ---
docker run -d -p 3000:3000 nombre # Ejecutar en background
docker ps                          # Ver contenedores activos
docker stop $(docker ps -q)       # Parar TODOS
docker rm $(docker ps -aq)        # Eliminar TODOS

# --- Compose ---
docker compose up -d              # Levantar servicios
docker compose down               # Parar servicios
docker compose logs -f api        # Logs de un servicio
docker compose exec api sh        # Entrar en un servicio

# --- Limpieza ---
docker system prune -a            # Limpiar TODO lo que no se usa
docker volume prune               # Limpiar volúmenes huérfanos

Próximos pasos

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.