Módulo 3: Volumes, Networking y Registries
8. Proyecto: Persistent Data App
Descripción
Este es el proyecto integrador del Módulo 3 y cierra la Phase 1 (Docker Fundamentals). Vas a construir una aplicación que demuestra los tres pilares que aprendiste: volumes para persistir datos, networking para conectar containers, y port mapping para acceder a servicios desde tu máquina. El resultado es un mini-sistema de dos containers que se comunican y persisten datos.
Qué vas a construir: Una aplicación Python que funciona como un "Log Analyzer" — recibe textos, los almacena en un volumen persistente, y usa Redis como cache para resultados. Dos containers, una network, un volume, y port mapping. Es el preludio exacto de lo que harás con Docker Compose en el Módulo 5.
Por qué importa: Este proyecto demuestra que ya puedes pensar en sistemas Docker multi-componente. Es la transición de "un container aislado" a "containers que trabajan juntos como un sistema." Exactamente lo que necesitas para Phase 2.
Objetivo del Proyecto
Construir un sistema de dos containers:
- ✅ Redis (container 1): Cache de datos, persistido con named volume
- ✅ Python app (container 2): Escribe/lee datos de Redis, accesible via port mapping
- ✅ Network custom que conecta ambos containers
- ✅ Datos que sobreviven a
docker rmgracias al volume en Redis - ✅ Acceso desde el host via port mapping
Tiempo estimado: 25-35 minutos.
Recap: Lo Que Ya Sabes
| Cápsula | Concepto aplicado |
|---|---|
| 02 | Persistencia: sin volumes los datos se pierden |
| 03 | Named volumes: datos que sobreviven al container |
| 04 | Bind mounts: código local en el container |
| 05 | Networking: containers se comunican por nombre |
| 06 | Port mapping: acceso desde el host |
| 07 | Docker Hub: compartir imágenes |
Paso 1: Crear la Estructura del Proyecto
mkdir -p docker-essentials/module-03/persistent-app
cd docker-essentials/module-03/persistent-app
La aplicación: app.py
"""
Log Analyzer — Almacena y analiza entradas de texto usando Redis como backend.
Demuestra volumes (persistencia Redis) y networking (Python ↔ Redis).
CONCEPTOS DOCKER APLICADOS:
- host="redis": En una custom network, los containers se resuelven por nombre.
El container Redis tiene --name redis, por eso la app usa ese hostname.
- La app DEBE correr con --network app-net para estar en la misma red que Redis.
"""
import redis
import json
import sys
from datetime import datetime
from collections import Counter
def get_redis_client():
"""
Conecta a Redis usando el nombre del container como hostname.
NETWORKING: En Docker, cada container en una custom network tiene un DNS interno.
Cuando escribes host="redis", el resolver de la red app-net traduce "redis"
a la IP del container llamado redis. Por eso --name redis es crítico.
"""
try:
# decode_responses=True evita bytes y devuelve strings directamente
client = redis.Redis(host="redis", port=6379, decode_responses=True)
client.ping() # Verifica conectividad antes de continuar
return client
except redis.ConnectionError:
print("ERROR: No se puede conectar a Redis.")
print("Asegúrate de que Redis está corriendo en la misma network.")
print(" docker run -d --network app-net --name redis redis:7-alpine")
sys.exit(1)
def add_entry(r, text):
"""
Agrega una entrada de texto al log.
REDIS DATA STRUCTURES usadas:
- INCR: contador atómico para IDs únicos
- HSET: hash (diccionario) por entrada
- LPUSH: lista que mantiene orden de inserción (último primero)
"""
entry = {
"text": text,
"timestamp": datetime.now().isoformat(),
"word_count": len(text.split()),
"char_count": len(text),
}
# INCR es atómico: evita race conditions si múltiples procesos escriben
entry_id = r.incr("entry_counter")
# Hash entry:1, entry:2... almacena cada entrada como key-value
r.hset(f"entry:{entry_id}", mapping=entry)
# LPUSH añade al inicio: las entradas más recientes primero
r.lpush("entry_ids", entry_id)
return entry_id, entry
def get_stats(r):
"""
Calcula estadísticas de todas las entradas.
LRANGE 0 -1: obtiene toda la lista (desde índice 0 hasta -1 = último).
HGETALL: devuelve todos los campos de un hash como dict.
"""
entry_ids = r.lrange("entry_ids", 0, -1)
total_entries = len(entry_ids)
if total_entries == 0:
return {"total_entries": 0, "message": "No hay entradas aún"}
all_words = []
total_chars = 0
total_words = 0
for eid in entry_ids:
entry = r.hgetall(f"entry:{eid}")
if entry:
total_words += int(entry.get("word_count", 0))
total_chars += int(entry.get("char_count", 0))
all_words.extend(entry.get("text", "").lower().split())
top_words = Counter(all_words).most_common(5)
return {
"total_entries": total_entries,
"total_words": total_words,
"total_chars": total_chars,
"avg_words_per_entry": round(total_words / max(total_entries, 1), 1),
"top_words": [{"word": w, "count": c} for w, c in top_words],
}
def show_entries(r, limit=5):
"""
Muestra las últimas entradas.
LRANGE 0 limit-1: obtiene los primeros N elementos de la lista.
Como usamos LPUSH, los más recientes están al inicio.
"""
entry_ids = r.lrange("entry_ids", 0, limit - 1)
entries = []
for eid in entry_ids:
entry = r.hgetall(f"entry:{eid}")
if entry:
entries.append({"id": eid, **entry})
return entries
def main():
r = get_redis_client()
print("=" * 60)
print("LOG ANALYZER v1.0 — Persistent Data App")
print("=" * 60)
if len(sys.argv) > 1:
command = sys.argv[1]
if command == "add" and len(sys.argv) > 2:
text = " ".join(sys.argv[2:])
entry_id, entry = add_entry(r, text)
print(f"\n✅ Entrada #{entry_id} agregada:")
print(f" Texto: {text}")
print(f" Palabras: {entry['word_count']}")
print(f" Timestamp: {entry['timestamp']}")
elif command == "stats":
stats = get_stats(r)
print(f"\n📊 Estadísticas:")
print(f" Total entradas: {stats['total_entries']}")
if stats['total_entries'] > 0:
print(f" Total palabras: {stats['total_words']}")
print(f" Promedio palabras/entrada: {stats['avg_words_per_entry']}")
print(f"\n Top palabras:")
for item in stats.get('top_words', []):
print(f" {item['word']:15s} ({item['count']})")
elif command == "list":
entries = show_entries(r)
print(f"\nÚltimas {len(entries)} entradas:")
for e in entries:
print(f" [{e['id']}] {e.get('text', '')[:50]}... ({e.get('word_count', 0)} palabras)")
elif command == "reset":
# FLUSHDB: borra toda la base de datos actual (solo la DB 0 en Redis)
r.flushdb()
print("\n🗑️ Base de datos limpiada.")
else:
print(f"\nComando no reconocido: {command}")
print("Uso: add <texto> | stats | list | reset")
else:
print("\nUso:")
print(" add <texto> — Agrega una entrada")
print(" stats — Muestra estadísticas")
print(" list — Lista últimas entradas")
print(" reset — Limpia la base de datos")
print("=" * 60)
if __name__ == "__main__":
main()
Dependencias: requirements.txt
redis==5.0.1
Dockerfile
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENTRYPOINT ["python", "app.py"]
CMD []
.dockerignore
__pycache__/
*.pyc
.venv/
.git/
.env
*.log
.DS_Store
Paso 2: Construir la Imagen
docker build -t log-analyzer:1.0 .
Output esperado:
[+] Building 8.5s (9/9) FINISHED
=> [1/4] FROM python:3.11-slim ...
=> [2/4] WORKDIR /app
=> [3/4] COPY requirements.txt .
=> [4/4] RUN pip install --no-cache-dir -r requirements.txt
=> [5/5] COPY . .
=> naming to docker.io/library/log-analyzer:1.0
Paso 3: Crear la Infraestructura Docker
Network
docker network create app-net
Volume para Redis
docker volume create redis-data
Iniciar Redis
docker run -d \
--name redis \
--network app-net \
-v redis-data:/data \
redis:7-alpine \
redis-server --appendonly yes
Desglose:
-d— Detached (background)--name redis— Nombre que la app Python usará para conectarse--network app-net— Misma network que la app-v redis-data:/data— Volume para persistir datos de Redisredis-server --appendonly yes— Redis con persistencia a disco (escribe cada cambio a/data/appendonly.aof; sin esto, los datos se pierden al reiniciar)
# Verifica que Redis está corriendo
docker ps --filter name=redis
Output esperado:
CONTAINER ID IMAGE STATUS PORTS NAMES
abc123... redis:7-alpine Up 5 seconds 6379/tcp redis
Paso 4: Usar la Aplicación
Agregar entradas
docker run --rm --network app-net log-analyzer:1.0 add "Docker permite containerizar aplicaciones AI con todas sus dependencias"
docker run --rm --network app-net log-analyzer:1.0 add "FastAPI es el framework preferido para APIs de AI en Python"
docker run --rm --network app-net log-analyzer:1.0 add "ChromaDB es una vector database ideal para sistemas RAG"
Output esperado (para cada comando):
============================================================
LOG ANALYZER v1.0 — Persistent Data App
============================================================
✅ Entrada #1 agregada:
Texto: Docker permite containerizar aplicaciones AI con todas sus dependencias
Palabras: 9
Timestamp: 2026-03-08T16:30:00.123456
============================================================
Ver estadísticas
docker run --rm --network app-net log-analyzer:1.0 stats
Output esperado:
============================================================
LOG ANALYZER v1.0 — Persistent Data App
============================================================
📊 Estadísticas:
Total entradas: 3
Total palabras: 28
Promedio palabras/entrada: 9.3
Top palabras:
es (2)
ai (2)
para (2)
docker (1)
...
============================================================
Listar entradas
docker run --rm --network app-net log-analyzer:1.0 list
Cómo funciona la comunicación
Cuando ejecutas docker run --rm --network app-net log-analyzer:1.0 add "texto":
- Docker crea un container temporal con la imagen
log-analyzer:1.0 - Lo conecta a la network
app-net(misma que Redis) - El DNS interno de Docker resuelve
redis→ IP del container Redis - La app Python abre conexión TCP a
redis:6379 - Redis escribe los datos en
/data(que es el volumeredis-data) - Al terminar, el container se elimina (
--rm), pero Redis y el volume siguen
Cada comando add, stats, list crea un container nuevo que vive unos segundos. Redis persiste entre todas esas ejecuciones gracias al volume y a --appendonly yes.
Paso 5: Verificar Persistencia
Este es el test más importante del proyecto: los datos sobreviven a la destrucción de containers.
# 1. Detén y elimina Redis
docker stop redis
docker rm redis
# 2. Verifica que el volume sigue existiendo
docker volume ls --filter name=redis-data
Output esperado:
DRIVER VOLUME NAME
local redis-data
# 3. Recrea Redis con el MISMO volume
docker run -d \
--name redis \
--network app-net \
-v redis-data:/data \
redis:7-alpine \
redis-server --appendonly yes
# 4. Verifica que los datos persisten
docker run --rm --network app-net log-analyzer:1.0 stats
Output esperado: Las mismas 3 entradas siguen ahí. Los datos sobrevivieron porque están en el named volume redis-data, no dentro del container.
# 5. Agrega una nueva entrada post-recreación
docker run --rm --network app-net log-analyzer:1.0 add "Los datos persisten gracias a Docker volumes"
docker run --rm --network app-net log-analyzer:1.0 stats
Output esperado: Ahora muestra 4 entradas.
Paso 6: Limpieza
# Detener y eliminar containers
docker stop redis
docker rm redis
# Eliminar network
docker network rm app-net
# Eliminar volumes (si quieres limpiar los datos)
docker volume rm redis-data
# Eliminar imágenes del proyecto (opcional)
docker image rm log-analyzer:1.0
Troubleshooting del Proyecto
Cuando algo falla, estos son los cinco problemas más frecuentes y cómo resolverlos:
1. ConnectionError: Cannot connect to Redis
Síntoma: La app Python muestra "ERROR: No se puede conectar a Redis" al ejecutar cualquier comando.
Causas posibles:
- Redis no está corriendo
- Redis y la app no están en la misma network
- Olvidaste
--network app-netal ejecutar la app
Solución:
# Verifica que Redis está corriendo
docker ps --filter name=redis
# Si no está, inícialo con network y volume
docker run -d \
--name redis \
--network app-net \
-v redis-data:/data \
redis:7-alpine \
redis-server --appendonly yes
# Verifica que la app usa la misma network
docker run --rm --network app-net log-analyzer:1.0 stats
2. Network not found
Síntoma: Error response from daemon: network app-net not found
Causa: La network debe crearse antes de usarla. Si la eliminaste con docker network rm app-net o nunca la creaste, los containers no pueden unirse.
Solución:
# Crea la network primero
docker network create app-net
# Luego inicia Redis (y la app) con --network app-net
3. Volume data not persisting
Síntoma: Después de docker stop redis && docker rm redis y recrear, los datos desaparecen.
Causas posibles:
- El nombre del volume no coincide al recrear (ej: escribiste
redis_dataen vez deredis-data) - Redis no se inició con
--appendonly yes(sin persistencia a disco) - Usaste un volume anónimo o un path incorrecto
Solución:
# Verifica que el volume existe y tiene el nombre correcto
docker volume ls --filter name=redis-data
# Al recrear Redis, usa EXACTAMENTE el mismo binding
docker run -d \
--name redis \
--network app-net \
-v redis-data:/data \
redis:7-alpine \
redis-server --appendonly yes
El flag --appendonly yes es obligatorio: hace que Redis escriba cambios a /data/appendonly.aof dentro del volume. Sin él, Redis solo mantiene datos en memoria y se pierden al reiniciar.
4. Container can't resolve hostname 'redis'
Síntoma: redis.exceptions.ConnectionError: Error -2 connecting to redis:6379. Name or service not known
Causa: Estás usando la network por defecto (bridge) en vez de una custom network. En la network default, los containers no se resuelven por nombre.
Solución:
# La app DEBE usar --network app-net
docker run --rm --network app-net log-analyzer:1.0 stats
# Si omitiste --network app-net, la app intenta resolver "redis" y falla
Solo las custom networks tienen DNS automático por nombre. En bridge, tendrías que usar la IP del container (que cambia cada vez).
5. Port already in use
Síntoma: Error: bind: address already in use o similar al iniciar Redis con -p 6379:6379.
Causa: Otro proceso (local Redis, otro container) ya usa el puerto 6379 en tu host.
Solución: Para este proyecto no necesitas port mapping: la app se conecta desde dentro de la network. Si aun así quieres exponer Redis al host:
# Usa otro puerto en el host
docker run -d ... -p 6380:6379 redis:7-alpine ...
# Luego conéctate con redis-cli -p 6380 desde tu máquina
O identifica qué usa el puerto:
# macOS/Linux
lsof -i :6379
Resumen rápido: Si algo falla
| Síntoma | Acción rápida |
|---|---|
| ConnectionError a Redis | docker ps → Redis corriendo? --network app-net en la app? |
| Network not found | docker network create app-net antes de run |
| Datos no persisten | Revisa -v redis-data:/data y --appendonly yes |
| Name or service not known | App sin --network app-net — añádelo |
| Port in use | No mapees puerto, o usa -p 6380:6379 |
Errores Comunes
Evita estos errores que suelen cometer quienes arrancan con Docker multi-container:
| Error | Qué pasa | Cómo evitarlo |
|---|---|---|
Olvidar --network app-net | La app no puede resolver el hostname redis. ConnectionError. | Siempre incluye --network app-net al ejecutar la app: docker run --rm --network app-net log-analyzer:1.0 ... |
No usar --appendonly yes | Redis no persiste a disco. Al recrear el container, todos los datos se pierden. | Inicia Redis con redis-server --appendonly yes para que escriba en el volume. |
docker volume prune | Elimina todos los volumes no usados. Pierdes redis-data y todo el contenido. | No ejecutes prune sin revisar qué borrarás. Usa docker volume rm redis-data solo si quieres limpiar este proyecto. |
| Containers sin nombre | Dificulta gestionarlos: no sabes cuál es cuál, y la app no puede usar DNS por nombre. | Usa --name redis para Redis. La app usa ese nombre para conectarse. |
| Crear network/volume en orden incorrecto | Si intentas docker run --network app-net sin haber creado app-net, falla. | Orden: (1) docker network create app-net, (2) docker volume create redis-data, (3) iniciar Redis, (4) ejecutar app. |
Orden de arranque recomendado
Para evitar errores de "network not found" o "volume not found", sigue siempre este orden:
- Network →
docker network create app-net - Volume →
docker volume create redis-data - Redis →
docker run -d --name redis --network app-net -v redis-data:/data redis:7-alpine redis-server --appendonly yes - App →
docker run --rm --network app-net log-analyzer:1.0 <comando>
Si reinicias desde cero (después de docker rm y docker network rm), repite los pasos 1-3. El volume persiste, así que no hace falta recrearlo a menos que quieras datos frescos.
Criterios de Éxito
Tu proyecto está completo si:
- ✅ Network creada:
app-netconecta la app Python con Redis - ✅ Volume creado:
redis-datapersiste datos de Redis - ✅ Redis corriendo: Container detached con volume y network
- ✅ App funciona: Puedes agregar, listar, y ver stats
- ✅ Persistencia verificada: Datos sobreviven a
docker stop+docker rm+ recrear Redis - ✅ Networking funciona: La app se conecta a Redis por nombre (
redis:6379) - ✅ Limpieza completa: Puedes derribar y limpiar todo
Checklist de validación
# Red existe
docker network ls --filter name=app-net
# Volume existe
docker volume ls --filter name=redis-data
# Redis corriendo
docker ps --filter name=redis --format "{{.Names}}: {{.Status}}"
# App funciona
docker run --rm --network app-net log-analyzer:1.0 stats
Verificación Completa
Antes de dar por terminado el proyecto, ejecuta esta batería de tests para confirmar que todo funciona correctamente. Hazlo en orden:
Test 1: Network existe
docker network ls --filter name=app-net
Esperado: Una línea con app-net en la columna NAME.
Test 2: Volume existe
docker volume ls --filter name=redis-data
Esperado: Una línea con redis-data en la columna VOLUME NAME.
Test 3: Redis está healthy
docker exec redis redis-cli ping
Esperado: PONG
Si falla: Redis no está corriendo o el container no se llama redis. Verifica con docker ps --filter name=redis.
Test 4: App puede conectar a Redis
docker run --rm --network app-net log-analyzer:1.0 stats
Esperado: Output del Log Analyzer sin errores de conexión. Si hay entradas, muestra estadísticas; si no, "No hay entradas aún".
Test 5: Datos persisten al recrear Redis
Este es el test crítico de persistencia:
# 5a. Agrega datos
docker run --rm --network app-net log-analyzer:1.0 add "Test de persistencia"
docker run --rm --network app-net log-analyzer:1.0 stats
# Anota cuántas entradas hay (ej: 5)
# 5b. Destruye Redis
docker stop redis
docker rm redis
# 5c. Recrea Redis con el mismo volume
docker run -d \
--name redis \
--network app-net \
-v redis-data:/data \
redis:7-alpine \
redis-server --appendonly yes
# 5d. Verifica que los datos siguen ahí
docker run --rm --network app-net log-analyzer:1.0 stats
Esperado: El mismo número de entradas (o más si agregaste después). Los datos sobrevivieron al ciclo completo.
Test 6: Clean shutdown funciona
docker stop redis
docker rm redis
Esperado: Redis se detiene y se elimina sin errores. El volume redis-data sigue existiendo (docker volume ls).
Qué Aprendiste en Phase 1
Con los módulos 1-3 completados, dominas los fundamentos de Docker:
| Pilar | Módulo | Qué dominas |
|---|---|---|
| Conceptos | 1 | Qué es Docker, por qué, arquitectura, comandos básicos |
| Images/Containers | 2 | Dockerfiles, build, layers, caching, ciclo de vida |
| Persistencia | 3 | Named volumes, bind mounts |
| Comunicación | 3 | Networking, port mapping |
| Distribución | 3 | Docker Hub, push/pull |
Qué Viene Después: Phase 2
La Phase 2 aplica todo esto a aplicaciones AI reales. La transición es directa: "Ya dominas Docker fundamentals. Ahora aplícalos a tus AI apps."
Módulo 4: FastAPI + OpenAI con Secrets
Tomarás una API real (FastAPI + OpenAI) y la containerizarás. La diferencia clave: secrets management. En este proyecto no había API keys; en Phase 2 aprenderás a inyectar OPENAI_API_KEY de forma segura con --env-file o Docker secrets, sin hardcodear en el Dockerfile. El patrón app + network + volume se repite, pero con una capa más de producción.
Módulo 5: Docker Compose
Todo lo que hiciste manualmente (network, volumes, containers, orden de arranque) se define en un archivo YAML. En vez de 6 comandos docker run y docker network create, tendrás algo como:
services:
redis:
image: redis:7-alpine
volumes: [redis-data:/data]
command: redis-server --appendonly yes
app:
build: .
depends_on: [redis]
networks: [app-net]
volumes:
redis-data: {}
networks:
app-net: {}
Un solo comando: docker compose up -d. La misma arquitectura que construiste a mano, pero declarativa y reproducible.
Módulo 6: Multi-stage Builds
Las imágenes de Python suelen superar 1GB. Con multi-stage builds reducirás eso a ~200MB: un stage para compilar/dependencias y otro minimal para el runtime. Menos superficie de ataque, pushes más rápidos, despliegues más ágiles.
Conexión con lo que hiciste aquí
| Este proyecto (Phase 1) | Phase 2 |
|---|---|
docker run manual para cada container | docker compose up para todo el stack |
| Network + volume creados a mano | Definidos en docker-compose.yml |
| App que habla con Redis por nombre | Igual, pero el compose crea la network automáticamente |
| Sin secrets | Secrets para API keys y credenciales |
| Imagen Python estándar | Imagen optimizada con multi-stage |
No deseches nada de lo que aprendiste: lo reutilizarás. Solo cambia la herramienta (CLI → Compose) y el dominio (log analyzer → AI apps).
Resumen
- Construiste un sistema de 2 containers (Redis + Python app) conectados por networking
- Named volumes persisten datos de Redis que sobreviven a
docker rm - Custom network permite que la app se conecte a Redis por nombre (
redis:6379) - Los datos sobreviven al ciclo de vida completo: stop → rm → recrear → datos intactos
- Este patrón (app + DB + network + volume) es exactamente lo que Docker Compose (Módulo 5) automatiza
- Completaste Phase 1: tienes todos los fundamentos para containerizar AI apps
Recursos Adicionales
- Docker Volumes (docs) — Referencia de volumes
- Docker Networking (docs) — Referencia de networking
- Redis Docker Image — Imagen oficial de Redis
- redis-py Documentation — Cliente Redis para Python
- Docker Compose Overview — Preview del Módulo 5
- Container Networking (Docker blog) — Explicación profunda de networking