16

Docker en desarrollo, tu entorno de trabajo

Vistas: 0
Tutorial: Tutorial de Docker
Docker en desarrollo, tu entorno de trabajo

Como te comenté en el capítulo anterior sobre Buildx multi-arquitectura, has aprendido a construir imágenes que funcionan en cualquier arquitectura. Pero, ¿y si te dijera que Docker no es solo para producción? ¿Qué pasa si lo usas para tu día a día como desarrollador?

Este capítulo te sumerge en el uso de Docker como herramienta de desarrollo diario, no solo de despliegue. Aprenderás a configurar entornos de desarrollo reproducibles, con recarga en caliente (hot reload), depuración (debugging), perfiles separados para desarrollo y producción, y bases de datos efímeras. El objetivo es transformar tu flujo de trabajo para que Docker no sea solo una herramienta de producción, sino tu entorno de desarrollo principal, portátil y compartible.

Voy a cubrir desde bind mounts hasta devcontainers completos, pasando por docker compose watch, perfiles Compose, gestión de entornos con .env y debugging dentro del contenedor. Todo con ejemplos que puedes probar ahora mismo.

El problema del desarrollo tradicional

Antes de Docker, el desarrollo de software solía seguir este ritual:

  1. Clonar el repositorio
  2. Leer un README de 30 páginas con instrucciones de instalación
  3. Instalar Python 3.10, Node 18, PostgreSQL 15, Redis… en tu máquina
  4. Descubrir que tu compañera tiene Python 3.11 y algo no funciona
  5. Pasar horas debugando «en mi máquina funciona»
  6. El onboarding de un nuevo desarrollador lleva días

Docker resuelve esto ofreciendo entornos idénticos para cada desarrollador. Pero si lo usas solo para producción, te pierdes su mayor ventaja: el desarrollo dentro de contenedores.

El flujo ideal con Docker

1. git clone repo
2. docker compose up
3. Código listo en http://localhost:8000
4. Modificas un archivo → hot reload automático
5. docker compose down → todo limpio

Ese es el objetivo de este capítulo.

Bind mounts: el corazón del desarrollo con Docker

Un bind mount monta un directorio del host dentro del contenedor. A diferencia de un volumen, que Docker gestiona, el bind mount apunta directamente a una ruta de tu sistema de archivos. Esto significa que cualquier cambio que hagas en tu código se refleja instantáneamente dentro del contenedor.

# docker-compose.yml (fragmento)
services:
  app:
    build: .
    volumes:
      - ./src:/app/src        # bind mount del código
      - /app/node_modules     # volumen anónimo para no sobrescribir
    environment:
      - NODE_ENV=development

Cuidado con node_modules

Si montas ./src:/app/src y tu package.json instala dependencias en /app/node_modules, al montar todo el directorio ./ podrías sobrescribir node_modules con el directorio vacío del host. Esto te puede ahorrar más de un disgusto si lo tienes en cuenta desde el principio. La solución es añadir un volumen anónimo:

volumes:
  - ./src:/app/src
  - /app/node_modules    # ← ancla node_modules dentro del contenedor

O usa un volumen nombrado:

volumes:
  - ./src:/app/src
  - node_modules_vol:/app/node_modules

volumes:
  node_modules_vol:

Bind mounts en Dockerfile vs Compose

No confundas COPY en Dockerfile (que va dentro de la imagen) con los bind mounts en Compose (que se montan en tiempo de ejecución). En desarrollo, no uses COPY para tu código. La imagen debe contener solo las dependencias y configuración base; el código se monta con bind mount.

# Dockerfile.dev — solo dependencias, sin COPY del código
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# El código se monta con docker-compose.override.yml
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]

Fíjate aquí que no hay COPY . /app. Eso es porque el código se monta desde fuera. Cuando hagas docker compose up, el bind mount se encarga de que el código esté disponible. Si usases COPY en desarrollo, tendrías que reconstruir la imagen cada vez que toques un archivo. Un coñazo, vamos.

Hot reload: recarga en caliente

El hot reload permite que el servidor se reinicie automáticamente cuando detecta cambios en los archivos. Cada lenguaje o framework tiene su herramienta. Te muestro las más comunes.

Python (FastAPI + Uvicorn)

# Dockerfile.dev
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# --reload activa hot reload
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - ./app:/app
    ports:
      - "8000:8000"

Cada cambio en ./app reinicia Uvicorn automáticamente. Esto no es más que un watcher de archivos que detecta modificaciones y reinicia el proceso.

Node.js (Nodemon)

FROM node:20-alpine
WORKDIR /app
COPY package*.json .
RUN npm install
# nodemon watch en desarrollo
CMD ["npx", "nodemon", "src/index.js"]
volumes:
  - ./src:/app/src
  - /app/node_modules

Go (Air)

Air es la herramienta de hot reload para Go. Observa los archivos .go y recompila y ejecuta al detectar cambios.

FROM golang:1.23-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
# Instalar Air
RUN go install github.com/air-verse/air@latest
CMD ["air", "-c", ".air.toml"]
volumes:
  - .:/app

Necesitas un archivo .air.toml en la raíz del proyecto para configurar qué observar y cómo compilar.

PHP (Symfony)

FROM php:8.3-cli
WORKDIR /app
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY composer.* ./
RUN composer install
CMD ["symfony", "server:start", "--no-tls", "--port=8000"]

Y añades symfony-cli:

RUN curl -1sLf 'https://dl.cloudsmith.io/public/symfony/stable/setup.sh' | bash
RUN apt-get install -y symfony-cli

docker compose watch: la evolución del hot reload

Docker Compose Watch (introducido en Compose v2.22+) es una mejora sobre los bind mounts simples. Permite acciones sincronizadas basadas en cambios de archivos:

  • sync: copia archivos modificados al contenedor (similar a bind mount pero más eficiente)
  • rebuild: reconstruye la imagen cuando cambia el Dockerfile o ciertos archivos
  • sync+restart: sincroniza y reinicia el servicio
# docker-compose.yml
services:
  web:
    build: .
    ports:
      - "3000:3000"
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          ignore:
            - node_modules/
            - *.test.ts
        - action: rebuild
          path: package.json
        - action: sync+restart
          path: ./config
          target: /app/config

Para activarlo, ejecuta:

docker compose watch

Esto mantiene el comando en primer plano, observando los archivos y ejecutando las acciones definidas. Es más declarativo y predecible que un bind mount simple.

Comparativa: bind mount vs watch

CaracterísticaBind mountdocker compose watch
SincronizaciónInmediata (kernel)Policy-based
Rebuild automático❌ No✅ Sí
Ignorar patronesManual (dockerignore)✅ ignore explícito
Reinicio de servicio❌ No✅ sync+restart
Eficiencia en I/OAltaMedia (copia en lugar de montar)

¿Cuándo usar cada uno? Si trabajas con un lenguaje interpretado como Python o PHP, el bind mount te vale. Si necesitas recompilar (Go, Rust, o cuando cambias dependencias), docker compose watch con rebuild te ahorrará teclear comandos manualmente.

docker-compose.override.yml: separa desarrollo de producción

El archivo docker-compose.override.yml se aplica automáticamente sobre docker-compose.yml cuando ejecutas docker compose up. Te permite tener una configuración base para producción y sobrescribir partes para desarrollo.

# docker-compose.yml (base, compartido)
services:
  app:
    build: .
    ports:
      - "80:80"
    environment:
      - NODE_ENV=production
      - DB_HOST=db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
# docker-compose.override.yml (solo desarrollo, no se sube a producción)
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - ./src:/app/src
      - /app/node_modules
    ports:
      - "3000:3000"      # Puerto de desarrollo
      - "9229:9229"      # Puerto de debugging
    environment:
      - NODE_ENV=development
      - DEBUG=true
      - LOG_LEVEL=debug
  db:
    ports:
      - "5432:5432"      # Exponer DB para herramientas locales
    volumes:
      - pgdata_dev:/var/lib/postgresql/data

volumes:
  pgdata_dev:

Cuando ejecutas docker compose up, Docker fusiona ambos archivos. En producción, simplemente no incluyes el override:

# Producción
docker compose -f docker-compose.yml up -d

# Desarrollo (usa override automáticamente)
docker compose up -d

# Desarrollo con override explícito
docker compose -f docker-compose.yml -f docker-compose.override.yml up -d

Herencia múltiple

Puedes tener varios overrides para diferentes escenarios:

# Desarrollo con perfil de testing
docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.test.yml up -d

Esto no es más que un sistema de herencia: cada archivo añade o sobrescribe propiedades del anterior. Muy útil para tener configuraciones modulares.

Perfiles con Compose Profiles

Los perfiles de Compose (compose profiles) permiten activar o desactivar grupos de servicios con una bandera --profile.

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "8000:8000"

  db:
    image: postgres:16-alpine
    profiles: ["dev", "prod"]    # Siempre activo en dev y prod

  redis:
    image: redis:7-alpine
    profiles: ["dev", "prod"]

  mailpit:
    image: axllent/mailpit
    ports:
      - "8025:8025"
    profiles: ["dev"]            # Solo en desarrollo

  adminer:
    image: adminer
    ports:
      - "8080:8080"
    profiles: ["dev"]            # Solo en desarrollo

  prometheus:
    image: prom/prometheus
    profiles: ["monitoring"]     # Solo cuando se activa monitoring

  grafana:
    image: grafana/grafana
    profiles: ["monitoring"]
# Desarrollo normal
docker compose --profile dev up

# Desarrollo + monitoreo
docker compose --profile dev --profile monitoring up

# Producción
docker compose --profile prod up

# Solo monitoreo (sin app)
docker compose --profile monitoring up

Perfiles con dependencias

Los perfiles también funcionan con depends_on:

services:
  tests:
    build: .
    profiles: ["ci"]
    depends_on:
      db:
        condition: service_healthy
# En CI/CD
docker compose --profile ci up --abort-on-container-exit tests

Esto te permite tener un perfil específico para CI que levanta solo los servicios necesarios para los tests, sin tener que modificar la configuración principal.

Gestión de entornos con .env, .env.dev y .env.prod

Docker Compose carga variables de entorno desde un archivo .env por defecto. Para múltiples entornos, usa el flag --env-file:

# .env.dev
APP_ENV=development
DEBUG=true
DB_NAME=myapp_dev
DB_USER=dev_user
DB_PASS=dev_pass
LOG_LEVEL=debug
API_PORT=8000
# .env.prod
APP_ENV=production
DEBUG=false
DB_NAME=myapp_prod
DB_USER=prod_user
DB_PASS=${DB_PROD_PASS}
LOG_LEVEL=warning
API_PORT=80
# docker-compose.yml
services:
  app:
    build: .
    env_file:
      - .env
      - .env.${APP_ENV:-dev}   # Carga específica según APP_ENV
    environment:
      - APP_ENV=${APP_ENV:-dev}
# Desarrollo
docker compose --env-file .env.dev up

# Producción
docker compose --env-file .env.prod up

Buenas prácticas con .env

  1. Nunca subas .env.prod al repositorio — contiene secretos reales
  2. Sube .env.dev y .env.example para que otros desarrolladores sepan qué variables se necesitan
  3. Usa ${VAR:-default} para valores por defecto
  4. Separa secretos en .env.secrets y cárgalos desde ahí (y añádelo a .gitignore)

Fíjate en el tercer punto: ${APP_ENV:-dev} significa «usa la variable APP_ENV, y si no existe, usa ‘dev’ como valor por defecto». Esto hace que el mismo docker-compose.yml funcione en cualquier entorno sin cambios.

Dev Containers (devcontainer.json)

Los Dev Containers son una especificación de VS Code (y ahora compatible con JetBrains) que permite definir un entorno de desarrollo completo dentro de un contenedor Docker. El archivo devcontainer.json describe la imagen, extensiones, configuraciones y puertos.

// .devcontainer/devcontainer.json
{
  "name": "Mi App Python",
  "build": {
    "dockerfile": "Dockerfile",
    "context": ".."
  },
  "forwardPorts": [8000, 5432],
  "portsAttributes": {
    "8000": {
      "label": "API",
      "onAutoForward": "notify"
    },
    "5432": {
      "label": "PostgreSQL"
    }
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.vscode-pylance",
        "charliermarsh.ruff",
        "tamasfe.even-better-toml"
      ],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python",
        "python.formatting.provider": "ruff"
      }
    }
  },
  "postCreateCommand": "pip install -r requirements.txt && pre-commit install",
  "remoteUser": "vscode",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {},
    "ghcr.io/devcontainers/features/git:1": {}
  },
  "mounts": [
    "source=${localWorkspaceFolderBasename}-vscode-extensions,target=/home/vscode/.vscode-server/extensions,type=volume"
  ]
}
# .devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/python:3.12

# Instalar herramientas adicionales
RUN apt-get update && apt-get install -y \
    postgresql-client \
    redis-tools \
    && rm -rf /var/lib/apt/lists/*

# Instalar herramientas Python globales
RUN pip install --no-cache-dir \
    poetry \
    pre-commit \
    ruff

Ventajas de Dev Containers

  • Entorno idéntico para todo el equipo
  • Onboarding inmediato: abres el repo, VS Code detecta devcontainer.json, ofrece «Reopen in Container»
  • Aislamiento total: cada proyecto tiene sus propias dependencias, versiones de lenguajes, extensiones
  • Docker-in-Docker: puedes ejecutar Docker dentro del Dev Container para builds y pruebas

Dev Containers + Docker Compose

// .devcontainer/devcontainer.json
{
  "name": "App con servicios",
  "dockerComposeFile": ["../docker-compose.yml", "../docker-compose.override.yml"],
  "service": "app",
  "workspaceFolder": "/workspace",
  "forwardPorts": [8000, 5432, 6379],
  "shutdownAction": "stopCompose"
}

Con esta configuración, al abrir el Dev Container se levanta todo el stack (app + db + redis), y al cerrarlo se detiene todo. Esto no es más que la punta del iceberg de lo que puedes hacer con Dev Containers.

Depuración (debugging) en contenedores

Depurar código dentro de un contenedor es más sencillo de lo que parece. Cada lenguaje tiene su adaptador.

Python (debugpy)

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt debugpy
CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "--wait-for-client", "main.py"]
# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "5678:5678"    # Puerto de debugpy
    volumes:
      - ./app:/app
    environment:
      - DEBUG=true

En VS Code, configura el lanzador:

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to Container",
      "type": "debugpy",
      "request": "attach",
      "connect": {
        "host": "localhost",
        "port": 5678
      },
      "pathMappings": [
        {
          "localRoot": "${workspaceFolder}/app",
          "remoteRoot": "/app"
        }
      ]
    }
  ]
}

Node.js (–inspect)

FROM node:20-alpine
WORKDIR /app
COPY package*.json .
RUN npm install
CMD ["node", "--inspect=0.0.0.0:9229", "src/index.js"]
ports:
  - "9229:9229"
// .vscode/launch.json
{
  "name": "Attach Node",
  "type": "node",
  "request": "attach",
  "address": "localhost",
  "port": 9229,
  "localRoot": "${workspaceFolder}/src",
  "remoteRoot": "/app/src"
}

Go (Delve)

FROM golang:1.23-alpine
RUN go install github.com/go-delve/delve/cmd/dlv@latest
WORKDIR /app
COPY . .
CMD ["dlv", "debug", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient"]
ports:
  - "2345:2345"
security_opt:
  - seccomp:unconfined   # Necesario para Delve
cap_add:
  - SYS_PTRACE           # Necesario para Delve
// .vscode/launch.json
{
  "name": "Attach Go (Delve)",
  "type": "go",
  "request": "attach",
  "mode": "remote",
  "remotePath": "/app",
  "port": 2345,
  "host": "127.0.0.1"
}

PHP (Xdebug)

FROM php:8.3-cli
RUN pecl install xdebug && docker-php-ext-enable xdebug
COPY xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini
; xdebug.ini
xdebug.mode=debug
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.idekey=VSCODE
xdebug.start_with_request=yes
ports:
  - "9003:9003"

Bases de datos efímeras para desarrollo

Una base de datos efímera es un contenedor de base de datos que se crea, se usa y se destruye en cada sesión de desarrollo. Esto te garantiza un estado limpio y reproducible.

# docker-compose.yml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp_dev
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
    # Sin volúmenes persistentes → efímera
    tmpfs: /var/lib/postgresql/data  # Opcional: usa RAM para datos
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev -d myapp_dev"]
      interval: 5s
      timeout: 5s
      retries: 5

  db-test:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp_test
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    tmpfs: /var/lib/postgresql/data
    profiles: ["test"]
# Desarrollo con datos efímeros
docker compose up -d db    # Sin volumen → datos desaparecen al hacer down

# Con datos persistentes (override)
docker compose -f docker-compose.yml -f docker-compose.dev-persist.yml up -d
# docker-compose.dev-persist.yml
services:
  db:
    volumes:
      - pgdata_dev:/var/lib/postgresql/data

volumes:
  pgdata_dev:

Estrategias con bases de datos efímeras

  1. Solo tmpfs: datos en RAM, ultra rápidos, desaparecen al parar el contenedor
  2. Volumen anónimo: persisten mientras no hagas docker compose down -v
  3. Script de seed: ejecuta docker compose exec db psql -f /scripts/seed.sql tras levantar
  4. init scripts: monta ./init.sql en /docker-entrypoint-initdb.d/ para poblar al arrancar
services:
  db:
    image: postgres:16-alpine
    volumes:
      - ./init:/docker-entrypoint-initdb.d  # Scripts de inicialización
      - pgdata_dev:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp_dev
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev

Patrón development containers completo

Este es el patrón completo para un proyecto Python/FastAPI con PostgreSQL, Redis, hot reload y debugging:

proyecto/
├── .devcontainer/
│   ├── devcontainer.json
│   └── Dockerfile
├── .vscode/
│   └── launch.json
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── models.py
│   └── routes.py
├── tests/
│   └── test_api.py
├── docker/
│   ├── Dockerfile          # Producción (multi-stage)
│   └── Dockerfile.dev      # Desarrollo (hot reload + debug)
├── docker-compose.yml      # Base
├── docker-compose.override.yml  # Desarrollo
├── docker-compose.prod.yml      # Producción
├── .env.dev
├── .env.prod
├── .gitignore
├── requirements.txt
└── README.md
# docker/Dockerfile.dev
FROM python:3.12-slim

WORKDIR /app

# Dependencias del sistema
RUN apt-get update && apt-get install -y \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Dependencias Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt debugpy

# Puerto de la app y debugging
EXPOSE 8000 5678

CMD ["uvicorn", "app.main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]
# docker/Dockerfile
# Producción — multi-stage
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY app/ ./app/
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: docker/Dockerfile
    ports:
      - "8000:8000"
    environment:
      - APP_ENV=${APP_ENV:-production}
      - DB_HOST=db
      - DB_NAME=${DB_NAME:-myapp}
      - DB_USER=${DB_USER:-app}
      - DB_PASS=${DB_PASS:-secret}
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${DB_NAME:-myapp}
      POSTGRES_USER: ${DB_USER:-app}
      POSTGRES_PASSWORD: ${DB_PASS:-secret}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-app}"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3
# docker-compose.override.yml
services:
  api:
    build:
      dockerfile: docker/Dockerfile.dev
    volumes:
      - ./app:/app
    ports:
      - "5678:5678"
    environment:
      - APP_ENV=development
      - DEBUG=true
      - LOG_LEVEL=debug

  db:
    ports:
      - "5432:5432"
    volumes:
      - pgdata_dev:/var/lib/postgresql/data

  redis:
    ports:
      - "6379:6379"

volumes:
  pgdata_dev:

Script de validación del entorno

Aquí tienes una función Python que puedes usar para verificar que tu entorno de desarrollo Docker está bien configurado:

import subprocess
import sys

def validar_entorno_desarrollo():
    """
    Verifica que el entorno de desarrollo con Docker esté correctamente
    configurado. Comprueba Docker, Compose, Dev Container y puertos.
    """
    checks = []

    # 1. Docker instalado
    try:
        r = subprocess.run(
            ["docker", "--version"],
            capture_output=True, text=True, timeout=10
        )
        if r.returncode == 0:
            checks.append(("✅ Docker instalado", r.stdout.strip()))
        else:
            checks.append(("❌ Docker no responde", r.stderr))
    except FileNotFoundError:
        checks.append(("❌ Docker no encontrado",
                       "Instala Docker Desktop o Docker Engine"))
    except subprocess.TimeoutExpired:
        checks.append(("❌ Docker timeout", "El daemon no responde"))

    # 2. Docker Compose
    try:
        r = subprocess.run(
            ["docker", "compose", "version"],
            capture_output=True, text=True, timeout=10
        )
        if r.returncode == 0:
            checks.append(("✅ Docker Compose instalado", r.stdout.strip()))
        else:
            checks.append(("❌ Docker Compose no disponible", r.stderr))
    except FileNotFoundError:
        checks.append(("❌ Docker Compose no encontrado", ""))
    except subprocess.TimeoutExpired:
        checks.append(("❌ Docker Compose timeout", ""))

    # 3. Archivos de configuración
    archivos = [
        "docker-compose.yml",
        "docker-compose.override.yml",
        ".env.dev",
        ".devcontainer/devcontainer.json"
    ]
    import os
    for archivo in archivos:
        if os.path.exists(archivo):
            checks.append((f"✅ {archivo} presente", ""))
        else:
            checks.append((f"⚠️  {archivo} no encontrado",
                          "Puedes generarlo con crear_docker_compose_dev()"))

    # 4. Puertos ocupados
    puertos = [8000, 5432, 6379, 5678, 9229]
    for puerto in puertos:
        r = subprocess.run(
            ["ss", "-tlnp", f"sport = :{puerto}"],
            capture_output=True, text=True, timeout=5
        )
        if r.stdout.strip():
            checks.append((
                f"⚠️  Puerto {puerto} ocupado",
                "Otro proceso está usando este puerto"
            ))

    # Mostrar resultados
    print("\n=== Validación del Entorno de Desarrollo Docker ===\n")
    errores = 0
    for estado, detalle in checks:
        print(f"  {estado}")
        if detalle:
            print(f"    {detalle}")
        if estado.startswith("❌"):
            errores += 1

    print(f"\n{len(checks) - errores}/{len(checks)} checks pasados")
    if errores > 0:
        print(f"⚠️  {errores} problemas encontrados")
        return False
    return True

Errores comunes al trabajar con Docker en desarrollo

#ErrorConsecuenciaSolución
1Usar COPY en lugar de bind mountCada cambio requiere rebuild completo de la imagenUsa volumes: - ./src:/app/src en docker-compose.override.yml
2Olvidar node_modules en bind mountnode_modules del contenedor se sobrescribe con el directorio vacío del hostAñade /app/node_modules como volumen anónimo
3No separar Dockerfile.dev de DockerfileLa imagen de desarrollo es pesada y lenta (incluye debuggers en producción)Crea docker/Dockerfile.dev separado con hot reload y debug
4Exponer puertos de debug en producciónEl contenedor en producción expone 5678 o 9229, un riesgo de seguridadUsa docker-compose.override.yml solo para desarrollo (no desplegar)
5No usar .env-file para entornosVariables hardcodeadas en docker-compose.yml, difícil cambiar entre entornosUsa --env-file .env.dev y variables con ${VAR:-default}
6Contenedores con datos persistentes en devEstado sucio entre sesiones, tests que fallan por datos residualesUsa tmpfs o volúmenes anónimos para bases de datos efímeras
7Ignorar docker compose watchBind mounts funcionan pero no reconstruyen cuando cambian dependenciasUsa develop.watch con acciones sync, rebuild y sync+restart
8No configurar healthchecksdepends_on espera solo a que el contenedor arranque, no a que el servicio respondaAñade condition: service_healthy con healthcheck
9Desarrollar como root dentro del contenedorArchivos creados pertenecen a root, problemas de permisos en el hostUsa remoteUser en devcontainer.json y USER en Dockerfile
10No compartir devcontainer.jsonCada desarrollador configura su IDE manualmente, entornos inconsistentesSube .devcontainer/devcontainer.json al repositorio

Verificación

Aquí tienes varios comandos para verificar que todo lo que has configurado en este capítulo funciona correctamente. Ejecútalos en tu terminal y comprueba que obtienes resultados similares.

1. Verificar que Docker y Docker Compose están instalados y tienen las versiones correctas

docker --version
docker compose version

Deberías ver algo como:

Docker version 27.0.0, build abc123
Docker Compose version v2.28.0

Si Compose no muestra una versión v2.22+, la función docker compose watch no estará disponible.

2. Probar un bind mount con hot reload

Crea un proyecto de prueba y verifica que los cambios se reflejan sin reconstruir:

mkdir -p test-bindmount/app
cd test-bindmount

Crea un docker-compose.yml:

services:
  test:
    image: python:3.12-slim
    working_dir: /app
    volumes:
      - ./app:/app
    command: python -m http.server 8000
    ports:
      - "8000:8000"
docker compose up -d
curl http://localhost:8000
# Deberías ver el listado del directorio /app
echo "<h1>Hola Docker Dev</h1>" > app/index.html
curl http://localhost:8000/index.html
# Deberías ver "Hola Docker Dev" — el cambio se refleja instantáneamente
docker compose down

3. Verificar que docker compose watch funciona

cd ..
mkdir -p test-watch/app
cd test-watch

Crea un docker-compose.yml:

services:
  web:
    image: alpine:latest
    working_dir: /data
    volumes:
      - ./app:/data
    command: sh -c "while true; do sleep 1; done"
    develop:
      watch:
        - action: sync
          path: ./app
          target: /data
docker compose watch &
sleep 3
echo "test" > app/test.txt
docker compose exec web cat /data/test.txt
# Debería mostrar "test" — la sincronización funciona
docker compose down

4. Verificar que los perfiles Compose funcionan

cd ..
mkdir -p test-profiles
cd test-profiles

Crea un docker-compose.yml:

services:
  app:
    image: alpine:latest
    command: sh -c "echo 'App iniciada' && sleep 10"

  redis:
    image: redis:7-alpine
    profiles: ["dev"]

  adminer:
    image: adminer
    profiles: ["dev"]
    ports:
      - "8080:8080"
# Sin perfil — solo arranca app
docker compose up -d
docker compose ps
# Deberías ver solo el servicio "app"

# Con perfil dev — arranca app + redis + adminer
docker compose --profile dev up -d
docker compose ps
# Deberías ver los tres servicios

docker compose down

5. Verificar el debugging con debugpy (Python)

cd ..
mkdir -p test-debug
cd test-debug

Crea un Dockerfile.dev:

FROM python:3.12-slim
WORKDIR /app
RUN pip install debugpy uvicorn fastapi
CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "--wait-for-client", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Crea main.py:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Debug mode active"}

Crea un docker-compose.yml:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "8000:8000"
      - "5678:5678"
    volumes:
      - .:/app
docker compose up -d
# El contenedor se queda esperando a que VS Code se conecte al debugger
# Desde VS Code: configura attach y lanza la depuración
# Una vez conectado, visita http://localhost:8000
docker compose down

Si todos estos comandos funcionan, tienes un entorno de desarrollo Docker completamente operativo.

Conclusión

Este capítulo ha transformado Docker de herramienta de producción a entorno de desarrollo principal, cubriendo desde bind mounts hasta devcontainers completos.

En bind mounts aprendiste que montar tu código fuente en el contenedor es la clave del desarrollo ágil: modificas un archivo y el cambio se refleja instantáneamente sin reconstruir la imagen. Descubriste la trampa de node_modules y cómo evitarla con volúmenes anónimos.

En hot reload exploraste las herramientas específicas para cada lenguaje: Uvicorn --reload para Python, Nodemon para Node, Air para Go y Symfony CLI para PHP. Cada una con su Dockerfile.dev separado que activa la recarga automática.

En docker compose watch viste la evolución moderna de los bind mounts, con acciones declarativas (sync, rebuild, sync+restart) que hacen más predecible el flujo de desarrollo.

En docker-compose.override.yml dominaste la separación de configuraciones: un archivo base para producción y un override que automáticamente añade bind mounts, puertos de debug y perfiles de desarrollo.

En perfiles Compose aprendiste a activar o desactivar grupos de servicios con --profile, permitiendo tener administradores, workers y herramientas de monitoreo sin afectar el arranque principal.

En gestión de entornos implementaste .env.dev, .env.prod y el flag --env-file, combinado con variables por defecto (${VAR:-default}) para que el mismo compose funcione en cualquier entorno.

En Dev Containers configuraste devcontainer.json con extensiones, features (Docker-in-Docker), postCreateCommand y forwarding de puertos. El resultado: cualquier desarrollador abre el repositorio y tiene el entorno completo en segundos.

En depuración conectaste debugpy (Python), –inspect (Node), Delve (Go) y Xdebug (PHP) desde VS Code hacia el contenedor, con breakpoints, variables y call stack funcionando dentro del contenedor.

En bases de datos efímeras implementaste contenedores de base de datos sin persistencia (tmpfs, volúmenes anónimos) que garantizan un estado limpio en cada sesión de desarrollo, evitando datos residuales que rompen tests.

En el patrón development containers completo unificaste todo en una estructura de proyecto reproducible: docker/Dockerfile.dev, docker/Dockerfile (producción), docker-compose.yml, docker-compose.override.yml, docker-compose.prod.yml, .env.dev, .devcontainer/devcontainer.json, y el script dev_env_setup.py que automatiza la creación del proyecto completo.

Al aplicar estas técnicas, tu equipo podrá pasar de «en mi máquina funciona» a «funciona en Docker» — entornos idénticos, onboarding instantáneo, y desarrollo ágil con hot reload, debugging y bases de datos efímeras.

En el próximo capítulo veremos cómo optimizar imágenes Docker para que sean más rápidas, más pequeñas y más seguras. Dale caña.


Más información,

Deja una respuesta