1. ¿Qué es Docker y por qué importa?

Docker es una plataforma de contenedorización que permite empaquetar una aplicación junto con todas sus dependencias en una unidad portátil llamada contenedor. A diferencia de las máquinas virtuales, los contenedores comparten el kernel del sistema operativo host, lo que los hace mucho más ligeros y rápidos.

AspectoMáquina VirtualContenedor Docker
Tiempo de arranque1-2 minutosMilisegundos
Tamaño típico5-50 GB50-500 MB
AislamientoCompleto (OS propio)Proceso aislado
PortabilidadMediaAlta
Overhead de recursosAltoMuy bajo

La propuesta de valor de Docker es simple: si el contenedor funciona en tu máquina local, funcionará idéntico en el servidor de staging y en producción. Elimina la clase entera de bugs causados por diferencias de entorno.

2. Conceptos clave

  • Imagen: Plantilla inmutable de solo lectura. Define el sistema operativo base, las dependencias y el código de la aplicación. Es como un snapshot del sistema.
  • Contenedor: Instancia en ejecución de una imagen. Puedes tener múltiples contenedores del mismo tipo. Son efímeros por naturaleza.
  • Dockerfile: Archivo de texto con instrucciones para construir una imagen. Es el "código fuente" de tu imagen.
  • Docker Hub / Registry: Repositorio de imágenes. Docker Hub es el público por defecto. Puedes usar uno privado (GitHub Container Registry, AWS ECR, etc.).
  • Volumen: Mecanismo para persistir datos fuera del contenedor. Los datos en el sistema de archivos del contenedor se pierden cuando este se elimina.
  • Red Docker: Permite la comunicación entre contenedores de forma aislada del host.

3. Comandos esenciales de Docker CLI

Estos son los comandos que usarás a diario. Dominarlos al 100% te ahorrará mucho tiempo:

# ===== IMÁGENES ===== docker pull nginx:1.26-alpine # Descargar imagen docker images # Listar imágenes locales docker rmi nginx:1.26-alpine # Eliminar imagen docker build -t mi-app:1.0 . # Construir imagen desde Dockerfile docker build --no-cache -t mi-app:1.0 . # Construir sin caché # ===== CONTENEDORES ===== docker run -d --name web -p 8080:80 nginx # Ejecutar en background docker run --rm -it ubuntu bash # Interactivo, se elimina al salir docker ps # Contenedores en ejecución docker ps -a # Todos (incluidos detenidos) docker stop web # Detener contenedor docker start web # Iniciar contenedor detenido docker rm web # Eliminar contenedor (debe estar detenido) docker rm -f web # Forzar eliminación # ===== INSPECCIÓN Y DEBUG ===== docker logs web # Ver logs docker logs -f web # Logs en tiempo real (follow) docker exec -it web bash # Terminal dentro del contenedor docker inspect web # Información detallada en JSON docker stats # Uso de recursos en tiempo real # ===== LIMPIEZA ===== docker system prune # Eliminar recursos no usados docker system prune -af # Limpieza agresiva (imágenes incluidas) docker volume prune # Eliminar volúmenes huérfanos

4. Escribir Dockerfiles eficientes

Un Dockerfile mal escrito produce imágenes enormes, lentas de construir y con capas innecesarias. Aquí el patrón correcto para una aplicación Node.js, con todas las optimizaciones aplicadas:

# Dockerfile optimizado para Node.js # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 1. Usar siempre imagen base específica (nunca :latest) FROM node:22-alpine # 2. Crear usuario no-root (seguridad) RUN addgroup -g 1001 -S nodejs && \ adduser -S appuser -u 1001 -G nodejs # 3. Establecer directorio de trabajo WORKDIR /app # 4. Copiar SOLO package.json primero (aprovecha caché de capas) COPY package*.json ./ # 5. Instalar dependencias (esta capa se cachea si package.json no cambia) RUN npm ci --only=production && \ npm cache clean --force # 6. Copiar el código fuente COPY --chown=appuser:nodejs . . # 7. Cambiar al usuario no-root USER appuser # 8. Puerto que expone el contenedor (documentación) EXPOSE 3000 # 9. Healthcheck para orquestadores HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD wget -qO- http://localhost:3000/health || exit 1 # 10. Comando de inicio CMD ["node", "server.js"]

💡 Regla de oro del caché: Ordena las instrucciones del Dockerfile de "menos frecuente de cambiar" a "más frecuente de cambiar". Las instrucciones que copian el código fuente deben ir al final, porque cualquier cambio en el código invalida todas las capas posteriores.

Crea siempre un archivo .dockerignore junto al Dockerfile para excluir archivos innecesarios del contexto de build:

# .dockerignore node_modules npm-debug.log .git .gitignore .env .env.* *.md tests/ .DS_Store coverage/ dist/ *.log

5. Multi-stage builds para imágenes optimizadas

Los multi-stage builds son una de las características más poderosas de Docker moderno. Permiten usar múltiples imágenes base en un solo Dockerfile: una para compilar/construir y otra mucho más ligera para ejecutar. El resultado final solo contiene lo necesario para producción.

# Dockerfile multi-stage para una app Go # ── STAGE 1: Build ──────────────────────────────── FROM golang:1.23-alpine AS builder WORKDIR /build # Descargar dependencias primero (caché) COPY go.mod go.sum ./ RUN go mod download # Compilar COPY . . RUN CGO_ENABLED=0 GOOS=linux go build \ -ldflags="-w -s" \ -o /app/servidor ./cmd/servidor # ── STAGE 2: Runtime (imagen final minimalista) ─── FROM scratch # ¡imagen completamente vacía! # Copiar solo el binario compilado del stage anterior COPY --from=builder /app/servidor /servidor COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ EXPOSE 8080 ENTRYPOINT ["/servidor"] # Resultado: imagen de ~5 MB vs ~350 MB con golang:alpine

6. Docker Compose: orquestación local

Docker Compose permite definir y ejecutar aplicaciones multicontenedor con un solo archivo YAML. Es indispensable para el desarrollo local: define toda tu stack (app, base de datos, redis, nginx) en un archivo versionable.

# compose.yml (docker compose v2) services: app: build: context: . dockerfile: Dockerfile target: development # stage específico ports: - "3000:3000" environment: - NODE_ENV=development - DATABASE_URL=postgres://user:pass@db:5432/myapp - REDIS_URL=redis://cache:6379 volumes: - .:/app # hot-reload en desarrollo - /app/node_modules # excluir node_modules del bind mount depends_on: db: condition: service_healthy cache: condition: service_started restart: unless-stopped 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 -d myapp"] interval: 10s timeout: 5s retries: 5 cache: image: redis:7-alpine command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru volumes: postgres-data: # volumen nombrado (persistente)

Los comandos básicos de Docker Compose que necesitas:

docker compose up -d # Iniciar todo en background docker compose up --build # Reconstruir imágenes e iniciar docker compose down # Detener y eliminar contenedores docker compose down -v # Incluir volúmenes (BORRA DATOS) docker compose logs -f app # Logs de un servicio específico docker compose exec app bash # Terminal en un servicio docker compose ps # Estado de los servicios docker compose restart app # Reiniciar un servicio

7. Redes y volúmenes en Docker

Por defecto, Docker Compose crea una red para todos los servicios definidos en el mismo archivo. Los contenedores se comunican entre sí usando el nombre del servicio como hostname. Así, la app puede conectarse a la base de datos con el hostname db, no con una IP.

# Tipos de volúmenes # 1. Volumen nombrado (gestionado por Docker, recomendado para datos) volumes: - postgres-data:/var/lib/postgresql/data # 2. Bind mount (mapea directorio del host, útil en desarrollo) volumes: - ./src:/app/src # 3. tmpfs (en memoria, para datos temporales sensibles) tmpfs: - /run - /tmp # Comandos de volúmenes docker volume ls # Listar volúmenes docker volume inspect postgres-data # Inspeccionar volumen docker volume rm postgres-data # Eliminar volumen

8. Buenas prácticas para producción

El salto de desarrollo a producción con Docker requiere tener en cuenta estas prácticas:

  • Nunca uses :latest en producción. Siempre pin a una versión específica (node:22.11-alpine). Así los deployments son reproducibles.
  • Ejecuta como usuario no-root. Por defecto, los procesos en contenedores corren como root, lo que es un riesgo de seguridad. Crea un usuario dedicado en el Dockerfile.
  • Usa variables de entorno para secretos. Nunca hardcodees contraseñas o API keys en el Dockerfile o en el código. Usa .env localmente y gestores de secretos en producción.
  • Implementa healthchecks. Los orquestadores (Kubernetes, Docker Swarm, ECS) usan healthchecks para saber cuándo reiniciar un contenedor.
  • Limita recursos. Establece límites de CPU y memoria para evitar que un contenedor consuma todos los recursos del host.
  • Usa imágenes minimalistas. Alpine o Distroless tienen menos superficie de ataque y menos vulnerabilidades. Revisa regularmente con docker scout cves.
# Límites de recursos en compose.yml services: app: deploy: resources: limits: cpus: '0.5' # Máximo 50% de 1 CPU memory: 512M reservations: cpus: '0.1' memory: 128M security_opt: - no-new-privileges:true # Impide escalar privilegios read_only: true # Sistema de archivos de solo lectura tmpfs: - /tmp # Escrituras permitidas solo en /tmp

⚠️ Seguridad: Antes de subir una imagen a producción, analiza sus vulnerabilidades con docker scout cves mi-imagen:tag o trivy image mi-imagen:tag. Es habitual encontrar vulnerabilidades en imágenes desactualizadas, incluso en las oficiales.

Conclusión

Docker es hoy una habilidad fundamental para cualquier desarrollador que trabaje con aplicaciones de backend, servicios web o infraestructura. La curva de aprendizaje inicial es real, pero los beneficios en reproducibilidad, portabilidad y velocidad de deployment compensan ampliamente la inversión.

Los próximos pasos naturales son: aprender Docker Swarm para orquestación básica en producción, explorar Kubernetes para orquestación a gran escala, y familiarizarte con GitHub Actions o GitLab CI para construir y publicar imágenes automáticamente en cada push.