flowchart TB
subgraph docker-compose.yml
subgraph net[taskflow_net — red privada]
APP[app<br/>tu imagen<br/>env: DATABASE_URL=postgres://db:5432/...]
DB[(db<br/>postgres:16<br/>vol: pgdata)]
APP -->|"db" resuelve por nombre| DB
end
DEV[Tu máquina] -->|localhost:3000| APP
end
DB -.->|persiste| V[(volumen pgdata<br/>sobrevive al contenedor)]
42 31. Necesitamos la app y Postgres levantándose juntas
El entorno completo en un archivo
docker run postgres, docker run app, crear la red a mano, acordarse del orden… funciona si no se te olvida un paso. docker-compose.yml declara el entorno completo — app + BD + su red + sus volúmenes — en un archivo que se levanta con un comando y se versiona con el código.
42.1 El problema
La receta manual: crear una red, lanzar Postgres con 5 flags, esperar a que esté listo, lanzar la app apuntando al host correcto, repetir mañana. Cada dev lo hace distinto, nadie documenta el orden, y “Postgres arrancó” no significa “Postgres acepta conexiones” — la app crashea en el arranque por llegar 2 segundos temprano.
42.2 Cómo lo resuelve un equipo
Un equipo serio declara el entorno como código: docker-compose.yml dice qué servicios existen, cómo se conectan, qué volúmenes persisten sus datos y qué espera a qué. El onboarding es git clone + docker compose up. Y como el archivo vive en el repo, el entorno también tiene code review.
Un volumen es almacenamiento que sobrevive al contenedor: el contenedor muere y renace, el volumen (los datos de Postgres) sigue ahí. Sin volumen, docker compose down borra tu base de datos.
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 repositorio (repo) es la carpeta del proyecto con todo su historial Git adentro. GitHub/GitLab son servicios que hospedan repos para compartirlos y respaldarlos.
42.3 Conceptos nuevos
- Ops docker-compose: servicios, redes, volúmenes,
depends_on - Ops Volúmenes: dónde viven los datos cuando el contenedor muere
- Ops
depends_on+ healthcheck: “Postgres listo” no es “Postgres arrancado” - Ops El workflow diario:
compose up, logs, rebuild —.envdentro de compose
42.4 La explicación visual
La magia silenciosa: dentro de la red de compose, db es un nombre DNS — la app conecta a postgres://db:5432 sin saber IPs. El volumen pgdata guarda los datos fuera del contenedor: docker compose down mata contenedores, no datos.
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.
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.
El DNS es la agenda telefónica de internet: traduce nexus.com → 203.0.113.10. Tu dominio apunta vía DNS a tu load balancer o servidor — por eso “propagación de DNS” toma minutos.
42.5 Implementación
El docker-compose.yml completo — agnóstico de lenguaje (cambia la imagen del servicio app):
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.
services:
app:
build: . # the Dockerfile from cap. 30
ports: ["3000:3000"] # host:container
environment:
DATABASE_URL: postgres://taskflow:taskflow@db:5432/taskflow
JWT_SECRET: ${JWT_SECRET} # from your shell's env / .env file
CORS_ORIGIN: http://localhost:5173
depends_on:
db:
condition: service_healthy # wait for READY, not just started
command: ["sh", "-c", "npm run migrate && node dist/index.js"]
db:
image: postgres:16
environment:
POSTGRES_USER: taskflow
POSTGRES_PASSWORD: taskflow
POSTGRES_DB: taskflow
volumes:
- pgdata:/var/lib/postgresql/data # data outlives the container
healthcheck: # what "ready" means
test: ["CMD-SHELL", "pg_isready -U taskflow"]
interval: 2s
timeout: 3s
retries: 10
volumes:
pgdata:El workflow diario:
docker compose up --build # build images + start everything
docker compose logs -f app # follow one service's logs
docker compose down # stop and remove containers (data persists)
docker compose down -v # ALSO wipe the volume — fresh DB
docker compose exec db psql -U taskflow # jump inside the DB containerEscribe el docker-compose.yml de mi proyecto [Express+TS/FastAPI/Go] +
Postgres 16. Requisitos: servicio app que buildea del Dockerfile local,
expone el puerto, recibe DATABASE_URL apuntando al servicio db por
nombre, espera a db con depends_on + healthcheck (pg_isready), corre
las migraciones antes de arrancar el servidor, credenciales de dev en
environment, JWT_SECRET desde .env del host, volumen nombrado para los
datos, y los comandos del workflow diario (up, logs, exec psql, down -v).
42.6 ¿Por qué así y no “un script bash que lanza todo”?
Lo único que necesitas llevarte: compose convierte pasos en declaración — describes el estado final (“estos servicios, esta red, estos volúmenes”) y Docker calcula cómo llegar. El script describe pasos; el YAML describe la realidad — y se puede leer, revisar y versionar.
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”.
| Alternativa | Qué es | Qué te cuesta | Cuándo |
|---|---|---|---|
| docker-compose | Declarativo, un archivo, en el repo | Solo local/dev — no es orquestador de prod | El default para dev |
| Makefile/script | Comandos encadenados | Frágil: orden manual, sin healthchecks | Proyecto de un servicio |
| Devcontainers | El IDE vive dentro del contenedor | Config extra, IDE dependiente | Onboarding cero-instalación |
| Kubernetes local | kind/minikube — prod-like | Overkill para dev diario | Cuando prod es k8s y quieres paridad |
42.7 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
depends_on: [db] sin healthcheck |
“Ya depende de db” | depends_on solo ordena el arranque — service_healthy espera a que Postgres acepte conexiones |
localhost en DATABASE_URL dentro de compose |
Es el host que siempre funciona | Dentro de la red de compose el host es db — el nombre del servicio es DNS |
| Datos dentro del contenedor | Sin volumen declarado | docker compose down = adiós BD → volumen nombrado siempre |
| Secretos en el compose commiteado | POSTGRES_PASSWORD: prod123 |
Dev passwords ok en compose; lo real viene de ${VAR} del entorno |
| Rebuild manual olvidado | “Cambié el código y no se ve” | up --build o watch — la imagen no se regenera sola |
42.8 Buenas prácticas
- Las migraciones corren en el
commanddeapp(o un serviciomigrateone-shot):git clone+up= entorno funcional, no solo servicios vivos. - Un
.envpara compose con las llaves no-secretas del entorno de dev — compose lo lee automáticamente para${VAR}. - Servicios con nombres de dominio:
db,api,frontend— el DNS interno los resuelve; nunca hardcodees IPs. docker compose execpara entrar (psql, shell) — el contenedor es una máquina que se puede inspeccionar, no una caja sellada.
42.9 Ejercicio
- ¿Por qué
depends_onsinconditionno evita el crash de arranque? ¿Qué haceservice_healthyque el default no hace? - Corriste
docker compose downy tus datos siguen ahí al próximoup. ¿Dónde vivían? ¿Qué flag los habría borrado? - La app conecta a
postgres://db:5432— ¿quién resuelvedba una IP y por qué no puedes usarlocalhost?
Una IP es la dirección numérica de una máquina en la red: 203.0.113.10. Los servidores tienen IP pública; dentro de una VPC tienen IPs privadas que internet no ve.
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.
42.10 Mini reto
Un compañero clona el repo y levanta todo con un solo comando.
Ejercicio.
depends_onpor defecto espera a que el contenedor exista (service_started), no a que Postgres acepte conexiones — la app arranca contra una BD aún inicializándose → crash.service_healthyespera alhealthcheck(pg_isreadydevuelve ok) = “listo de verdad”.- En el volumen nombrado
pgdata— los volúmenes viven fuera del ciclo de vida del contenedor.docker compose down -vlos borra (la BD fresca del siguienteup). - La red interna de compose tiene DNS: cada servicio es alcanzable por su nombre.
localhostdentro del contenedor de la app es el contenedor mismo — no tu máquina, no el contenedor de Postgres.
Mini reto. Es la prueba de fuego del capítulo: git clone + docker compose up --build debe bastar — build de la imagen, Postgres con healthcheck, depends_on ordenado, migraciones en el command, .env.example para las llaves que faltan. Si necesita un paso manual, ese paso pertenece al YAML.
42.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.
Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.
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.
return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.
Un dominio es el nombre que compras (midominio.com) y apuntas a tu servidor por DNS. Sin él, tus usuarios tendrían que memorizar una IP. El HTTPS serio requiere dominio.
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.
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.
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.
Un log es el diario del programa: qué pasó, cuándo, con qué request. Estructurado = en JSON con campos (request_id, user_id), para filtrar por máquina y no con los ojos.
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.