flowchart LR
subgraph "Stage 1: build (se descarta)"
B[imagen full<br/>tsc/pip/go toolchain<br/>instala TODO + compila]
end
subgraph "Stage 2: runtime (lo que viaja)"
R[imagen slim/distroless<br/>solo el artefacto<br/>USER no-root<br/>HEALTHCHECK]
end
B -->|COPY --from=build<br/>solo dist/ o el binario| R
43 32. Necesitamos una imagen que producción acepte
Funciona, pero pesa 1.2 GB y corre como root
El Dockerfile del cap. 30 funciona — y es inaceptable para prod: lleva el toolchain completo (tsc, devDependencies, pip), corre como root, y arranca sin migraciones ni healthcheck. La imagen de producción se construye en etapas (multi-stage): la herramienta de build se queda atrás; solo viaja lo que el runtime necesita.
43.1 El problema
“Funciona” no es el checklist de producción. La imagen de dev pesa 1.2GB (toolchain + dev deps + cache de paquetes), corre como root (un proceso escapado = root en el host), no verifica que la app esté viva, y asume que la BD ya tiene el esquema — el primer deploy en un entorno limpio crashea contra tablas inexistentes.
Un deploy es poner tu código a correr en el servidor: copiar la imagen, iniciar el proceso, verificar que responde. El objetivo es que sea aburrido — automático, repetible, con rollback.
Un caché es una copia rápida de algo costoso de obtener: el resultado de una query pesada, la sesión. Redis es el caché estándar. La regla de oro: cachear es fácil, invalidar (saber cuándo el caché ya no vale) es lo difícil.
Una imagen es la plantilla inmutable de la que nacen contenedores: la foto del disco + cómo arrancar. Se construye en capas (Dockerfile), se versiona con tags, se publica en un registry.
Un entorno es una instancia completa donde corre tu app con su propia config y datos: local (tu máquina), staging (réplica de prueba), producción (la real). Cada entorno tiene sus propias llaves y su propia base de datos.
Un proceso es un programa en ejecución: el sistema operativo le da memoria propia y un lugar en la fila del CPU. Tu API es un proceso; Postgres es otro; el navegador es varios. Que “se caiga el proceso” = el programa murió.
Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.
El esquema es el plano de la base de datos: qué tablas hay, qué columnas tiene cada una y de qué tipo. Es contrato: una fila que no cumple el esquema no entra. Se cambia con migraciones, no a mano.
43.2 Cómo lo resuelve un equipo
Un equipo serio separa build de runtime: el multi-stage build usa una imagen gorda para compilar y copia solo el artefacto a una imagen mínima para correr. Encima: usuario no-root, healthcheck declarado, entrypoint que corre migraciones antes de servir, y la misma imagen en staging y prod — lo que cambia son las env vars (cap. 24: el artefacto no sabe dónde está).
Una migración es un cambio versionado del esquema: un archivo que dice “crea esta columna” (up) y “bórrala” (down). Son el historial Git de la estructura de la BD — se aplican en orden y no se editan una vez aplicadas.
Una variable de entorno es configuración que vive fuera del código: DATABASE_URL, JWT_SECRET. El mismo binario corre en dev y prod con distintas vars — los secretos nunca se escriben en el código.
El runtime es el motor que ejecuta tu código: Node.js es el runtime de JavaScript fuera del navegador; CPython es el de Python; Go compila a binario y el runtime va empaquetado dentro. Es “quien corre” lo que escribiste.
El build (construcción) es el proceso que convierte tu código fuente en algo ejecutable/entregable: compilar TypeScript a JS, empaquetar el frontend, armar la imagen Docker. Lo que se despliega es el resultado del build, no tu código crudo.
Un compilador traduce código a otra forma: TypeScript → JavaScript (transpila), Go → binario (compila). Atrapa errores antes de ejecutar. tsc, esbuild, go build son compiladores.
43.3 Conceptos nuevos
- Ops Multi-stage builds: herramientas de build fuera de la imagen final
- Ops Misma imagen en staging y prod, variables distintas (conexión con el cap. de config)
- Ops Migraciones al arranque: entrypoint que corre
migrateantes de servir - Ops No-root, healthcheck, señales y shutdown limpio: lo que plataforma revisa
43.4 La explicación visual
El stage de build puede pesar 2GB — no importa: se descarta. Lo que cuenta es lo que el COPY --from cruza a la imagen final.
43.5 Implementación
Dockerfile de producción por stack:
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci # all deps incl. dev (tsc lives here)
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev # prod deps only — no tsc, no vitest
COPY --from=build /app/dist ./dist
USER node # not root
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD node -e "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["sh", "-c", "npm run migrate && node dist/index.js"] # migrate, then serveFROM python:3.12-slim AS build
WORKDIR /app
RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=build /install /usr/local
COPY . .
RUN useradd -m app && chown -R app /app
USER app # not root
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request;urllib.request.urlopen('http://localhost:8000/health')"
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"]FROM golang:1.22 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server ./cmd/server
FROM gcr.io/distroless/static
COPY --from=build /app/server /server
COPY --from=build /app/migrations /migrations
USER nonroot # distroless has no root shell anyway
EXPOSE 8080
# no shell in distroless → migrations run as a separate one-shot job,
# or embed golang-migrate into the binary and run at boot
CMD ["/server"]Convierte mi Dockerfile a producción [Node+TS/Python/Go]. Requisitos:
multi-stage (build con toolchain → runtime solo con artefacto),
dependencias solo de producción en la imagen final, usuario no-root,
HEALTHCHECK contra /health, entrypoint que corre migraciones antes de
servir (o job separado si no hay shell), ENV de producción, y la misma
imagen servirá en staging y prod — solo cambian las env vars. Dame el
Dockerfile + el checklist de lo que cambió y por qué.
43.6 ¿Por qué cada stack lo hace así?
Lo único que necesitas llevarte: el patrón es universal — compilar gordo, correr flaco. Lo que cambia es qué es “el artefacto”: dist/ + deps de prod (Node), el site-packages instalado (Python), o un binario que es literalmente todo (Go — por eso su stage final puede ser distroless, sin shell ni OS).
| Requisito | Qué compra | En el Dockerfile de arriba |
|---|---|---|
| Multi-stage | Imagen final sin toolchain | AS build + COPY --from |
| Prod deps only | Menos superficie y peso | npm ci --omit=dev |
| No-root | Un exploit no es root en el host | USER node / useradd / nonroot |
| HEALTHCHECK | El orquestador sabe si estás vivo | HEALTHCHECK CMD ... |
| Migrate antes de servir | El esquema viaja con la versión | migrate && serve en el CMD |
| Graceful shutdown | Termina requests antes de morir | SIGTERM → cerrar server + pool (app-side) |
Shutdown limpio: cuando el orquestador hace rolling deploy manda SIGTERM — la app debe parar de aceptar requests, terminar los en curso (5-10s de gracia), cerrar el pool de BD y salir. Un process.exit(0) instantáneo corta requests a la mitad.
43.7 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
| El toolchain en la imagen final | “Build y run en la misma” | Multi-stage — COPY --from solo el artefacto |
USER root por omisión |
Es el default | USER explícito — plataformas serias lo exigen |
| Imagen distinta por entorno | Dockerfile.prod vs Dockerfile.staging |
UNA imagen + env vars — la paridad es el punto |
| Migraciones olvidadas | “La corrí a mano una vez” | En el entrypoint o job — el esquema viaja con el deploy |
SIGTERM ignorado |
El proceso muere a la fuerza | Handler: stop accepting → drain → close pool → exit |
43.8 Buenas prácticas
- La imagen es inmutable y versionada (
:1.4.2,:sha-abc123) — nuncalatesten prod: el rollback es cambiar el tag, no rebuildar. - Build una vez, promocionar la misma imagen dev→staging→prod — si se rebuilda por entorno, no estás probando lo que despliegas.
- Healthcheck vs la app: el
HEALTHCHECKdel Dockerfile es el fallback; el orquestador (cap. 33) usa/healthdel cap. 20 del Ap. G — ligero para el balanceador,/health/deeppara ti. - Escanea la imagen (
docker scout, Trivy en CI, cap. 34): las CVEs de la base son tuyas también — las bases slim/distroless tienen menos que escanear y menos que temer.
43.9 Ejercicio
- ¿Por qué
npm ci --omit=deven el stage final sidist/ya está compilado? ¿Qué queda dentro y qué se excluye? - El CMD corre
migrate && serve. ¿Qué pasa si dos réplicas arrancan a la vez y ambas intentan migrar? ¿Cómo lo resuelve el mundo real? - Enumera qué hace tu app idealmente al recibir
SIGTERMantes de salir.
Compilado (Go): el código se traduce a binario una vez y ese binario corre solo — rápido, deploy simple. Interpretado (Python, JS): un runtime lee el código en cada ejecución — flexible, pero necesita el runtime instalado.
43.10 Mini reto
Auditar la imagen contra un checklist de producción.
Ejercicio.
dist/es el JS compilado, pero el runtime aún necesita las deps de producción (express,pg,zod— imports en runtime).--omit=devexcluyetypescript,vitest,@types/*— lo que solo servía para compilar/testear.- Las migraciones serias toman un advisory lock (pg_migrate/golang- migrate lo hacen): una réplica migra, las demás esperan — la migración es idempotente y ordenada. Alternativa de plataforma: correr migraciones como job separado antes de rotar instancias (release phase de Heroku, init job de k8s) — la app solo sirve.
- Dejar de aceptar conexiones nuevas, (b) terminar las en curso con gracia (~10s), (c) cerrar el pool de BD y workers, (d)
exit(0). Requests cortados a la mitad son los errores fantasma de cada deploy.
- Dejar de aceptar conexiones nuevas, (b) terminar las en curso con gracia (~10s), (c) cerrar el pool de BD y workers, (d)
Mini reto. El checklist: multi-stage ✓, prod-deps only ✓, no-root ✓, healthcheck ✓, migraciones automáticas ✓, .dockerignore ✓, tag versionado ✓, SIGTERM manejado ✓, imagen escaneada sin CVEs críticas ✓, misma imagen en todos los entornos ✓. Cada ❌ es una línea concreta de trabajo — la imagen de prod es un checklist, no una sensación.
43.11 Vocabulario técnico del capítulo
Una variable es una caja con nombre donde guardas un valor: let total = 42 guarda el 42 bajo el nombre total. Puedes leerla y cambiarla después (total = 50). const = caja que no se puede reemplazar.
Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.
Un pool es el staff de conexiones a la BD: abrir una conexión por request es caro, así que el pool mantiene ~10–20 abiertas y las presta. El request la usa, la devuelve, y la siguiente la reutiliza.
Un servidor es una computadora que espera peticiones y las responde 24/7. Físicamente no es nada mágico: es una máquina (a veces una VM alquilada) corriendo tu programa, con la diferencia de que está siempre encendida y conectada.
Una dependencia es código de terceros que tu proyecto usa: npm install, pip install, go get las traen. Cada una es deuda — ahora funciona, pero hay que mantenerla, actualizarla y confiar en ella.
Un puerto es una puerta numerada dentro de una computadora: la IP llega al edificio, el puerto al departamento. :3000 = “la app que escucha en el departamento 3000”. Postgres suele usar 5432, HTTP el 80, HTTPS el 443.
localhost significa “esta misma máquina” — es la dirección que tu computadora usa para hablarse a sí misma. Cuando desarrollas, el “servidor” y el “cliente” viven en tu laptop: por eso todo es localhost:3000.