Stack: Docker + compose · Proyecto: TicketFlow Estado: Publicada — apertura del módulo de despliegue Prerrequisito: Lección 39 — Pools, paginación y compresión
Objetivos
- Escribir el Dockerfile multi-stage de TicketFlow: imagen pequeña, sin secretos, reproducible desde el lockfile.
- Armar el compose de desarrollo con healthchecks y volúmenes que no torturan el hot-reload.
- Aplicar la regla de la 27 a la imagen: una imagen, tres entornos — nada de config horneada.
1. El Dockerfile multi-stage, línea a línea
El multi-stage separa CONSTRUIR (compiladores, headers, dev deps) de EJECUTAR (runtime mínimo):
# ---- builder ----
FROM python:3.12-slim AS builder
ENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
COPY requirements.txt requirements.lock ./
RUN pip install --prefix=/install -r requirements.lock
# ---- runtime ----
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 curl && rm -rf /var/lib/apt/lists/* # solo el runtime de libpq, no el -dev
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN python manage.py collectstatic --noinput \
&& useradd -m 10001 && chown -R 10001 /app
USER 10001
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS http://localhost:8000/healthz/ | grep -q ok
CMD ["gunicorn", "ticketflow.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]Los detalles que separan una imagen adulta de un tutorial: --prefix=/install copia SOLO el venv instalado (no pip cache, no compiladores); libpq5 sin el -dev (compilar quedó en builder); usuario no-root (el contenedor comprometido no escribe fuera de lo suyo, 22); collectstatic en build (no en cada arranque); el HEALTHCHECK (43 lo usa para saber si el contenedor vive). El lockfile (requirements.lock) fija la versión EXACTA de cada dependencia (con hashes si el paranoico del 22 gana): la imagen es reproducible — el mismo hash hace 6 meses o hoy.
2. Las reglas de la capa COPY y el.dockerignore
Cada capa COPY invalida las siguientes: el orden es del MENOS cambiante al MÁS (requirements antes que el código: el código cambia a diario, las deps cada semana — el cache de pip sobrevive al deploy diario). Y el .dockerignore es el primo del .gitignore (06): excluye .git (240 MB de historia en la imagen: no), .env* (la 27: un secreto horneado en la imagen es un secreto filtrado — docker history y el registry lo conservan), tests/, docs/, el sqlite del dev y los virtualenvs. El test del secreto: docker history ticketflow --no-trunc | grep -i secret debe devolver vacío — y trivy image ticketflow (41 lo mete en CI) para las CVEs de la capa base.
3. El compose de desarrollo
El compose orquesta los 5 procesos de TicketFlow con las esperas sanas:
services:
db:
image: postgres:16-alpine
environment: {POSTGRES_USER: tf, POSTGRES_PASSWORD: tf, POSTGRES_DB: tf}
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck: {test: ["CMD-SHELL", "pg_isready -U tf"], interval: 5s, retries: 10}
redis:
image: redis:7-alpine
healthcheck: {test: ["CMD", "redis-cli", "ping"], interval: 5s, retries: 10}
web:
build: .
env_file: .env # la config del entorno (27), jamás horneada
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./site:/app/site # bind mount: el hot-reload lee tu editor
- ./ticketflow:/app/ticketflow
depends_on:
db: {condition: service_healthy} # espera SALUD, no solo "arrancó"
redis: {condition: service_healthy}
ports: ["8000:8000"]
worker:
build: .
command: celery -A ticketflow worker -l info
depends_on: {db: {condition: service_healthy}, redis: {condition: service_healthy}}
beat:
build: .
command: celery -A ticketflow beat -l info
volumes: {pgdata: {}}Dos matices de desarrollo: el runserver y el bind mount dan hot-reload (el código del editor entra al contenedor); y el worker/beat SIN el runserver usan la MISMA imagen (la 27: una imagen, comandos distintos — el comando es del entorno, no de la imagen). La producción NO usa este compose para "desplegar" (43 decide si es K8s/Cloud Run/compose de prod), pero las variables y healthchecks son los mismos: el compose de dev es el prototipo del despliegue.
4. Migraciones y arranque: el orden que no corrompe
El error clásico: command: python manage.py migrate && runserver en el compose — tres réplicas arrancan a la vez y tres migrate compiten por la tabla django_migrations (el lock de la 10 lo serializa, pero el warning y la lentitud no se explican solos). El patrón adulto: las migraciones son un JOB del despliegue (41/43), no el arranque del contenedor: el pipeline corre migrate UNA vez (11: expand-contract pensado para desplegar), después despliega las réplicas nuevas. El arranque del contenedor solo espera salud (healthcheck) y sirve. El wait-for-it.sh y primos son el parche del problema mal resuelto; el healthcheck de compose + el job de migrate en CI/CD lo resuelven de raíz.
5. El tamaño, la seguridad y la reproducibilidad
La métrica del tamaño: la imagen de runtime de TicketFlow queda ~180-220 MB (slim + venv + statics): docker images es el instrumento; cada 100 MB son 3 s extra de pull en el despliegue (55 veces al día, 41). Seguridad: el usuario no-root, la imagen base con digest pinneado (python:3.12-slim@sha256:... para que "slim" no cambie bajo tus pies), y el scan en CI (41). Y la prueba de reproducibilidad final — la de la 27:
docker build -t ticketflow:latest . # una vez
docker run --env-file .env.dev -p 8000:8000 ticketflow:latest
docker run --env-file .env.stag -p 8001:8000 ticketflow:latest
# mismo image ID en los tres entornos: el despliegue es config, no buildAutoevaluación
- ¿Qué queda en cada stage del multi-stage y por qué el runtime NO lleva ni pip ni el
-devde libpq? - ¿Por qué requirements antes que código en las capas COPY y qué invalida a qué? ¿Qué debe tener SIEMPRE el.dockerignore?
- El healthcheck de compose: ¿qué diferencia hay entre "arrancó" y "está sano", y qué condición lo usa el web?
- ¿Por qué
migrate && runserveren el compose es el patrón que corrompe y dónde viven las migraciones en el despliegue adulto? - La prueba de reproducibilidad: ¿qué demuestra el mismo image ID con tres.env y cómo se pinnea la base para que no cambie bajo tus pies?
Continúa con los ejercicios. Las solutions.md solo tras intentarlo.