flowchart LR D[Dockerfile<br/>la receta en texto] -->|docker build| I[Imagen<br/>paquete inmutable ~clase] I -->|docker run| C1[Contenedor 1] I -->|docker run| C2[Contenedor 2] I -->|deploy| P[Prod<br/>misma imagen]
41 30. Necesitamos que cualquier máquina ejecute esto igual
«En mi máquina funciona» deja de ser una excusa

El nuevo dev tardó dos días en arrancar el proyecto: versión de Node distinta, Postgres mal configurado, una variable que nadie documentó. Docker empaqueta la app completa — runtime, dependencias, código — en una imagen que corre igual en cualquier máquina. La receta es el Dockerfile; la imagen es la clase; el contenedor es la instancia.
41.1 El problema
“En mi máquina funciona” significa exactamente eso: funciona en tu máquina — con tu Node 20, tu Postgres 16, tu OS. La laptop del compañero tiene Node 18; el servidor tiene Ubuntu sin Node instalado; CI tiene otra cosa. Cada entorno es una lotería de versiones. La pregunta que Docker responde: ¿cómo hago que “mi máquina” viaje con el código?
Docker empaqueta tu app con todo lo que necesita (runtime, librerías, config) en una imagen — el mismo paquete corre idéntico en tu laptop y en producción. Es la respuesta a “en mi máquina sí funcionaba”.
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.
41.2 Cómo lo resuelve un equipo
Un equipo serio empaqueta el entorno como código: un Dockerfile es la receta declarativa — “parte de Node 20, instala dependencias, copia el código, corre node dist/index.js” — que cualquier máquina con Docker ejecuta idéntico. El onboarding pasa de dos días de wiki a docker compose up. Y el mismo artefacto que el dev corre es el que prod corre — la paridad dev/prod del cap. 24 llevada al extremo.
Docker Compose describe un sistema de varios contenedores en un YAML: la app + Postgres + Redis, sus redes y volúmenes. docker compose up levanta el entorno completo de desarrollo con un comando.
Producción (prod) es el entorno real: donde están los usuarios, los datos que importan y las consecuencias. Todo lo demás — local, staging — existe para que los errores ocurran antes de llegar ahí.
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.
41.3 Conceptos nuevos
- Ops Qué es un contenedor (sin la teoría de namespaces): máquina mínima reproducible
- Ops Imagen vs contenedor vs Dockerfile: la analogía clase/instancia
- Ops Dockerfile por lenguaje:
tsc→runtime vs venv vs binario estático de ~15MB - Ops
.dockerignore: qué nunca entra en una imagen
41.4 La explicación visual
Imagen vs contenedor = clase vs instancia: la imagen es el paquete inmutable (runtime + deps + código); cada docker run crea un contenedor vivo de esa imagen. Cambiar código = construir nueva imagen — los contenedores son descartables por diseño.
Un contenedor es un proceso con mochila propia: corre aislado con su filesystem y sus librerías, pero comparte el kernel de la máquina — por eso arranca en milisegundos donde una VM tarda minutos.
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.
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.
41.5 Implementación
El primer Dockerfile por stack — funcional (la versión de producción viene en el cap. 32):
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci # lockfile exact, not "npm install"
COPY . .
RUN npm run build # tsc → dist/
EXPOSE 3000
CMD ["node", "dist/index.js"]# .dockerignore — what never enters the image
node_modules
dist
.env
.git
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]# .dockerignore
.venv
__pycache__
.env
.git
FROM golang:1.22 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server ./cmd/server
FROM gcr.io/distroless/static # the runtime is just the binary
COPY --from=build /app/server /server
EXPOSE 8080
CMD ["/server"] # ~15MB total — static binary, no OS neededCorrerlo:
docker build -t taskflow-api . # recipe → image
docker run -p 3000:3000 --env-file .env taskflow-apiEscribe el Dockerfile de mi API [Express+TS/FastAPI/Go net-http] para
desarrollo. Requisitos: imagen base slim oficial, instalar dependencias
por lockfile (npm ci / requirements.txt / go mod download) en capa
separada para cachear, compilar si aplica (tsc/go build), .dockerignore
con node_modules|venv|.env|.git, EXPOSE del puerto correcto, y CMD que
arranque el servidor en 0.0.0.0. Explícame cada instrucción y cómo
construir y correr la imagen.
41.6 ¿Por qué cada stack lo hace así?
Lo único que necesitas llevarte: el Dockerfile es siempre la misma receta — base → deps → código → run. Lo que cambia es cuánto necesita el runtime: TS/Python llevan su intérprete dentro de la imagen; Go compila un binario que no necesita nada más — por eso su imagen final cabe en ~15MB.
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.
- Capas y caché: cada instrucción es una capa cacheada. Por eso
COPY package*.json+RUN npm civan antes deCOPY . .— las deps solo se reinstalan cuando el lockfile cambia, no en cada build. - Tamaños honestos:
node:20-slim~200MB + node_modules; Python similar + venv; Godistroless~15MB — el binario estático es todo el runtime. La diferencia no es estética: menos imagen = menos superficie de ataque y deploys más rápidos. CGO_ENABLED=0: fuerza el binario 100% estático — sin esa flag, Go enlaza libc y eldistrolessno la tiene.
41.7 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
COPY . . antes de instalar deps |
El orden “natural” de escribir | Deps primero (cachea), código después (cambia seguido) |
.env dentro de la imagen |
COPY . . lo incluye |
.dockerignore — la imagen que lleva secretos es un incidente |
node_modules copiado al build |
Sin .dockerignore |
Excluirlo — la imagen instala las suyas para su OS |
npm install en vez de npm ci |
Es el comando que conoces | ci = lockfile exacto, reproducible; install puede resolver distinto |
| Correr en dev lo que es de dev | --reload, debugger, sourcemaps |
La imagen de prod es otro Dockerfile (cap. 32) |
41.8 Buenas prácticas
- Bases oficiales y slim:
node:20-slim,python:3.12-slim— el-slimquita lo que no necesitas; Alpine existe pero sus libc distintas muerden a veces. .dockerignoredesde el día uno:.env,.git,node_modules,dist— lo que no entra no puede filtrarse ni pesar.- Un proceso por contenedor: el contenedor es tu app — si muere, muere el contenedor y el orquestador lo nota.
Kubernetes es el orquestador de contenedores: le dices “quiero 3 réplicas de esta imagen” y él las distribuye, reinicia las que mueren y las expone. Poderoso y complejo — se usa cuando las máquinas ya son manada.
- Tags con sentido:
taskflow-api:1.4.2/:commit-sha—latestes el tag que “nadie sabe qué versión es”.
EXTRA El roadmap externo lista — Docker (el estándar de contenedores, este capítulo), Kubernetes (orquestador de manadas de contenedores — nu-05 lo ubica), rkt (histórico, ya descontinuado — buen recordatorio de que las listas envejecen y los conceptos quedan).
41.9 Ejercicio
- ¿Por qué
COPY package*.json+RUN npm civan antes deCOPY . .? ¿Qué se rompe si inviertes el orden? - Lista tres cosas que tu
.dockerignoredebe excluir y el daño concreto de cada una si entra. - La imagen de Go pesa ~15MB y la de Node ~250MB+. ¿Qué hay dentro de cada una que explica la diferencia?
41.10 Mini reto
Reducir el tamaño de la imagen a la mitad explicando cada decisión (spoiler: Go gana).
Ejercicio.
- La caché de capas: deps cambian poco (lockfile estable) → su capa se reutiliza en cada build. Código cambia siempre → su capa va al final. Invertir el orden: cada cambio de código invalida también la capa de deps →
npm cicompleto en cada build (minutos en vez de segundos). .env(secretos dentro de la imagen — cualquiera con la imagen los tiene),node_modules/.venv(deps del OS equivocado + peso — la imagen instala las suyas),.git(historial completo = secretos históricos + peso).- Node/Python llevan el intérprete completo + stdlib + deps en la imagen (~200-400MB). Go compila a binario estático: la imagen es literalmente el ejecutable + nada —
distrolessno tiene ni shell.
Mini reto. Los pasos típicos: base slim en vez de full (−150MB), multi-stage para dejar el toolchain fuera de la imagen final (cap. 32), .dockerignore agresivo, npm ci --omit=dev (sin devDependencies), y en Go el salto a distroless/scratch. Cada decisión responde “¿esto lo necesita el runtime o solo el build?” — lo del build no viaja.
41.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 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.
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.
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 commit es una foto guardada del código con un mensaje que dice por qué cambió. La historia del proyecto es una cadena de commits: puedes volver a cualquiera, ver qué cambió y quién lo hizo.
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.
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ó.
Un secreto es un dato que no puede publicarse: contraseñas de BD, llaves de API, el secreto que firma los JWT. Viven en .env (fuera de git) o en un secret manager — nunca en el código ni en el repo.
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.