# 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. - Published: 2026-01-21 - Updated: 2026-04-10 - Author: SharpSkill - Tags: docker, containerization, devops, docker compose, deployment - Reading time: 18 min --- 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 # Remove an image docker rmi ``` 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 ``` ## 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. ```javascript // config/secrets.js 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 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/devops/docker-from-development-to-production