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:
- Clonar el repositorio
- Leer un README de 30 páginas con instrucciones de instalación
- Instalar Python 3.10, Node 18, PostgreSQL 15, Redis… en tu máquina
- Descubrir que tu compañera tiene Python 3.11 y algo no funciona
- Pasar horas debugando «en mi máquina funciona»
- 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 archivossync+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ística | Bind mount | docker compose watch |
|---|---|---|
| Sincronización | Inmediata (kernel) | Policy-based |
| Rebuild automático | ❌ No | ✅ Sí |
| Ignorar patrones | Manual (dockerignore) | ✅ ignore explícito |
| Reinicio de servicio | ❌ No | ✅ sync+restart |
| Eficiencia en I/O | Alta | Media (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
- Nunca subas .env.prod al repositorio — contiene secretos reales
- Sube .env.dev y .env.example para que otros desarrolladores sepan qué variables se necesitan
- Usa
${VAR:-default}para valores por defecto - 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
- Solo tmpfs: datos en RAM, ultra rápidos, desaparecen al parar el contenedor
- Volumen anónimo: persisten mientras no hagas
docker compose down -v - Script de seed: ejecuta
docker compose exec db psql -f /scripts/seed.sqltras levantar - init scripts: monta
./init.sqlen/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
| # | Error | Consecuencia | Solución |
|---|---|---|---|
| 1 | Usar COPY en lugar de bind mount | Cada cambio requiere rebuild completo de la imagen | Usa volumes: - ./src:/app/src en docker-compose.override.yml |
| 2 | Olvidar node_modules en bind mount | node_modules del contenedor se sobrescribe con el directorio vacío del host | Añade /app/node_modules como volumen anónimo |
| 3 | No separar Dockerfile.dev de Dockerfile | La imagen de desarrollo es pesada y lenta (incluye debuggers en producción) | Crea docker/Dockerfile.dev separado con hot reload y debug |
| 4 | Exponer puertos de debug en producción | El contenedor en producción expone 5678 o 9229, un riesgo de seguridad | Usa docker-compose.override.yml solo para desarrollo (no desplegar) |
| 5 | No usar .env-file para entornos | Variables hardcodeadas en docker-compose.yml, difícil cambiar entre entornos | Usa --env-file .env.dev y variables con ${VAR:-default} |
| 6 | Contenedores con datos persistentes en dev | Estado sucio entre sesiones, tests que fallan por datos residuales | Usa tmpfs o volúmenes anónimos para bases de datos efímeras |
| 7 | Ignorar docker compose watch | Bind mounts funcionan pero no reconstruyen cuando cambian dependencias | Usa develop.watch con acciones sync, rebuild y sync+restart |
| 8 | No configurar healthchecks | depends_on espera solo a que el contenedor arranque, no a que el servicio responda | Añade condition: service_healthy con healthcheck |
| 9 | Desarrollar como root dentro del contenedor | Archivos creados pertenecen a root, problemas de permisos en el host | Usa remoteUser en devcontainer.json y USER en Dockerfile |
| 10 | No compartir devcontainer.json | Cada desarrollador configura su IDE manualmente, entornos inconsistentes | Sube .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,
- Documentación oficial de Docker — Docker Compose overview
- Documentación oficial de Docker — Compose file reference
- Docker Compose Watch (Docker Docs)
- Docker Compose Profiles (Docker Docs)
- Docker Compose Override / Extends (Docker Docs)
- Documentación oficial de Docker — Bind mounts
- Documentación oficial de Docker — tmpfs mounts
- Dev Containers specification
- VS Code Dev Containers
- Debugpy — Python debugger for VS Code
- Delve — Go debugger
- Uvicorn — ASGI server con –reload
- Nodemon — Node.js auto-restart
- Air — Go live reload
- Symfony CLI — PHP dev server
- Tutorial de atareao.es: Docker — Fuente de este capítulo
- Tutorial de atareao.es: Traefik v3
- Tutorial de atareao.es: Podman
- Tutorial de atareao.es: Seguridad self-hosted
- Docker Development Best Practices (Docker Blog)
- Setting up a Python Dev Environment with Docker (Real Python)
- Using Docker for Node.js Development (Node.js Docs)
- Go Development with Docker (Go Blog)
- Dev Containers tutorial (Microsoft)
- 12 Factor App — Config
- postgres Docker Hub
- redis Docker Hub