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.
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_onsolo 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?
| Aspecto | Single-stage | Multi-stage |
|---|---|---|
| Tamaño imagen | ~400MB | ~150MB |
| Herramientas de build | Incluidas | Excluidas |
| Superficie de ataque | Mayor | Menor |
USER node | No | Sí (no corre como root) |
Importante: Nunca corras contenedores como root en producción. La línea
USER nodeusa el usuarionodeque 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: .envcarga variables de entorno. Nunca hardcodees contraseñas en el compose.restart: unless-stoppedhace 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
- Si quieres automatizar el deploy de tu imagen Docker, mira cómo hacerlo con GitHub Actions y Netlify/Vercel
- Para proyectos reales con NestJS y Docker, revisa el caso real de Atrapaclientes
- Si estás aprendiendo desarrollo, la guía para estudiantes DAW/DAM incluye recursos de Docker para principiantes