Docker: del desarrollo a producción

Guía completa de Docker para contenerizar aplicaciones. Dockerfile, Docker Compose, builds multi-stage y despliegue en producción con ejemplos prácticos.

Guía de Docker del desarrollo a producción

Docker revoluciona la forma en que se desarrollan, prueban y despliegan aplicaciones. Al encapsular una aplicación y sus dependencias en un contenedor portátil, Docker elimina el clásico problema de "funciona en mi máquina" y garantiza consistencia en todos los entornos. Esta guía cubre el camino completo desde el primer Dockerfile hasta el despliegue en producción.

Docker en 2026

Docker Desktop 5.x trae mejoras significativas de rendimiento, incluyendo soporte nativo de containerd, gestión optimizada de recursos e integración fluida con Kubernetes. Las imágenes multi-arquitectura (ARM/x86) son ahora práctica estándar.

Fundamentos de contenerización

Un contenedor es una unidad de software ligera que empaqueta código, runtime, bibliotecas del sistema y configuraciones. A diferencia de las máquinas virtuales que virtualizan el hardware, los contenedores comparten el kernel del sistema anfitrión, lo que los hace más rápidos de iniciar y menos intensivos en recursos.

bash
# terminal
# Docker installation on Ubuntu
sudo apt update
sudo apt install -y docker.io

# Add user to docker group (avoids sudo)
sudo usermod -aG docker $USER

# Verify installation
docker --version
# Docker version 26.1.0, build 1234567

# First container: downloads image and runs
docker run hello-world

Este comando descarga la imagen hello-world de Docker Hub y lanza un contenedor que muestra un mensaje de confirmación.

bash
# terminal
# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# List downloaded images
docker images

# Remove a container
docker rm <container_id>

# Remove an image
docker rmi <image_name>

Estos comandos básicos gestionan el ciclo de vida de contenedores e imágenes.

Crear el primer Dockerfile

Un Dockerfile contiene instrucciones para construir una imagen Docker. Cada instrucción crea una capa en la imagen final, permitiendo caché y reutilización.

dockerfile
# Dockerfile
# Base image: Node.js 22 on Alpine Linux (lightweight)
FROM node:22-alpine

# Set working directory in the container
WORKDIR /app

# Copy dependency files first (cache optimization)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Expose port (documentation)
EXPOSE 3000

# Startup command
CMD ["node", "server.js"]

El orden de las instrucciones es crucial para la optimización del caché. Los archivos que cambian raramente (package.json) deben copiarse antes del código fuente.

bash
# terminal
# Build image with a tag
docker build -t my-app:1.0 .

# Run the container
docker run -d -p 3000:3000 --name my-app-container my-app:1.0

# Check logs
docker logs my-app-container

# Access container shell
docker exec -it my-app-container sh

El flag -d ejecuta el contenedor en segundo plano, -p mapea el puerto 3000 del contenedor al puerto 3000 del host.

Alpine vs Debian

Las imágenes Alpine son significativamente más pequeñas (alrededor de 5 MB vs 120 MB para Debian). Sin embargo, utilizan musl libc en lugar de glibc, lo que puede causar incompatibilidades con algunas dependencias nativas. Cuando surjan problemas, se recomienda preferir imágenes basadas en Debian (node:22-slim).

Builds multi-stage para producción

Los builds multi-stage crean imágenes de producción optimizadas separando el entorno de construcción del entorno de ejecución. Solo los artefactos necesarios se incluyen en la imagen final.

dockerfile
# Dockerfile.production
# ============================================
# Stage 1: Build
# ============================================
FROM node:22-alpine AS builder

WORKDIR /app

# Copy and install dependencies (including devDependencies)
COPY package*.json ./
RUN npm ci

# Copy source code
COPY . .

# Build the application (TypeScript, bundling, etc.)
RUN npm run build

# ============================================
# Stage 2: Production
# ============================================
FROM node:22-alpine AS production

# Non-root user for security
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy only necessary files from builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# Switch to non-root user
USER nodejs

# Environment variables
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

# Startup command
CMD ["node", "dist/server.js"]

Este enfoque reduce significativamente el tamaño de la imagen final al excluir herramientas de compilación, devDependencies y archivos fuente.

bash
# terminal
# Build with specific file
docker build -f Dockerfile.production -t my-app:production .

# Compare image sizes
docker images | grep my-app
# my-app    production    abc123    150MB
# my-app    1.0           def456    450MB

La reducción de tamaño puede alcanzar un 60-70% dependiendo del proyecto, mejorando los tiempos de despliegue y reduciendo la superficie de ataque.

Docker Compose para orquestación local

Docker Compose simplifica la gestión de aplicaciones multi-contenedor. Un archivo YAML declara todos los servicios, sus configuraciones y dependencias.

yaml
# docker-compose.yml
version: "3.9"

services:
  # Main application
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    volumes:
      # Mount source code for hot-reload
      - ./src:/app/src
      - ./package.json:/app/package.json
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - app-network

  # PostgreSQL database
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      # Data persistence
      - postgres_data:/var/lib/postgresql/data
      # Initialization script
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # Redis cache
  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network

# Named volumes for persistence
volumes:
  postgres_data:
  redis_data:

# Dedicated network for isolation
networks:
  app-network:
    driver: bridge

Los servicios se comunican usando sus nombres (db, cache) a través de la red interna de Docker. Los healthchecks aseguran que las dependencias estén listas antes de que la aplicación inicie.

bash
# terminal
# Start all services
docker compose up -d

# View logs from all services
docker compose logs -f

# Logs from a specific service
docker compose logs -f app

# Stop and remove containers
docker compose down

# Removal including volumes (caution: data loss)
docker compose down -v

# Rebuild after Dockerfile changes
docker compose up -d --build

¿Listo para aprobar tus entrevistas de DevOps?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Gestión de secretos y variables de entorno

La gestión segura de secretos es crucial en producción. Docker ofrece varios enfoques según el contexto.

yaml
# docker-compose.override.yml (development only)
version: "3.9"

services:
  app:
    env_file:
      - .env.development
    environment:
      - DEBUG=true

Para producción, Docker secrets ofrece mayor seguridad.

yaml
# docker-compose.production.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.production
    secrets:
      - db_password
      - api_key
    environment:
      - NODE_ENV=production
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
      - API_KEY_FILE=/run/secrets/api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

El código de la aplicación lee los secretos desde archivos montados.

config/secrets.jsjavascript
const fs = require('fs');
const path = require('path');

// Utility function to read Docker secrets
function readSecret(secretName) {
  const secretPath = `/run/secrets/${secretName}`;

  // Check if secret file exists
  if (fs.existsSync(secretPath)) {
    return fs.readFileSync(secretPath, 'utf8').trim();
  }

  // Fallback to classic environment variables
  const envVar = secretName.toUpperCase();
  return process.env[envVar];
}

module.exports = {
  databasePassword: readSecret('db_password'),
  apiKey: readSecret('api_key'),
};

Este enfoque evita exponer secretos en variables de entorno o imágenes Docker.

Optimización de imágenes Docker

Varias técnicas reducen el tamaño de la imagen y mejoran el rendimiento.

dockerfile
# Dockerfile.optimized
FROM node:22-alpine AS base

# Install necessary tools in a single layer
RUN apk add --no-cache \
    dumb-init \
    && rm -rf /var/cache/apk/*

# ============================================
# Stage: Dependencies
# ============================================
FROM base AS deps

WORKDIR /app

# Copy only lock files for caching
COPY package.json package-lock.json ./

# Install with mounted npm cache (BuildKit)
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# ============================================
# Stage: Builder
# ============================================
FROM base AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build

# ============================================
# Stage: Production
# ============================================
FROM base AS production

# Image metadata
LABEL maintainer="team@example.com"
LABEL version="1.0"
LABEL description="Production-ready Node.js application"

# Non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy production dependencies
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules

# Copy build output
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

ENV NODE_ENV=production

# dumb-init as PID 1 for signal handling
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

El uso de dumb-init asegura el manejo correcto de señales Unix, permitiendo el apagado graceful del contenedor.

bash
# terminal
# Enable BuildKit for advanced features
export DOCKER_BUILDKIT=1

# Build with cache and detailed output
docker build --progress=plain -t my-app:optimized .

# Analyze image layers
docker history my-app:optimized

# Detailed image inspection
docker inspect my-app:optimized
Seguridad de imágenes

Es fundamental escanear regularmente las imágenes en busca de vulnerabilidades usando herramientas como Trivy o Snyk. Las imágenes base deben actualizarse periódicamente para incluir parches de seguridad.

Redes avanzadas en Docker

Docker ofrece varios controladores de red para diferentes casos de uso.

yaml
# docker-compose.networking.yml
version: "3.9"

services:
  # Publicly accessible frontend
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    networks:
      - frontend-network
      - backend-network

  # API accessible only from frontend
  api:
    build: ./api
    networks:
      - backend-network
      - database-network
    # No external ports exposed

  # Isolated database
  database:
    image: postgres:16-alpine
    networks:
      - database-network
    # Accessible only by API

networks:
  frontend-network:
    driver: bridge
  backend-network:
    driver: bridge
    internal: true  # No internet access
  database-network:
    driver: bridge
    internal: true

Esta configuración aísla los servicios siguiendo el principio de mínimo privilegio. La base de datos solo es accesible desde la API.

bash
# terminal
# Inspect Docker networks
docker network ls

# Details of a specific network
docker network inspect app-network

# Create a custom network
docker network create --driver bridge --subnet 172.28.0.0/16 custom-network

# Connect a container to an existing network
docker network connect custom-network my-container

Volúmenes y persistencia de datos

Los volúmenes Docker preservan datos más allá del ciclo de vida del contenedor.

yaml
# docker-compose.volumes.yml
version: "3.9"

services:
  app:
    image: my-app:latest
    volumes:
      # Named volume for persistent data
      - app_data:/app/data
      # Bind mount for development
      - ./uploads:/app/uploads:rw
      # Read-only mount for configuration
      - ./config:/app/config:ro

  backup:
    image: alpine
    volumes:
      # Access same volume for backups
      - app_data:/data:ro
      - ./backups:/backups
    command: |
      sh -c "tar czf /backups/backup-$$(date +%Y%m%d).tar.gz /data"

volumes:
  app_data:
    driver: local
    driver_opts:
      type: none
      device: /path/to/host/data
      o: bind

La distinción entre volúmenes nombrados y bind mounts es importante: los volúmenes son gestionados por Docker mientras que los bind mounts usan directamente el sistema de archivos del host.

bash
# terminal
# List volumes
docker volume ls

# Inspect a volume
docker volume inspect app_data

# Remove orphaned volumes
docker volume prune

# Backup a volume
docker run --rm -v app_data:/data -v $(pwd):/backup alpine \
  tar czf /backup/volume-backup.tar.gz /data

Despliegue en producción

Un flujo de despliegue robusto incluye construcción, pruebas y push a un registro.

bash
# deploy.sh
#!/bin/bash
set -e

# Variables
REGISTRY="registry.example.com"
IMAGE_NAME="my-app"
VERSION=$(git describe --tags --always)

echo "Building version: $VERSION"

# Build production image
docker build \
  -f Dockerfile.production \
  -t $REGISTRY/$IMAGE_NAME:$VERSION \
  -t $REGISTRY/$IMAGE_NAME:latest \
  --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
  --build-arg VERSION=$VERSION \
  .

# Security scan
echo "Running security scan..."
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image $REGISTRY/$IMAGE_NAME:$VERSION

# Push to registry
echo "Pushing to registry..."
docker push $REGISTRY/$IMAGE_NAME:$VERSION
docker push $REGISTRY/$IMAGE_NAME:latest

echo "Deployment complete: $REGISTRY/$IMAGE_NAME:$VERSION"

Para despliegues en servidor, un archivo compose de producción separado adapta la configuración.

yaml
# docker-compose.prod.yml
version: "3.9"

services:
  app:
    image: registry.example.com/my-app:latest
    restart: always
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Esta configuración define la asignación de recursos, la estrategia de actualización y los healthchecks para un despliegue confiable.

bash
# terminal
# Production deployment with Docker Compose
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Zero-downtime update (rolling update)
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

# Rollback if issues occur
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps \
  --scale app=0 && \
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

Monitoreo y depuración de contenedores

El monitoreo de contenedores es esencial en producción.

bash
# terminal
# Real-time statistics for all containers
docker stats

# Statistics for a specific container with custom format
docker stats my-app --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

# Inspect processes in a container
docker top my-app

# Real-time Docker events
docker events --filter container=my-app

# Copy files from/to a container
docker cp my-app:/app/logs/error.log ./error.log

Para depuración avanzada, varias técnicas están disponibles.

bash
# terminal
# Interactive shell in a running container
docker exec -it my-app sh

# Execute a single command
docker exec my-app cat /app/config/settings.json

# Start a container in debug mode
docker run -it --rm --entrypoint sh my-app:latest

# Inspect environment variables
docker exec my-app printenv

# Analyze logs with filters
docker logs my-app --since 1h --tail 100 | grep ERROR
yaml
# docker-compose.monitoring.yml
version: "3.9"

services:
  app:
    # ... existing configuration
    labels:
      - "prometheus.scrape=true"
      - "prometheus.port=3000"
      - "prometheus.path=/metrics"

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=secret

volumes:
  prometheus_data:
  grafana_data:

Este stack de monitoreo permite recopilar y visualizar métricas de contenedores.

Conclusión

Docker transforma el ciclo de desarrollo al garantizar consistencia en todos los entornos. La contenerización aporta portabilidad, aislamiento y reproducibilidad—cualidades esenciales para aplicaciones modernas.

Checklist de Docker para producción

  • ✅ Builds multi-stage para imágenes optimizadas
  • ✅ Usuario no-root en contenedores
  • ✅ Healthchecks configurados para todos los servicios
  • ✅ Secretos gestionados mediante Docker secrets o variables de entorno seguras
  • ✅ Límites de recursos (CPU, memoria) definidos
  • ✅ Volúmenes para persistencia de datos críticos
  • ✅ Logging centralizado con rotación de archivos
  • ✅ Escaneo de seguridad de imágenes antes del despliegue
  • ✅ Estrategia de actualización sin tiempo de inactividad
  • ✅ Red aislada entre servicios

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Dominar Docker es una habilidad fundamental para todo desarrollador moderno. Desde el entorno local hasta el despliegue en producción, Docker estandariza flujos de trabajo y simplifica operaciones. Los conceptos presentados aquí forman una base sólida para explorar Kubernetes y la orquestación de contenedores a gran escala.

Etiquetas

#docker
#containerization
#devops
#docker compose
#deployment

Compartir

Artículos relacionados