---
name: contenedores-docker
description: Docker y containerización. Dockerfiles optimizados con multi-stage builds, docker-compose, volúmenes, networking, health checks, security scanning, build caching, distroless images. Anti-patrones comunes.
version: "1.0.1"
evolved: true
evolved-from: "1.0.0"
evolved-at: "2026-05-05"
evolved-by: "aprender"
evolved-note: "Gotcha de la sesión SIGM 2026-05-05 (L6): scripts en /docker-entrypoint-initdb.d con dependencias externas (mc, aws, etc.) abortan initdb"
herramientasPermitidas: [Read]
exclusiones:
  - "No cargar para orquestación Kubernetes (Deployments, Services, Helm, HPA) — para Kubernetes cargar `kubernetes-orquestacion`."
  - "No cargar para infraestructura como código con Terraform o Pulumi — para IaC cargar `terraform-experto`."
  - "No cargar para CI/CD pipelines (GitHub Actions, GitLab CI, build triggers) — para pipelines cargar `ci-cd-pipelines`."
  - "No cargar para registros de contenedores en cloud (ECR, GCR, ACR) y sus políticas de acceso — para cloud cargar `cloud-aws`, `azure-cloud` o `gcp-cloud`."
evolvable: true  # default para skill estandar
---
# Docker y Containerización — Producción

## Cuándo NO cargar

- La tarea es orquestación de contenedores con Kubernetes: Deployments, Services, Helm, HPA — cargar `kubernetes-orquestacion`.
- El trabajo es infraestructura como código con Terraform o Pulumi — cargar `terraform-experto`.
- La tarea es configurar pipelines CI/CD: GitHub Actions, GitLab CI, build triggers automáticos — cargar `ci-cd-pipelines`.
- El trabajo es con registros de contenedores en cloud (ECR, GCR, ACR) y sus políticas IAM — cargar `cloud-aws`, `azure-cloud` o `gcp-cloud`.

Docker en producción requiere imágenes seguras, pequeñas y reproducibles.
Este skill cubre desde Dockerfiles hasta orquestación con Compose.

---

## Reglas Obligatorias

1. **SIEMPRE multi-stage build** — imagen final < 200MB si posible.
2. **NUNCA correr como root** — definir `USER` no-root en el Dockerfile.
3. **SIEMPRE `.dockerignore`** — sin él, el contexto incluye `.git`, `.env`, `node_modules`.
4. **SIEMPRE `HEALTHCHECK`** — el orquestador necesita saber si el contenedor está sano.
5. **NUNCA secrets en `ARG` o `ENV`** del Dockerfile — son visibles en `docker history`.
6. **SIEMPRE tag de versión exacta** en producción — NUNCA `latest`.
7. **`CMD` en forma exec (array)** — `CMD ["ejecutable", "arg"]`, no shell string.
8. **Copiar dependencias ANTES que código** — maximiza el caché de build.
9. **Limpiar apt-get en la misma capa de RUN** — `rm -rf /var/lib/apt/lists/*`.

---

## Dockerfile Base — Python API

```dockerfile
# Etapa 1: Dependencias (caché eficiente)
FROM python:3.12-slim AS deps

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential libpq-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY pyproject.toml uv.lock* requirements*.txt ./
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

# Etapa 2: Build
FROM deps AS builder
COPY . .
RUN python -m compileall -q /app

# Etapa 3: Producción (imagen final mínima)
FROM python:3.12-slim AS production

RUN groupadd --gid 1001 appuser && \
    useradd --uid 1001 --gid appuser --shell /bin/bash --create-home appuser

WORKDIR /app

COPY --from=builder --chown=appuser:appuser /usr/local/lib/python3.12/site-packages \
    /usr/local/lib/python3.12/site-packages
COPY --from=builder --chown=appuser:appuser /app /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 curl \
    && rm -rf /var/lib/apt/lists/*

USER appuser

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PYTHONFAULTHANDLER=1 \
    PORT=8000

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", \
     "--workers", "2", "--log-level", "info"]
```

---

## Anti-patrones Comunes

| Anti-patrón | Consecuencia | Corrección |
|-------------|-------------|------------|
| `FROM python:3.12` (imagen completa) | Imagen 1GB+ innecesaria | `python:3.12-slim` o `distroless` |
| Correr como `root` en contenedor | Escalación de privilegios | `USER 1001` en Dockerfile |
| `COPY . .` antes de dependencias | Invalidar caché en cada cambio | Copiar deps primero |
| Sin `.dockerignore` | Contexto enorme, secretos incluidos | `.dockerignore` siempre |
| `apt-get` sin limpiar | Imagen más grande | Limpiar en misma `RUN` |
| Secrets en `ARG` o `ENV` | Visibles en `docker history` | Runtime env o secrets |
| Latest tag en producción | No reproducible | Tag de versión exacta |
| Sin `HEALTHCHECK` | Orquestador no sabe si está sano | `HEALTHCHECK` siempre |
| `CMD` como shell string | Signals no llegan al proceso | Forma exec (array) |
| Múltiples procesos en un contenedor | Viola responsabilidad única | Un proceso por contenedor |

---

## Checklist de Imagen Productiva

- [ ] Multi-stage build — imagen final < 200MB si posible
- [ ] Usuario no-root definido
- [ ] Variables sensibles fuera de la imagen
- [ ] `HEALTHCHECK` definido
- [ ] `.dockerignore` presente y completo
- [ ] Dependencias del sistema limpiadas en misma capa de RUN
- [ ] Tag de versión semántica (no `latest`) en producción
- [ ] Escaneo de vulnerabilidades en CI/CD
- [ ] `CMD` en forma exec (array), no shell
- [ ] `PYTHONUNBUFFERED=1` y `PYTHONDONTWRITEBYTECODE=1` para Python

---

Para ejemplos completos de multi-stage Node.js/Angular, .dockerignore, docker-compose, networking, volúmenes, secrets, security scanning en CI y optimización de build cache, ver [recursos/ejemplos-y-configuraciones.md](recursos/ejemplos-y-configuraciones.md).

## Gotchas / Errores comunes no obvios

**`COPY . .` antes de `RUN pip install` invalida el caché de build en cada cambio de código fuente**: Docker invalida el caché de una capa cuando cambia cualquier archivo copiado — si el código fuente cambia en cada commit, la capa de instalación de dependencias nunca se cachea. Causa: el orden de las instrucciones en el Dockerfile determina la eficiencia del caché. Fix: copiar PRIMERO los archivos de dependencias (`pyproject.toml`, `requirements.txt`, `package.json`) e instalar, luego copiar el código fuente — las dependencias solo se reinstalan cuando sus archivos de definición cambian.

**`CMD ["python", "app.py"]` en lugar de `CMD ["gunicorn", ...]` en producción deja el proceso de Python sin manejo de señales SIGTERM**: cuando Kubernetes envía `SIGTERM` para terminar gracefully un pod, el proceso debe recibirlo y terminar limpiamente. Python corriendo con `python app.py` puede no propagar la señal correctamente si usa threads. Causa: `CMD` en forma exec pasa la señal al proceso directamente, pero si el proceso no maneja `SIGTERM`, el contenedor espera el `SIGKILL` tras el `terminationGracePeriodSeconds`. Fix: usar un servidor WSGI/ASGI (`gunicorn`, `uvicorn`) que maneja `SIGTERM` gracefully, o implementar el signal handler en la aplicación.

**Secrets pasados como `ARG` en el Dockerfile son visibles en `docker history` aunque se eliminen en una capa posterior**: `ARG SECRET_KEY` seguido de `RUN rm -f /app/.env` no elimina el valor del ARG del historial de la imagen — `docker history --no-trunc imagen` muestra los valores de los ARG. Causa: `docker history` registra los comandos de cada capa, incluyendo los valores de ARG usados. Fix: NUNCA pasar secretos como `ARG` o `ENV` en el Dockerfile; para secrets en build-time usar `--secret` de Docker BuildKit: `RUN --mount=type=secret,id=api_key cat /run/secrets/api_key`.

**`.dockerignore` mal configurado incluye archivos `.env` o `node_modules` en el contexto de build, ralentizando el daemon y exponiendo secretos**: si `.dockerignore` no existe o no excluye `node_modules`, el contexto de build puede ser de cientos de MB — todo se transfiere al daemon antes del build. Causa: el contexto de build incluye todo el directorio por defecto. Fix: crear `.dockerignore` con al menos `.git`, `node_modules`, `__pycache__`, `.env*`, `*.pyc`, `dist`, `build`, y verificar el tamaño del contexto con `docker build` mirando la línea "Sending build context to Docker daemon X.XX MB".

**Scripts en `database/init/` (mounted a `/docker-entrypoint-initdb.d`) no pueden depender de binarios fuera de la imagen base**: un script con `set -e` que invoca `mc` (cliente MinIO), `aws`, `gh`, `kubectl` o cualquier binario no presente en la imagen `postgres`/`postgis` aborta el initdb con `command not found` (exit 127). El contenedor sale con error y los scripts SQL que iban después (incluido schemas/seeds completos) NUNCA se ejecutan. Caso real (SIGM 2026-05-04): `02-crear-buckets-minio.sh` con `mc alias set` aborta `postgis/postgis:17-3.5` antes de cargar el schema; ningún schema se aplicaba aunque el archivo init.sh estaba bien. Causa: el directorio `/docker-entrypoint-initdb.d` ejecuta todos los archivos `.sh` y `.sql` en orden alfabético; cualquier fallo aborta toda la cadena. Fix: scripts que invocan binarios externos van en `scripts/setup/` o `scripts/init-deps/` y se ejecutan manualmente o via `docker compose run --rm --entrypoint /scripts/setup/X.sh <servicio_que_tiene_el_binario>`. Solo SQL puro o bash que use `psql` pueden vivir en `database/init/`.
