43  32. Necesitamos una imagen que producción acepte

Funciona, pero pesa 1.2 GB y corre como root

NotaEn una frase

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 migrate antes de servir
  • Ops No-root, healthcheck, señales y shutdown limpio: lo que plataforma revisa

43.4 La explicación visual

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

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 serve
FROM 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) — nunca latest en 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 HEALTHCHECK del Dockerfile es el fallback; el orquestador (cap. 33) usa /health del cap. 20 del Ap. G — ligero para el balanceador, /health/deep para 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

  1. ¿Por qué npm ci --omit=dev en el stage final si dist/ ya está compilado? ¿Qué queda dentro y qué se excluye?
  2. 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?
  3. Enumera qué hace tu app idealmente al recibir SIGTERM antes 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.

  1. dist/ es el JS compilado, pero el runtime aún necesita las deps de producción (express, pg, zod — imports en runtime). --omit=dev excluye typescript, vitest, @types/* — lo que solo servía para compilar/testear.
  2. 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.
    1. 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.

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.

43.12 Lo que deberías saber hacer ahora