Apariencia
📙 Clase 10 — Docker y contenerización
Python para Backend · 2026-09-08 · Carpeta:
02-Ejercicios/Clase-10⬅️ Volver al índice de clases
🎯 Qué aprendí
- Por qué "en mi máquina funciona" no alcanza para entregar/desplegar un proyecto, y qué problema resuelve Docker (empaquetar app + runtime + dependencias en una unidad reproducible).
- La diferencia entre imagen (plantilla inmutable) y contenedor (instancia en ejecución), y de dónde salen las imágenes base (Docker Hub).
- A escribir un
Dockerfilereal (FastAPI) entendiendo el cacheo de capas, y a hacerdocker build+docker runcon el mapeo de puertos correcto. - Por qué
localhostdentro de un contenedor no es lo mismo quelocalhosten la Mac, y las dos formas de resolverlo (host.docker.internala mano, o el nombre del servicio con Docker Compose). - Persistencia con volúmenes (
docker volume create/-v), y cómo se declaran redes y volúmenes endocker-compose.yml. - Una introducción a Kubernetes (para cuándo un solo
docker runya no alcanza). - Un stack completo (
orders+products+users+ sus bases) corriendo de verdad con un solodocker compose up.
🗺️ Índice
- 🎯 Qué aprendí
- 📖 PARTE TEÓRICA
- 📚 1. Definiciones clave
- 🐳 2. Introducción a Docker: el problema de partida
- 🖼️ 3. Docker no es una máquina virtual: imagen vs. contenedor
- 🐍 4. Nuestro primer Dockerfile: empaquetando FastAPI
- 🚀 5. Construir y ejecutar FastAPI dentro del contenedor
- 🐘 6. PostgreSQL también puede ser un contenedor
- 🧩 7. Docker Compose: de muchos comandos a una arquitectura declarativa
- 🚢 8. Kubernetes: cuando un solo
docker runya no alcanza
- 💻 PARTE PRÁCTICA
- 🏋️ 11. EJERCICIOS CON SOLUCIÓN
- ❓ Preguntas y respuestas (autoevaluación)
- 📎 Apuntes relacionados
- ➡️ Siguiente
📖 PARTE TEÓRICA
📚 1. Definiciones clave
| Término | Qué es | Se profundiza en |
|---|---|---|
| Docker | Plataforma que empaqueta una aplicación junto con su runtime y todas sus dependencias en una unidad reproducible, para que se comporte igual en cualquier entorno (desarrollo, staging, producción). | sección 2 |
| WSL 2 (Windows Subsystem for Linux 2) | Capa de compatibilidad de Windows que corre un kernel de Linux real dentro de Windows. Docker Desktop para Windows la usa como backend porque los contenedores son procesos de Linux. No aplica en macOS — Docker Desktop para Mac usa el framework de virtualización nativo de macOS en su lugar. | sección 2 |
| Dockerfile | Archivo de texto con la receta: la secuencia de instrucciones para construir una imagen (qué base usar, qué copiar, qué instalar, con qué comando arrancar). | sección 3 |
| Imagen | Plantilla inmutable que resulta de construir un Dockerfile con docker build. No cambia una vez creada; a partir de ella se pueden lanzar tantos contenedores como se quiera. | sección 3 |
| Contenedor | Instancia en ejecución de una imagen (con docker run). Puede haber varios contenedores corriendo al mismo tiempo a partir de la misma imagen, y son desechables: si eliminás uno, la imagen no desaparece. | sección 3 |
docker build | Comando que construye una imagen a partir de un Dockerfile. | sección 3 |
docker run | Comando que crea y arranca un contenedor a partir de una imagen. | sección 3 |
| Docker Hub | Registry (repositorio) público de imágenes Docker en la nube. Con docker push subís una imagen que construiste; con docker pull bajás una imagen ya publicada (propia o de terceros — p. ej. imágenes oficiales como python o postgres). Es la forma más común de distribuir una imagen entre máquinas/entornos sin reconstruirla cada vez. | sección 3 |
FROM | Instrucción del Dockerfile que fija la imagen base sobre la que se construye la nuestra (p. ej. python:3.12-slim). | sección 4 |
WORKDIR | Instrucción que fija la carpeta de trabajo dentro del contenedor; los comandos siguientes se ejecutan relativos a ella. | sección 4 |
COPY | Instrucción que copia archivos del proyecto (fuera del contenedor) hacia adentro de la imagen. | sección 4 |
RUN | Instrucción que ejecuta un comando durante la construcción de la imagen (p. ej. instalar dependencias). | sección 4 |
EXPOSE | Instrucción que documenta qué puerto usa la app dentro del contenedor; no lo publica por sí sola hacia el host. | sección 4 |
CMD | Instrucción que define el comando que se ejecuta al arrancar el contenedor (no durante el build). | sección 4 |
-p host:contenedor | Flag de docker run que mapea un puerto del host a un puerto del contenedor (p. ej. -p 8002:8000). Sin esto, el puerto de EXPOSE no es accesible desde fuera. | sección 5 |
--name | Flag de docker run que le da un nombre fijo al contenedor (en vez de uno aleatorio), para poder referenciarlo después (docker stop products-service, etc.). | sección 5 |
--host 0.0.0.0 vs 127.0.0.1 | Dirección en la que escucha el servidor (Uvicorn). 0.0.0.0 acepta conexiones de cualquier origen (necesario dentro de un contenedor); 127.0.0.1 solo acepta conexiones del propio proceso/máquina — inaccesible desde fuera del contenedor. | sección 5 |
| Contenedor efímero | Un contenedor no guarda cambios de forma permanente: si lo borrás (docker rm), todo lo que escribió en su propio sistema de archivos (por ejemplo, los datos de una base) se pierde con él. | sección 6 |
| Volumen (Docker) | Espacio de almacenamiento gestionado por Docker que vive fuera del contenedor (en el sistema de archivos del host), y que sobrevive aunque el contenedor se borre. Se crea con docker volume create y se monta con -v. | sección 6 |
-v volumen:ruta | Flag de docker run que monta un volumen (o una carpeta del host) dentro del contenedor, en la ruta indicada — lo que se escriba ahí persiste fuera del contenedor. | sección 6 |
| Docker Compose | Herramienta que describe varios contenedores y cómo se relacionan en un único archivo docker-compose.yml, para levantarlos todos con un solo comando en vez de un docker run por cada uno. | sección 7 |
docker-compose.yml | El archivo YAML donde se declara cada service: (imagen o build, puertos, variables de entorno...) — la "receta" de toda la arquitectura local. | sección 7 |
| Red interna de Compose | Red virtual que Compose crea automáticamente para los servicios de un mismo archivo: cada uno es alcanzable por su nombre (DNS interno) y por su puerto interno, sin necesidad de host.docker.internal. | sección 7 |
networks: | Clave de primer nivel en docker-compose.yml que declara una red con nombre (p. ej. orderflow-net); cada servicio la referencia en su propio bloque networks: para unirse a ella. | sección 7 |
driver: bridge | Tipo de red más común de Docker — una red virtual aislada donde los contenedores se ven entre sí por nombre. | sección 7 |
| Kubernetes (K8s) | Plataforma de código abierto (originada en Google) que orquesta contenedores: decide en qué máquina corre cada uno, lo reinicia si se cae, lo escala según demanda y balancea tráfico entre las copias sanas. | sección 8 |
| Clúster | Conjunto de máquinas (nodos) que Kubernetes gestiona como si fueran una sola gran computadora. | sección 8 |
| Nodo (Node) | Una máquina (física o virtual) del clúster, donde corre kubelet + un runtime de contenedores y, arriba, los Pods programados en ella. | sección 8 |
| Pod | La unidad más chica que Kubernetes programa: envuelve uno o más contenedores que siempre corren juntos en el mismo Nodo. | sección 8 |
| Control Plane | El "cerebro" del clúster (API Server, Scheduler, etcd): decide qué Pods crear y en qué Nodo, y mantiene el estado deseado. | sección 8 |
kubectl | CLI para hablar con el Control Plane (kubectl apply, kubectl get pods, ...). | sección 8 |
| Service (Kubernetes) | Objeto que expone un grupo de Pods bajo una dirección estable y balancea tráfico entre ellos — no confundir con "un microservicio" (p. ej. Products Service). | sección 8 |
🐳 2. Introducción a Docker: el problema de partida
¿Por qué no basta con "en mi máquina funciona"?
Para poder ejecutar el proyecto Products en una computadora nueva, hoy hay que reproducir a mano toda esta cadena de dependencias — y si un solo paso falla o difiere de cómo está armada tu máquina, la aplicación no arranca:
text
Cadena actual de instalación (manual, frágil)
1. Python instalado
2. Entorno virtual activo
3. Dependencias instaladas
4. PostgreSQL configurado
5. Base de datos creada
6. Variables de entorno definidas
7. Uvicorn ejecutándose⚠️ Cada uno de estos 7 pasos es un punto de falla: una versión de Python distinta, un
.envque falta, un PostgreSQL que no está corriendo igual que en tu máquina... y el proyecto no levanta, aunque el código sea exactamente el mismo.
La pregunta clave: ¿qué ocurre si entregamos el proyecto a otra persona cuya computadora está configurada de forma diferente?
Docker resuelve exactamente este problema: empaqueta la aplicación, el runtime y todas sus dependencias en una unidad reproducible. Así, el comportamiento es idéntico en cualquier entorno: desarrollo, staging o producción.
💡 Es la misma idea que un
venv(aislar dependencias de Python), pero un nivel más abajo: Docker no solo aísla las librerías de Python, sino todo el sistema que la app necesita (versión de Python, PostgreSQL, variables de entorno, el comando de arranque) en una sola unidad portable.
🗺️ Diagrama: el problema que Docker resuelve

🗺️ Diagrama: mapa de la Sesión 10

🖼️ 3. Docker no es una máquina virtual: imagen vs. contenedor
La distinción entre imagen y contenedor es fundamental para entender Docker:
- Imagen → receta/plantilla inmutable. Se construye una sola vez con
docker build. - Contenedor → instancia ejecutándose. Puede haber varios contenedores de la misma imagen corriendo simultáneamente.
Del Dockerfile al contenedor, el flujo siempre sigue este camino:
text
Dockerfile → docker build → Imagen → docker run → Contenedor
(receta) (cocinar) (plato (servir) (plato ya
preparado) en la mesa)💡 La analogía de la clase: un
Dockerfilees la receta,docker buildes cocinar siguiendo esa receta, la imagen es el plato ya preparado (listo, pero todavía no servido) y el contenedor es ese mismo plato servido en la mesa — la instancia real que alguien se está comiendo. Podés servir (docker run) el mismo plato preparado (imagen) tantas veces como quieras, en tantas mesas (contenedores) como necesites.
📌 Dato clave: si eliminás un contenedor, la imagen no desaparece. Podés crear nuevos contenedores a partir de ella en cualquier momento — es justamente lo que la hace reutilizable y distinta de una máquina virtual (que es pesada y única).
🧪 Tip de entrevista: "¿Cuál es la diferencia entre una imagen y un contenedor en Docker?" → La imagen es una plantilla inmutable (el resultado de
docker build); el contenedor es una instancia en ejecución de esa imagen (creada condocker run). Una imagen puede generar múltiples contenedores al mismo tiempo.
💡 La imagen construida localmente con
docker buildno se distribuye sola. Para llevarla a otra máquina/servidor sin reconstruirla, se sube a un registry como Docker Hub (docker push) y del otro lado se descarga condocker pull— ver glosario, sección 1. Es la pieza que conecta la imagen (el "plato preparado") con la instrucciónFROMdel Dockerfile de la sección 4:FROM python:3.12-slimjustamente descarga esa imagen oficial desde Docker Hub.
🗺️ Diagrama: imagen vs. contenedor

Docker Hub en la práctica: buscando la imagen base de Python
En hub.docker.com se buscan imágenes ya publicadas para usar como base en el FROM del Dockerfile. Buscando python aparecen más de 325.000 resultados — la clave es fijarse en la procedencia de cada una:
| Insignia | Qué significa | ¿Cuándo usarla? |
|---|---|---|
| Docker Official Image | Imagen mantenida y revisada por el equipo de Docker (o el proyecto oficial de ese lenguaje). Es la opción por defecto y la más confiable — la que usa el Dockerfile de la sección 4 (python:3.12-slim). | Casi siempre — es la base recomendada salvo que necesites algo muy específico. |
| Docker Hardened Image | Variante reforzada en seguridad (superficie de ataque reducida). Marcada como Recommended en la búsqueda. | Producción con requisitos de seguridad estrictos. |
Imagen de un tercero (circleci/python, cimg/python, demisto/python, ubuntu/python...) | Publicada por una empresa/comunidad para un propósito puntual (p. ej. imágenes pensadas para correr en CI). | Solo si tu caso de uso coincide con lo que esa imagen resuelve puntualmente — no como base genérica. |
⚠️ No cualquier imagen de la búsqueda es igual de confiable. Antes de poner algo en un
FROM, fijate si tiene la insignia Docker Official Image (o Verified Publisher) — no es lo mismo que una imagen subida por un usuario cualquiera sin mantenimiento.
🧪 Tip de entrevista: "¿Qué diferencia hay entre una Docker Official Image y cualquier otra imagen en Docker Hub?" → La oficial la mantiene el equipo de Docker o el proyecto del lenguaje/herramienta, con builds revisados y actualizados regularmente; el resto son imágenes publicadas por terceros, sin esa garantía de mantenimiento ni de seguridad.
🗺️ Diagrama: Docker Hub — resultados de búsqueda "python"

🐍 4. Nuestro primer Dockerfile: empaquetando FastAPI
Docker lee el Dockerfile de arriba hacia abajo y construye capas de sistema de archivos: cada instrucción genera una nueva capa que Docker guarda en caché, acelerando las construcciones posteriores (si no cambió esa línea ni lo que depende de ella, Docker reutiliza la capa en vez de rehacerla).
Dockerfile completo (para empaquetar un proyecto FastAPI):
dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app",
"--host", "0.0.0.0",
"--port", "8000"]¿Qué hace cada instrucción?
| Instrucción | Qué hace |
|---|---|
FROM python:3.12-slim | Imagen base de Python 3.12 en su variante ligera (slim — menos paquetes de sistema, imagen final más chica). |
WORKDIR /app | Establece /app como carpeta de trabajo dentro del contenedor. |
COPY requirements.txt . + RUN pip install ... | Copia e instala las dependencias antes que el código — aprovecha el caché de capas: si el código cambia pero requirements.txt no, Docker no vuelve a instalar nada. |
COPY app ./app | Recién acá copia el código de la aplicación (va después a propósito, ver callout). |
EXPOSE 8000 | Documenta que la app usa el puerto 8000; no lo publica automáticamente hacia el host (eso lo hace docker run -p). |
CMD [...] | Comando que arranca al iniciar el contenedor (no durante el build): levanta Uvicorn escuchando en 0.0.0.0:8000. |
💡 Por qué el orden importa (no es solo estético):
COPY requirements.txt .yRUN pip installvan antes deCOPY app ./appa propósito. Como Docker cachea capa por capa, si solo cambiás código de la app (no las dependencias), el build reutiliza la capa depip installya hecha y no vuelve a descargar/instalar nada — mucho más rápido que si copiaras todo el proyecto de una y ejecutaraspip installdespués.
⚠️
EXPOSEes documentación, no publica el puerto. Para acceder desde fuera del contenedor hace falta mapear el puerto al correrlo:docker run -p 8000:8000 ...(lo vemos al ejecutar el contenedor en la parte práctica).
🧪 Tip de entrevista: "¿Por qué copiar
requirements.txtantes que el resto del código en un Dockerfile?" → Por el cacheo de capas: separar la instalación de dependencias del copiado del código evita reinstalar todo elrequirements.txtcada vez que cambia una línea de la app, acelerando builds repetidos.
🗺️ Diagrama: Dockerfile completo y qué hace cada instrucción
![Slide "Nuestro primer Dockerfile — Empaquetando FastAPI": explica que Docker lee el Dockerfile de arriba hacia abajo y construye capas de sistema de archivos, cada instrucción genera una nueva capa que se almacena en caché. A la izquierda, el Dockerfile completo (FROM python:3.12-slim, WORKDIR /app, COPY requirements.txt ., RUN pip install --no-cache-dir -r requirements.txt, COPY app ./app, EXPOSE 8000, CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]); a la derecha, seis tarjetas explicando qué hace cada instrucción (FROM: imagen base de Python 3.12 en su variante ligera; WORKDIR: establece /app como carpeta de trabajo dentro del contenedor; COPY + RUN: copia e instala dependencias antes que el código, aprovecha el caché; EXPOSE: documenta el puerto 8000, no lo publica automáticamente; CMD: comando que arranca al iniciar el contenedor)](/clase-10-primer-dockerfile-fastapi.png)
📌 Este Dockerfile es el que mostró el profe en la diapositiva. Ya se copió a
02-Ejercicios/Clase-10/orders_service/Dockerfile(mismoorders_serviceque la Clase 7) — de ahí en más,docker build/docker runse corren de verdad ahí y el resultado real se documenta en la parte práctica de abajo.
🚀 5. Construir y ejecutar FastAPI dentro del contenedor
Con el Dockerfile listo, los dos comandos esenciales son:
bash
# Construir la imagen (nombre:tag)
docker build -t orderflow-products:1.0 .
# Ejecutar el contenedor
docker run --name products-service \
-p 8002:8000 \
orderflow-products:1.0Mapeo de puertos: localhost:8002 → :8000 (FastAPI, adentro del contenedor).
💡
-p 8002:8000significa puerto del host : puerto del contenedor. El contenedor siempre usa el 8000 internamente (es lo que declaróEXPOSE 8000y lo que escucha Uvicorn); nosotros decidimos exponerlo como el 8002 en nuestra máquina. Podrías correr varios contenedores de la misma imagen mapeando cada uno a un puerto de host distinto (8002, 8003, 8004...) sin que choquen entre sí.
Punto crítico: la dirección de escucha (--host)
| Flag | Resultado | |
|---|---|---|
| ✅ Correcto | --host 0.0.0.0 | Acepta conexiones desde fuera del contenedor (desde el host, u otro contenedor). |
| ❌ Incorrecto | --host 127.0.0.1 | Solo escucha dentro del propio contenedor — inaccesible desde el host, aunque el -p esté bien mapeado. |
⚠️ Este es el error más común al contenerizar FastAPI: dejar
--host 127.0.0.1(el default de Uvicorn en desarrollo local) hace que el-p 8002:8000no sirva de nada — el proceso ni siquiera acepta conexiones que le lleguen desde fuera del contenedor. Por eso elCMDde la sección 4 usa explícitamente--host 0.0.0.0.
🧪 Tip de entrevista: "Mapeaste el puerto con
-p 8002:8000pero no podés acceder a la API desde el navegador. ¿Qué revisás primero?" → Que el proceso dentro del contenedor esté escuchando en0.0.0.0y no en127.0.0.1— el mapeo de puertos no ayuda si la app solo acepta conexiones locales al propio contenedor.
🗺️ Diagrama: build, run y la dirección de escucha correcta

🐘 6. PostgreSQL también puede ser un contenedor
No hace falta instalar PostgreSQL en la máquina — podemos ejecutarlo como contenedor, igual que a orders_service. Pero eso trae un problema nuevo: la persistencia de datos.
Ejecutar la base de datos:
bash
docker run --name products-db \
-e POSTGRES_USER=orderflow \
-e POSTGRES_PASSWORD=orderflow123 \
-e POSTGRES_DB=orderflow_products \
-p 5433:5432 \
-d postgres:17-alpine📌 Nota de puertos: acá el host usa
5433(no5432) porque en esta máquina ya puede estar corriendo elbd_test_backendde la Clase 4/Clase 7 ocupando el5432— dos contenedores no pueden publicar el mismo puerto del host a la vez. Ver Comandos → Docker.
El problema: los contenedores son efímeros. Si eliminamos el contenedor, los datos desaparecen — se van con él. La solución es usar un volumen, que persiste en el sistema de archivos del host, no en el del contenedor:
bash
docker volume create products_data
docker run --name products-db \
-e POSTGRES_USER=orderflow \
-e POSTGRES_PASSWORD=orderflow123 \
-e POSTGRES_DB=orderflow_products \
-p 5433:5432 \
-v products_data:/var/lib/postgresql/data \
-d postgres:17-alpine| Comando/flag | Qué hace |
|---|---|
docker volume create products_data | Crea un volumen gestionado por Docker (vive fuera de cualquier contenedor, en el host). |
-v products_data:/var/lib/postgresql/data | Monta ese volumen en /var/lib/postgresql/data — la ruta donde Postgres guarda sus archivos de datos adentro del contenedor. Lo que Postgres escriba ahí en realidad queda en el volumen, no en el contenedor. |
💡 Una aplicación se puede reconstruir. La información de negocio almacenada en la base de datos no debería depender del filesystem interno del contenedor — si el contenedor de
products-dbse borra y se crea de nuevo (misma imagen, mismo volumen), los datos siguen ahí. Sin el-v, sería empezar de cero cada vez.
🧪 Tip de entrevista: "¿Por qué un contenedor de base de datos necesita un volumen?" → Porque el filesystem de un contenedor se borra junto con él (
docker rm). Un volumen vive fuera del contenedor, en el host, así que los datos sobreviven aunque el contenedor se recree.
🗺️ Diagrama: contenedor efímero + volumen que persiste

📝 Decisión propia — no creé el
products-dbde arriba. Verifiqué condocker inspect/psqlque mibd_test_backend(el mismo Postgres de la Clase 4/Clase 7) ya tiene la baseorderflow_products_dbcreada, y ya tiene un volumen — Docker se lo crea solo (aunque eldocker runoriginal no llevaba-v) porque la imagen oficial de Postgres declaraVOLUME /var/lib/postgresql/data; solo que es anónimo (sin nombre fácil) en vez de nombrado comoproducts_data. Reutilizarlo es válido y más simple que levantar un segundo Postgres — la única razón para seguir el ejercicio del profe tal cual sería practicardocker volume createa propósito (un volumen nombrado se gestiona más fácil:docker volume ls,docker volume inspect). Con esto, el.envde un servicio que use esta base queda:DATABASE_URL=postgresql+psycopg://postgres:postgres@host.docker.internal:5432/orderflow_products_db
🧩 7. Docker Compose: de muchos comandos a una arquitectura declarativa
En vez de recordar y ejecutar cada docker run a mano (uno por orders-service, otro por orders-db, otro por products-service, otro por products-db...), Compose describe toda la arquitectura en un único archivo YAML. Con un solo comando se levantan todos los servicios.
docker-compose.yml (fragmento):
yaml
services:
products-db:
image: postgres:17-alpine
products-service:
build:
context: ./products_service
orders-db:
image: postgres:17-alpine
orders-service:
build:
context: ./orders_service| Clave | Qué es |
|---|---|
services: | La lista de todo lo que Compose va a levantar — cada entrada es un contenedor. |
image: postgres:17-alpine | Igual que en docker run — usa una imagen ya publicada (Docker Hub). |
build: context: ./orders_service | En vez de una imagen ya hecha, Compose construye la imagen desde ese Dockerfile (equivalente a pararse en esa carpeta y correr docker build). |
El archivo docker-compose.yml se convierte en la representación ejecutable de la arquitectura local del proyecto — cuatro servicios, uno declarado al lado del otro, en vez de cuatro comandos sueltos que hay que recordar en orden.
🗺️ Diagrama: docker-compose.yml como nodo central

El error más común: dentro de Docker, localhost significa "yo mismo"
bash
# ❌ Configuración incorrecta
PRODUCTS_URL=http://localhost:8002Dentro del contenedor orders-service, localhost apunta al propio contenedor de Orders, no al de Products — es el mismo problema que ya vimos con Postgres, pero entre dos contenedores de la app. La solicitud nunca llega a destino.
bash
# ✅ Configuración correcta con Compose
PRODUCTS_URL=http://products-service:8000Compose crea una red DNS interna: cada servicio es accesible por su nombre (products-service, no una IP ni host.docker.internal). Y se usa el puerto interno (8000), no el publicado al host.
| Puerto | Qué es | ¿Quién lo usa? |
|---|---|---|
8002 (publicado) | Expuesto al host (tu Mac). | Solo útil desde fuera de la red de Docker (vos, con curl/navegador). |
8000 (interno) | El puerto real donde escucha Uvicorn, siempre el mismo. | Lo que usan los servicios entre sí, dentro de la red de Compose. |
⚠️ Este es el error más frecuente al empezar con Docker. Entre contenedores del mismo
docker-compose.yml, siempre se usa el nombre del servicio y el puerto interno — nuncalocalhost, nunca el puerto publicado, y (a diferencia de lo que hicimos a mano en la sección 9) tampoco hace faltahost.docker.internalpara esto: ese nombre es para llegar a algo que corre fuera de la red de Docker (en tu Mac, comobd_test_backend); para hablar entre servicios de un mismo Compose, el nombre del servicio ya alcanza.
🗺️ Diagrama: localhost vs. nombre del servicio (con Compose)

🗺️ Diagrama: cómo se vinculan los servicios de verdad (red interna de Compose)

Redes y volúmenes: cómo se declaran de verdad en el YAML
La red y el volumen del diagrama de arriba no aparecen solos por magia — se declaran explícitamente en docker-compose.yml, con dos claves de primer nivel (hermanas de services:):
yaml
networks:
orderflow-net:
driver: bridge
volumes:
products_data:
orders_data:Y cada servicio que las necesita las referencia por nombre:
yaml
services:
products-service:
networks:
- orderflow-net
volumes:
- products_data:/var/lib/postgresql/data| Recurso | ¿Para qué sirve? |
|---|---|
networks: | Permite que orders-service encuentre a products-service por nombre. Sin una red compartida, los contenedores quedan aislados entre sí (ver diagrama de arriba). |
volumes: | Guarda los datos de PostgreSQL en el host. Aunque el contenedor se elimine y se recree, los datos permanecen intactos (mismo concepto de la sección 6, ahora declarado en Compose en vez de con docker volume create + -v sueltos). |
💡 Es la misma idea de las secciones 6 y 7 (volumen para persistencia, nombre de servicio para comunicación) — la diferencia es que acá no hay que acordarse de ningún flag: Compose lee el YAML y arma la red y los volúmenes solo, con el nombre que vos les diste (
orderflow-net,products_data,orders_data).
🗺️ Diagrama: redes y volúmenes declarados en el YAML

🚢 8. Kubernetes: cuando un solo docker run ya no alcanza
📝 El profe mencionó Kubernetes de pasada (no hubo diapositiva propia para esto) — esta sección es un resumen verificado (no transcripción literal de la clase) para tener la definición clara y una imagen mental de cómo funciona.
¿Qué es Kubernetes? Es una plataforma de código abierto (originada en Google) para orquestar contenedores: en vez de que vos corras docker run a mano en cada máquina, Kubernetes decide en qué máquina corre cada contenedor, lo reinicia solo si se cae, lo escala según demanda y reparte el tráfico entre las copias que estén sanas — todo esto sobre un clúster (varias máquinas trabajando como si fueran una sola).
¿Por qué hace falta si ya tenemos Docker? Docker (lo que vimos en toda esta clase) sabe construir y correr un contenedor en una máquina. Eso alcanza para desarrollo y para un servicio chico. En producción real hace falta: varias copias del mismo contenedor repartidas en varias máquinas, que si una máquina se cae los contenedores se reprogramen en otra, que si sube el tráfico se creen más copias solas, y que las actualizaciones se hagan sin downtime — eso es lo que Kubernetes orquesta por vos.
Con Docker solo (docker run) | Con Kubernetes | |
|---|---|---|
| ¿Cuántas máquinas? | Una, la que corriste el comando | Un clúster (varias) |
| ¿Si el contenedor se cae? | Se queda caído — reiniciás a mano (docker start) | Se reprograma solo en otro nodo sano |
| ¿Si sube el tráfico? | Nada automático | Escala el número de réplicas (autoscaling) |
| ¿Cómo se actualiza? | Parás y levantás el contenedor a mano | Rolling update — reemplaza de a poco, sin downtime |
🧪 Tip de entrevista: "¿Cuál es la diferencia entre Docker y Kubernetes?" → No compiten, se complementan: Docker construye y corre contenedores individuales; Kubernetes orquesta muchos contenedores (típicamente creados con Docker) a través de muchas máquinas — los reinicia, los escala y balancea tráfico entre ellos.
🗺️ Diagrama: cómo funciona un clúster de Kubernetes

💻 PARTE PRÁCTICA
🐳 9. orders_service contenerizado: build + run reales
Archivo real: 02-Ejercicios/Clase-10/orders_service (el mismo microservicio de la Clase 7, copiado tal cual — sin venv/ ni .env reales — y con su propio Dockerfile de la sección 4). Pasos reales, verificados en terminal:
1. Ubicate en la carpeta del proyecto:
bash
cd /Users/styp/Documents/Cursos/Python_para_backend/02-Ejercicios/Clase-10/orders_service2. Construí la imagen (solo hace falta repetir esto si cambiás código o requirements.txt):
bash
docker build -t orders_service:1.0 .3. Corré el contenedor:
bash
docker run -d --name orders-service \
-p 8001:8000 \
--add-host=host.docker.internal:host-gateway \
--env-file .env \
orders_service:1.0-d→ en segundo plano (detached).-p 8001:8000→ tu máquina8001→ contenedor8000.--add-host=host.docker.internal:host-gateway→ fuerza quehost.docker.internalresuelva a la IP del host (ver el🐛de abajo — sin esto no resolvió solo).--env-file .env→ le pasa las variables de entorno (DATABASE_URL,PRODUCTS_URL,JWT_SECRET) sin hornearlas en la imagen.
💡 El
.env(a partir de.env.example, no versionado) tieneDATABASE_URLyPRODUCTS_URLapuntando ahost.docker.internalen vez delocalhost— es la forma en que un contenedor en Docker Desktop para Mac llega a servicios corriendo en tu propia máquina (Postgres, Products Service). Ver Comandos → Clase 10.
🐛 Error real que salió sin el
--add-host: probando la conexión a la base desde adentro del contenedor (psycopg.connect(...)), tirófailed to resolve host 'host.docker.internal': Name or service not known— confirmado condocker exec orders-service cat /etc/hosts(el nombre ni aparecía ahí). Recrear el contenedor con--add-host=host.docker.internal:host-gatewaylo resolvió al toque.
🗺️ Diagrama: 3 capas de red — contenedor, Mac, red externa
127.0.0.1, la IP y el puerto significan algo distinto según en qué anillo de red estés parado — es la raíz del bug de arriba:

4. Verificá que levantó:
bash
docker ps # debería listar "orders-service" como Up
docker logs orders-service # ver el log de arranque de Uvicorn
curl http://localhost:8001/health # {"status":"ok","service":"Orders Service","version":"1.0.0"}Para pararlo/limpiarlo cuando termines:
bash
docker stop orders-service
docker rm orders-service⚠️ El
/healthfunciona sin base de datos real. Si probás un endpoint que sí consulta Postgres (/api/v1/orders), va a fallar salvo que tengas un Postgres corriendo y accesible enhost.docker.internal:5432— eso lo resuelve mejor Docker Compose (siguiente tema de la clase), que conecta varios contenedores entre sí sin depender dehost.docker.internal.
🗺️ Diagrama: qué pasa al hacer docker build + docker run

🗺️ Diagrama: por qué host.docker.internal y no localhost

🧩 10. El stack completo con Docker Compose (orders + products + users)
Mismo patrón que orders_service (sección 9): se copiaron products_service y users_service de la Clase 7 a 02-Ejercicios/Clase-10 (sin venv/ ni .env reales — la Clase 7 queda intacta, es una copia). A cada uno se le agregó su Dockerfile (idéntico al de la sección 4), y un docker-compose.yml en la raíz de Clase-10 los levanta a los tres de una:
yaml
networks:
orderflow-net:
driver: bridge
volumes:
orders_data:
products_data:
users_data:
services:
orders-db:
image: postgres:17-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: orderflow_orders_db
volumes:
- orders_data:/var/lib/postgresql/data
networks: [orderflow-net]
orders-service:
build:
context: ./orders_service
env_file: [./orders_service/.env]
depends_on: [orders-db, products-service]
ports: ["8001:8000"]
networks: [orderflow-net]
# products-db / products-service y users-db / users-service: mismo patrón,
# puertos 8002 y 8003 — ver el archivo completo en el repo.Cada .env apunta a su base por el nombre del servicio, no por host.docker.internal (eso ya no hace falta adentro de la red de Compose — sección 7):
bash
# orders_service/.env
DATABASE_URL=postgresql+psycopg://postgres:postgres@orders-db:5432/orderflow_orders_db
PRODUCTS_URL=http://products-service:8000Levantar todo:
bash
cd 02-Ejercicios/Clase-10
docker compose up -d --buildSalida verificada:
bash
$ docker compose ps
NAME SERVICE STATUS PORTS
clase-10-orders-service-1 orders-service Up 0.0.0.0:8001->8000/tcp
clase-10-products-service-1 products-service Up 0.0.0.0:8002->8000/tcp
clase-10-users-service-1 users-service Up 0.0.0.0:8003->8000/tcp
clase-10-orders-db-1 orders-db Up 5432/tcp
clase-10-products-db-1 products-db Up 5432/tcp
clase-10-users-db-1 users-db Up 5432/tcp
$ curl http://localhost:8001/health
{"status":"ok","service":"Orders Service","version":"1.0.0"}
$ curl http://localhost:8002/health
{"status":"ok","service":"Products Service","version":"1.0.0"}
$ curl http://localhost:8003/health
{"status":"Felicitaciones! El backend esta UP","service":"Users Service","version":"1.0.0"}La prueba que importa — orders-service llegando a products-service por nombre, desde adentro del contenedor:
bash
$ docker exec clase-10-orders-service-1 python -c \
"import httpx; print(httpx.get('http://products-service:8000/health', timeout=3).json())"
{'status': 'ok', 'service': 'Products Service', 'version': '1.0.0'}🐛 Bug real encontrado y corregido: las bases que crea Compose son nuevas y vacías (distintas de
bd_test_backend), así que hacía falta correr las migraciones de Alembic contra ellas. Al intentarlo,docker compose exec orders-service alembic upgrade headtiróFAILED: No 'script_location' key found in configuration— elDockerfile(igual al de la sección 4, pensado solo para servir la API) copiabaapp/pero noalembic.ininimigrations/, así que esos archivos ni existían dentro del contenedor. Se agregaron dos líneas alDockerfilede los tres servicios:dockerfileCOPY app ./app COPY alembic.ini . COPY migrations ./migrationsReconstruyendo (
docker compose up -d --build) y corriendodocker compose exec <servicio> alembic upgrade headen los tres, las tablas (orders,products,users) quedaron creadas — verificado con\dten cada base.
💡 Por qué
depends_onno alcanza solo:depends_on: [orders-db]hace que Compose arranqueorders-dbantes queorders-service, pero no espera a que Postgres esté realmente listo para aceptar conexiones — en un proyecto real conviene sumar unhealthchecka la base ycondition: service_healthyen eldepends_on(no lo necesitamos acá porquecreate_enginede SQLAlchemy es perezoso: no conecta hasta la primera query).
🗺️ Diagrama: entornos y redes reales en esta máquina, ahora mismo
No es un diagrama de ejemplo — son las IPs y puertos reales de esta máquina, verificados con docker network inspect, mostrando algo importante: hay dos redes de Docker aisladas corriendo al mismo tiempo, y no se ven entre sí:

📌 Que
bd_test_backendy el stack de Compose usen el mismo número de puerto interno (5432) no es un problema — viven en redes distintas (172.17.xvs172.22.x), así que no chocan entre sí. Lo que sí importa es que los puertos publicados al host sean todos distintos (5432,8001,8002,8003) — esos comparten el mismo espacio de direcciones: el de tu Mac.
🏋️ 11. EJERCICIOS CON SOLUCIÓN
Todos los comandos y salidas de esta sección se verificaron en terminal contra el stack real de
02-Ejercicios/Clase-10— no hay salidas inventadas.
Ejercicio 1 — Construir una imagen con otro tag
Construí la imagen de products_service con el tag 2.0 en vez de 1.0.
🎯 Qué deberías lograr: docker images debe listar una fila products_service con TAG igual a 2.0.
💡 ¿Sabías que…? — un tag es solo una etiqueta
Un mismo build puede tener varios tags apuntando al mismo IMAGE ID — no son copias, son nombres distintos para lo mismo. Por ejemplo, después de construir mi_api:1.0 podés agregarle otro tag sin reconstruir nada:
bash
docker tag mi_api:1.0 mi_api:latest
docker images | grep mi_api # dos filas, mismo IMAGE IDVer solución
bash
cd 02-Ejercicios/Clase-10
docker build -t products_service:2.0 ./products_service
docker images | grep products_serviceSalida real:
products_service 2.0 f571c5554e4b 31 minutes ago 386MBEjercicio 2 — Correr esa imagen en otro puerto
Corré un contenedor de products_service:2.0, publicando el puerto 8010 de tu Mac, usando el .env real de products_service.
🎯 Qué deberías lograr: curl http://localhost:8010/health responde 200 con {"status":"ok","service":"Products Service",...}.
💡 ¿Sabías que…? — podés correr la misma imagen en paralelo, en puertos distintos
Nada te impide levantar 2 o más contenedores de la misma imagen, cada uno publicado en un puerto distinto del host — por ejemplo, para probar dos versiones a la vez:
bash
docker run -d --name api-v1 -p 9001:8000 mi_api:1.0
docker run -d --name api-v2 -p 9002:8000 mi_api:2.0Ver solución
bash
docker run -d --name test-products-v2 -p 8010:8000 \
--env-file ./products_service/.env products_service:2.0
curl http://localhost:8010/healthSalida real:
{"status":"ok","service":"Products Service","version":"1.0.0"}Ejercicio 3 — Leer los logs de arranque
Con el contenedor del ejercicio 2 corriendo, mirá sus logs y encontrá la línea donde Uvicorn dice en qué dirección está escuchando.
🎯 Qué deberías lograr: ubicar la línea Uvicorn running on http://0.0.0.0:8000.
💡 ¿Sabías que…? — docker logs -f sigue el log en vivo
Sin -f, docker logs muestra lo que ya se imprimió y termina. Con -f (follow) se queda mostrando líneas nuevas a medida que aparecen — útil mientras hacés requests de prueba en otra terminal:
bash
docker logs -f mi_contenedorCtrl+C corta el seguimiento (no detiene el contenedor).
Ver solución
bash
docker logs test-products-v2 --tail 6Salida real:
INFO: Started server process [1]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)Ejercicio 4 — Limpiar el contenedor de prueba
Apagá y borrá test-products-v2.
🎯 Qué deberías lograr: docker ps -a ya no debe listar test-products-v2.
💡 ¿Sabías que…? — docker rm -f hace las dos cosas de una
docker stop x && docker rm x son dos pasos porque rm normalmente se niega a borrar un contenedor corriendo. docker rm -f x fuerza el apagado y el borrado en un solo comando (útil en scripts, pero pensalo dos veces en algo importante).
Ver solución
bash
docker stop test-products-v2 && docker rm test-products-v2Ejercicio 5 — slim vs. imagen completa
En una copia de prueba del Dockerfile de users_service, cambiá FROM python:3.12-slim por FROM python:3.12 (sin slim), reconstruí con otro tag y comparé el tamaño final con docker images.
🎯 Qué deberías lograr: confirmar que la imagen completa pesa notablemente más que la slim — no hace falta que el número te dé exacto, sí que veas la diferencia.
💡 ¿Sabías que…? — slim no trae herramientas de compilación
python:X-slim quita compiladores y librerías de desarrollo que muchas apps no necesitan en producción (solo en el momento de instalar dependencias con extensiones en C). Ejemplo de referencia con otra imagen base:
bash
docker pull node:20-slim
docker pull node:20
docker images | grep node # la slim pesa una fracción de la completaVer solución
bash
# Dockerfile.full: mismo contenido que el real, con FROM python:3.12 (sin slim)
docker build -f Dockerfile.full -t users_service:full ./users_service
docker images | grep users_serviceSalida real verificada:
users_service full 9c6ee1b5d6cc 1.8GB
users_service latest f27f499c795d 409MBCasi 4.5× más pesada sin el -slim — para una API que solo necesita el intérprete de Python y las dependencias ya compiladas (pip install con wheels), esos ~1.4GB extra no aportan nada.
Ejercicio 6 — Predecir el cacheo de capas
Te muestran un Dockerfile donde alguien puso COPY app ./app antes de COPY requirements.txt . / RUN pip install. Explicá qué pasa con el caché de Docker cada vez que cambia una línea de código de la app (sin tocar requirements.txt).
🎯 Qué deberías lograr: identificar que, con ese orden invertido, todas las capas después de COPY app — incluido pip install — se invalidan y se re-ejecutan en cada build, aunque las dependencias no cambiaron.
💡 ¿Sabías que…? — Docker invalida en cascada, no una capa suelta
En cuanto una capa cambia, Docker reconstruye esa y todas las que siguen — no puede "saltear" una capa del medio. Ejemplo de referencia con otro Dockerfile mal ordenado:
dockerfile
FROM node:20-slim
WORKDIR /app
COPY . . # copia TODO primero (mal)
RUN npm install # se re-ejecuta con cualquier cambio de códigoVer solución
Orden correcto (el real, de la sección 4):
dockerfile
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./appAsí, si solo cambia app/, Docker reutiliza la capa de pip install ya hecha (cache hit) y el build es mucho más rápido.
Ejercicio 7 — Levantar orders_service standalone
Repetí el proceso de la sección 9 para levantar orders-service solo (sin Compose), publicando el puerto 8001.
🎯 Qué deberías lograr: curl http://localhost:8001/health → 200.
💡 ¿Sabías que…? — un mismo servicio puede correr "solo" o "en equipo"
Nada en el código de orders_service sabe si lo estás corriendo standalone o dentro de un docker-compose.yml — la diferencia la da únicamente el .env (a qué host apunta DATABASE_URL). Mismo Dockerfile, mismo build, distinto contexto de red.
Ver solución
bash
cd 02-Ejercicios/Clase-10/orders_service
docker build -t orders_service:1.0 .
docker run -d --name orders-service -p 8001:8000 \
--add-host=host.docker.internal:host-gateway \
--env-file .env orders_service:1.0
curl http://localhost:8001/healthEjercicio 8 — Reproducir (o no) el bug de host.docker.internal
Corré ese mismo contenedor sin el flag --add-host y probá conectarte a la base desde adentro.
🎯 Qué deberías lograr: en el setup real de esta clase, la conexión falló con failed to resolve host 'host.docker.internal'. (Este comportamiento puede variar según tu versión de Docker Desktop — si en tu máquina resuelve solo, no hay nada que arreglar en tu caso, pero vale la pena confirmarlo en vez de asumirlo.)
💡 ¿Sabías que…? — /etc/hosts es la fuente de verdad
Cuando algo "no resuelve un nombre", /etc/hosts dentro del contenedor es el primer lugar donde mirar — ahí se ve literalmente qué nombres conoce ese contenedor:
bash
docker exec mi_contenedor cat /etc/hostsVer solución
bash
docker run -d --name orders-service -p 8001:8000 --env-file .env orders_service:1.0
docker exec orders-service python -c "
import psycopg
from app.config import settings
psycopg.connect(settings.database_url.replace('+psycopg',''), connect_timeout=3)
"Salida real (sección 9): failed to resolve host 'host.docker.internal': Name or service not known.
Ejercicio 9 — Corregir con --add-host
Recreá el contenedor con --add-host=host.docker.internal:host-gateway y confirmá que la conexión funciona.
🎯 Qué deberías lograr: el script de conexión imprime CONEXION OK.
💡 ¿Sabías que…? — --add-host edita /etc/hosts por vos
Es literalmente un atajo para agregar una línea a /etc/hosts del contenedor al crearlo — host-gateway es una palabra especial que Docker traduce a la IP real del host:
bash
docker run --add-host=api.interno:host-gateway ...Ver solución
bash
docker stop orders-service && docker rm orders-service
docker run -d --name orders-service -p 8001:8000 \
--add-host=host.docker.internal:host-gateway \
--env-file .env orders_service:1.0Salida real: CONEXION OK: host.docker.internal 5432 orderflow_orders_db.
Ejercicio 10 — Inspeccionar los volúmenes del stack
Con el stack de Compose levantado, listá los volúmenes creados y mostrá el punto de montaje real en el host de orders_data.
🎯 Qué deberías lograr: docker volume ls lista clase-10_orders_data, clase-10_products_data, clase-10_users_data; docker volume inspect de cualquiera de ellos muestra un Mountpoint real en disco (dentro de la VM de Docker Desktop).
💡 ¿Sabías que…? — un volumen puede sobrevivir a docker compose down
docker compose down (sin -v) borra contenedores y redes, pero deja los volúmenes — por eso las tablas siguen ahí la próxima vez que hacés docker compose up. Solo docker compose down -v los borra también.
Ver solución
bash
docker volume ls --filter name=clase-10
docker volume inspect clase-10_orders_data --format '{{.Mountpoint}}'Ejercicio 11 — Publicado vs. interno
Dado -p 8005:8000: ¿qué número usa un curl desde tu Mac? ¿Y qué número usa otro contenedor de la misma red de Compose para llegar a este servicio?
🎯 Qué deberías lograr: responder 8005 para el curl desde el Mac, y 8000 (vía el nombre del servicio) para otro contenedor — nunca al revés.
💡 ¿Sabías que…? — el puerto publicado no existe "adentro"
El proceso dentro del contenedor no sabe en qué puerto lo publicaste — ni falta que le haga. Ejemplo de referencia: si publicás -p 9999:8000, un curl desde adentro del propio contenedor a localhost:9999 falla — adentro, el servicio solo existe en :8000.
Ver solución
- Desde el Mac:
curl http://localhost:8005/health - Desde otro contenedor de la red:
curl http://ese-servicio:8000/health
Ejercicio 12 — Corregir un PRODUCTS_URL mal configurado
Te pasan un .env de orders_service con PRODUCTS_URL=http://localhost:8002. Corregilo para que funcione dentro de la red de Compose.
🎯 Qué deberías lograr: el valor corregido debe apuntar al nombre del servicio y al puerto interno.
💡 ¿Sabías que…? — el mismo error, con otro par de servicios
Es exactamente el error de la sección 7: dentro de orders-service, localhost es el propio orders-service, nunca products-service — pase lo que pase con el nombre de las variables.
Ver solución
bash
PRODUCTS_URL=http://products-service:8000Ejercicio 13 — Escribir networks:/volumes: para un servicio nuevo
Un servicio ficticio reports-service necesita su propia base reports-db con volumen reports_data. Escribí el fragmento YAML: las claves de primer nivel networks:/volumes: y el bloque reports-db.
🎯 Qué deberías lograr: el YAML debe ser válido — docker compose config -q no debe tirar error.
💡 ¿Sabías que…? — un volumen nombrado no necesita más configuración
Declarar volumes: reports_data: (sin nada debajo) alcanza — Docker usa el driver local por defecto. Solo hace falta más configuración (driver:, driver_opts:) para casos especiales (NFS, volúmenes remotos).
Ver solución
yaml
volumes:
reports_data:
services:
reports-db:
image: postgres:17-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: orderflow_reports_db
volumes:
- reports_data:/var/lib/postgresql/data
networks:
- orderflow-netEjercicio 14 — Agregar pgAdmin al stack
Sumá un servicio pgadmin (imagen dpage/pgadmin4) al docker-compose.yml, en la misma red, publicado en el puerto 5050.
🎯 Qué deberías lograr: docker compose up -d pgadmin seguido de curl -I http://localhost:5050 debe responder 302 (redirect a la pantalla de login) — significa que el servicio arrancó bien.
💡 ¿Sabías que…? — pgAdmin valida el formato del email
PGADMIN_DEFAULT_EMAIL tiene que parecer un email real — dominios como .local o .test lo rechazan al arrancar (does not appear to be a valid email address), y el contenedor no expone nada hasta que ese chequeo pasa.
Ver solución
yaml
pgadmin:
image: dpage/pgadmin4:latest
environment:
PGADMIN_DEFAULT_EMAIL: admin@orderflow.dev
PGADMIN_DEFAULT_PASSWORD: admin123
ports:
- "5050:80"
networks:
- orderflow-netVerificado: admin@orderflow.local fue rechazado por pgAdmin al arrancar; admin@orderflow.dev funcionó (curl -I → 302).
Ejercicio 15 — Leer docker compose logs
Con el stack arriba, corré docker compose logs orders-service --tail 10 e identificá si hubo algún error de arranque.
🎯 Qué deberías lograr: ubicar las líneas Application startup complete y Uvicorn running on http://0.0.0.0:8000 — señal de que arrancó bien.
💡 ¿Sabías que…? — compose logs junta todos los servicios si no especificás uno
docker compose logs (sin nombre) muestra el log de todos los servicios intercalado, cada línea prefijada con el nombre del servicio — útil para ver el orden real de arranque de todo el stack.
Ver solución
bash
docker compose logs orders-service --tail 10Ejercicio 16 — Migrar una base fresca
Bajá el stack con docker compose down -v (borra también los volúmenes) y volvé a levantarlo; corré Alembic en los 3 servicios para recrear las tablas.
🎯 Qué deberías lograr: \dt en cada base debe mostrar de nuevo alembic_version + la tabla del dominio (orders, products, users).
💡 ¿Sabías que…? — down -v es la limpieza total
Sin -v, docker compose down deja los volúmenes (los datos sobreviven). Con -v, los borra — es la opción a usar cuando de verdad querés empezar de cero, no la que corrés "por las dudas" todos los días.
Ver solución
bash
docker compose down -v
docker compose up -d --build
docker compose exec orders-service alembic upgrade head
docker compose exec products-service alembic upgrade head
docker compose exec users-service alembic upgrade headEjercicio 17 — Dos redes, mismo puerto interno
Explicá por qué bd_test_backend (red bridge) y orders-db (red clase-10_orderflow-net) pueden usar el mismo puerto interno 5432 sin chocar entre sí.
🎯 Qué deberías lograr: explicar que viven en namespaces de red distintos — con IPs reales de evidencia.
💡 ¿Sabías que…? — es como dos casas con la misma numeración de puertas
Que dos redes usen el mismo puerto interno no es una coincidencia rara — cada red de Docker es su propio espacio aislado, así que "puerto 5432 de la red A" y "puerto 5432 de la red B" son cosas completamente distintas, igual que el "cuarto 5" puede existir en dos casas diferentes sin confundirse.
Ver solución
bd_test_backend → 172.17.0.3 (red bridge, 172.17.0.0/16). orders-db → 172.22.0.2 (red clase-10_orderflow-net, 172.22.0.0/16). Rangos de IP distintos → aunque ambos escuchen en :5432, nunca se ven ni chocan entre sí (ver diagrama de la sección 10).
Ejercicio 18 — ¿Reusar o separar bases?
Con el criterio de las secciones 6 y 10: un servicio de "Reportes" que solo lee datos de orders-db y products-db (no escribe) — ¿le conviene tener su propia base, o conectarse directo a las existentes?
🎯 Qué deberías lograr: justificar tu respuesta con el principio database per service, no solo dar una opinión.
💡 ¿Sabías que…? — leer no es lo mismo que ser dueño del esquema
Database per service protege sobre todo la escritura: que un servicio no dependa del esquema interno de otro. Un ejemplo de referencia: un dashboard de métricas que consulta 5 microservicios distintos — casi siempre lo hace llamando a un endpoint /metrics de cada uno, no leyendo sus tablas directo.
Ver solución
Preferible que "Reportes" consuma la API de orders-service y products-service (como hace orders-service con products-service en la sección 7), no que se conecte directo a orders-db/products-db. Conectarse directo a la base ajena acopla a "Reportes" al esquema interno de esos servicios — si mañana cambian una columna, "Reportes" se rompe sin que su propio equipo lo haya tocado.
Ejercicio 19 — El camino completo de una request
Con tus palabras (o un diagrama ASCII propio), describí el camino completo de curl http://localhost:8001/api/v1/orders hasta que llega a la tabla orders en Postgres, nombrando cada "anillo" que atraviesa.
🎯 Qué deberías lograr: mencionar al menos 4 pasos: Mac (puerto publicado 8001) → contenedor orders-service (puerto interno 8000) → orders-db (por nombre de servicio, puerto 5432) → la tabla orders en esa base.
💡 ¿Sabías que…? — cada flecha de los diagramas de esta clase es un paso real
No es casualidad que la Clase 10 tenga tantos diagramas de "capas" — cada uno (sección 9: Contenedor/Mac/Red externa; sección 10: dos redes aisladas) es literalmente el mapa de un tramo distinto de este mismo camino.
Ver solución
text
curl (tu Mac)
→ localhost:8001 [puerto publicado]
→ contenedor orders-service :8000 [puerto interno, Uvicorn]
→ orders-db:5432 [por nombre de servicio, red de Compose]
→ tabla "orders" (Postgres)Ejercicio 20 — Reto final: healthcheck + condition: service_healthy
Agregá un healthcheck a orders-db (con pg_isready) y cambiá el depends_on de orders-service para que espere a que esté healthy antes de arrancar.
🎯 Qué deberías lograr: docker compose ps debe mostrar orders-db como (healthy), y en el log de arranque orders-service debe iniciar después de que orders-db pase a Healthy (no antes, como pasaba con el depends_on simple de la sección 10).
💡 ¿Sabías que…? — depends_on simple solo espera a que el contenedor "exista"
depends_on: [orders-db] (sin condition) hace que Compose arranque orders-db primero, pero no espera a que Postgres esté listo para aceptar conexiones — apenas el proceso arrancó, ya sigue con el siguiente servicio. Con bases que tardan en inicializar, eso puede causar el primer intento de conexión fallido.
Ver solución
yaml
orders-db:
# ...
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 5
orders-service:
# ...
depends_on:
orders-db:
condition: service_healthy
products-service:
condition: service_startedVerificado: docker compose up mostró orders-db Waiting → orders-db Healthy → recién ahí orders-service Starting; docker compose ps mostró orders-db como Up ... (healthy).
❓ Preguntas y respuestas (autoevaluación)
1. ¿Qué problema resuelve Docker que un simple venv de Python no resuelve?
Un
venvsolo aísla las librerías de Python. Docker empaqueta además el runtime completo (versión de Python, el sistema operativo base, variables de entorno, el comando de arranque) — todo lo que hace falta para que la app se comporte igual en cualquier máquina, no solo las dependencias depip.
2. ¿Cuál es la diferencia entre una imagen y un contenedor?
La imagen es una plantilla inmutable (el resultado de
docker build); el contenedor es una instancia en ejecución de esa imagen (creada condocker run). Una misma imagen puede generar muchos contenedores a la vez.
3. ¿Por qué conviene copiar requirements.txt y correr pip install antes de copiar el código de la app en un Dockerfile?
Por el cacheo de capas: si el código cambia pero las dependencias no, Docker reutiliza la capa de
pip installya hecha en vez de reinstalar todo de nuevo — builds mucho más rápidos.
4. ¿Qué diferencia hay entre el puerto que declara EXPOSE en el Dockerfile y el que se mapea con -p en docker run?
EXPOSEsolo documenta qué puerto usa la app dentro del contenedor — no lo hace accesible desde fuera.-p host:contenedores lo que realmente publica ese puerto hacia el host.
5. ¿Qué diferencia hay entre una "Docker Official Image" y una imagen cualquiera de Docker Hub?
La oficial la mantiene el equipo de Docker o el proyecto del lenguaje/ herramienta, con builds revisados y actualizados regularmente. El resto son imágenes publicadas por terceros, sin esa garantía de mantenimiento ni de seguridad.
6. ¿Por qué un contenedor de base de datos necesita un volumen para no perder los datos?
Porque el filesystem de un contenedor se borra junto con él (
docker rm). Un volumen vive fuera del contenedor, en el host — así los datos sobreviven aunque el contenedor se borre y se recree.
7. Dentro de un contenedor, ¿a qué apunta localhost? ¿Y host.docker.internal?
localhostadentro de un contenedor es el propio contenedor, nunca tu Mac ni otro contenedor.host.docker.internales el nombre especial que resuelve a la IP real del host — necesario cuando no hay una red de Compose de por medio (por eso, a veces, hace falta forzarlo con--add-host).
8. Con Docker Compose, ¿cómo se comunican dos servicios entre sí sin usar host.docker.internal?
Por el nombre del servicio y su puerto interno — Compose crea una red DNS interna donde cada servicio es alcanzable por su nombre (p. ej.
http://products-service:8000), sin necesitarlocalhostnihost.docker.internal.
9. ¿Qué hace depends_on por defecto, y qué le falta para asegurar que un servicio realmente esté listo (no solo iniciado)?
Por defecto,
depends_onsolo controla el orden de arranque — arranca el contenedor dependido antes, pero no espera a que esté listo para aceptar conexiones. Para eso hace falta sumar unhealthchecka ese servicio y usarcondition: service_healthyen eldepends_on.
10. ¿Cuál es la diferencia entre Docker y Kubernetes?
No compiten, se complementan: Docker construye y corre contenedores individuales; Kubernetes orquesta muchos contenedores (típicamente creados con Docker) a través de muchas máquinas — los reinicia si se caen, los escala según demanda y balancea tráfico entre las copias sanas.
📎 Apuntes relacionados
- Clase 4 — primer
docker runde Postgres (bd_test_backend), base del patrón reusado toda esta clase. - Clase 7 —
orders_service,products_serviceyusers_serviceoriginales (copiados sin tocar en esta clase). - 00-Notas/01-Comandos.md — todos los comandos de Docker/Compose de esta clase, en su formato de referencia rápida.