42  31. Necesitamos la app y Postgres levantándose juntas

El entorno completo en un archivo

NotaEn una frase

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 — .env dentro de compose

42.4 La explicación visual

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)]

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 container
Escribe 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 command de app (o un servicio migrate one-shot): git clone + up = entorno funcional, no solo servicios vivos.
  • Un .env para 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 exec para entrar (psql, shell) — el contenedor es una máquina que se puede inspeccionar, no una caja sellada.

42.9 Ejercicio

  1. ¿Por qué depends_on sin condition no evita el crash de arranque? ¿Qué hace service_healthy que el default no hace?
  2. Corriste docker compose down y tus datos siguen ahí al próximo up. ¿Dónde vivían? ¿Qué flag los habría borrado?
  3. La app conecta a postgres://db:5432 — ¿quién resuelve db a una IP y por qué no puedes usar localhost?

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.

  1. depends_on por 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_healthy espera al healthcheck (pg_isready devuelve ok) = “listo de verdad”.
  2. En el volumen nombrado pgdata — los volúmenes viven fuera del ciclo de vida del contenedor. docker compose down -v los borra (la BD fresca del siguiente up).
  3. La red interna de compose tiene DNS: cada servicio es alcanzable por su nombre. localhost dentro 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.

42.12 Lo que deberías saber hacer ahora