# Guía del Estudiante - S11: Fundamentos de Docker y Desarrollo Local
Esta guía te acompañará durante la sesión S11 para aprender los fundamentos de Docker y su uso en desarrollo local. Incluye conceptos teóricos esenciales, prácticas paso a paso y ejercicios para consolidar tu aprendizaje.
## Objetivos de Aprendizaje
Al finalizar esta sesión serás capaz de:
- Comprender los conceptos fundamentales de contenedores Docker y su diferencia con máquinas virtuales
- Construir imágenes Docker personalizadas utilizando Dockerfiles siguiendo mejores prácticas
- Configurar y utilizar DevContainers para crear entornos de desarrollo consistentes y reproducibles
- Crear y gestionar aplicaciones multi-contenedor utilizando Docker Compose
- Integrar PostgreSQL con aplicaciones Flask mediante Docker Compose
- Gestionar secretos y credenciales de forma segura en entornos de desarrollo local utilizando variables de entorno y Docker Compose secrets
- Aplicar mejores prácticas de seguridad evitando hardcodear credenciales en código o imágenes
## Requisitos Previos
### Conocimientos Necesarios
- Conocimientos básicos de línea de comandos (terminal/consola)
- Familiaridad con conceptos básicos de sistemas operativos
- Conocimientos básicos de Python y Flask (para las prácticas avanzadas)
- Conceptos básicos de bases de datos relacionales (para PostgreSQL)
### Software Requerido
Antes de comenzar, asegúrate de tener instalado:
- **Docker Desktop** (o Docker Engine + Docker Compose):
- Windows: [Docker Desktop para Windows](https://www.docker.com/products/docker-desktop/)
- macOS: [Docker Desktop para Mac](https://www.docker.com/products/docker-desktop/)
- Linux: Docker Engine + Docker Compose plugin
- Versión mínima recomendada: Docker 24.0+
- **Editor de código** con soporte para DevContainers:
- VS Code con extensión "Dev Containers"
- Cursor con extensión "Dev Containers"
- **Python 3.11+** instalado localmente (opcional, para desarrollo sin contenedores)
- **Git** para control de versiones
### Verificación de Instalación
Verifica que Docker está correctamente instalado:
```bash
# Verificar versión de Docker
docker --version
# Verificar versión de Docker Compose
docker compose version
# Ejecutar contenedor de prueba
docker run hello-world
```
Si todos los comandos funcionan correctamente, estás listo para comenzar.
## Introducción Teórica
### ¿Qué es Docker?
Docker es una plataforma de contenedores que permite empaquetar aplicaciones y sus dependencias en contenedores ligeros y portables. A diferencia de las máquinas virtuales tradicionales que requieren un sistema operativo completo, los contenedores Docker comparten el kernel del sistema operativo host, lo que los hace más eficientes en términos de recursos.
**Conceptos fundamentales**:
- **Imagen**: Plantilla de solo lectura que contiene el sistema de archivos y configuración necesarios para ejecutar una aplicación. Las imágenes se construyen a partir de Dockerfiles y se almacenan en registros como Docker Hub.
- **Contenedor**: Instancia ejecutable de una imagen. Los contenedores son efímeros por defecto, aunque los datos pueden persistir mediante volúmenes. Cada contenedor tiene su propio sistema de archivos aislado pero comparte el kernel del host.
- **Dockerfile**: Archivo de texto que contiene instrucciones para construir una imagen. Define el sistema operativo base, dependencias, código de la aplicación y comandos de inicio.
- **Registro**: Repositorio donde se almacenan y distribuyen imágenes Docker. Docker Hub es el registro público por defecto, pero existen alternativas privadas como Google Container Registry (GCR) o Amazon ECR.
### Comandos Básicos de Docker
Docker proporciona dos formatos de comandos: el formato antiguo (legacy) y el formato nuevo (más explícito). Se recomienda usar el formato nuevo para mayor claridad.
**Gestión de imágenes** (formato nuevo recomendado):
- `docker image ls`: Lista todas las imágenes locales
- `docker image pull <imagen>`: Descarga una imagen desde un registro
- `docker image rm <imagen>`: Elimina una imagen local
- `docker image prune`: Elimina imágenes no utilizadas
- `docker image inspect <imagen>`: Muestra información detallada de una imagen
**Gestión de contenedores** (formato nuevo recomendado):
- `docker container ls`: Lista contenedores en ejecución
- `docker container ls -a`: Lista todos los contenedores incluyendo detenidos
- `docker container stop <contenedor>`: Detiene un contenedor en ejecución
- `docker container start <contenedor>`: Inicia un contenedor detenido
- `docker container restart <contenedor>`: Reinicia un contenedor
- `docker container rm <contenedor>`: Elimina un contenedor
- `docker container prune`: Elimina contenedores detenidos
- `docker container exec -it <contenedor> <comando>`: Ejecuta un comando en un contenedor en ejecución
### Volúmenes en Docker
Los volúmenes proporcionan mecanismos para persistir datos generados por contenedores y compartir archivos entre el host y los contenedores. Sin volúmenes, todos los datos dentro del sistema de archivos de un contenedor se pierden cuando el contenedor se elimina.
**Tipos de volúmenes**:
- **Named Volumes (Volúmenes Nombrados)**: Volúmenes gestionados por Docker, almacenados en un directorio gestionado por Docker en el host. Son ideales para persistencia de datos de bases de datos y aplicaciones.
- **Bind Mounts**: Mapean directamente un directorio o archivo del sistema de archivos del host a un directorio o archivo en el contenedor. Son útiles para desarrollo cuando se necesita sincronización bidireccional entre host y contenedor.
**Comandos de gestión de volúmenes**:
- `docker volume create <nombre>`: Crea un volumen nombrado
- `docker volume ls`: Lista todos los volúmenes
- `docker volume inspect <nombre>`: Muestra información detallada de un volumen
- `docker volume rm <nombre>`: Elimina un volumen específico
- `docker volume prune`: Elimina todos los volúmenes no utilizados
### Publicación de Puertos
Por defecto, los contenedores Docker están aislados de la red del host. Para acceder a servicios ejecutándose dentro de contenedores desde el host, es necesario publicar puertos.
**Conceptos importantes**:
- **Puerto del contenedor**: Puerto en el que la aplicación dentro del contenedor escucha
- **Puerto del host**: Puerto en el sistema host que se mapea al puerto del contenedor
- **EXPOSE en Dockerfile**: Instrucción que documenta qué puertos usa la aplicación, pero NO publica los puertos automáticamente
- **Publicación real**: Se realiza con `-p` o `-P` en `docker run` o con `ports:` en `docker-compose.yml`
**Opciones de publicación**:
- `-p <puerto-host>:<puerto-contenedor>`: Mapea un puerto específico del host a un puerto específico del contenedor
- `-P` (mayúscula): Publica todos los puertos documentados con `EXPOSE` en el Dockerfile a puertos aleatorios del host
### Construcción de Imágenes Docker
Los Dockerfiles definen cómo construir imágenes Docker mediante una serie de instrucciones. Cada instrucción crea una nueva capa en la imagen, y Docker cachea estas capas para acelerar builds posteriores.
**Sintaxis básica de Dockerfile**:
- `FROM`: Especifica la imagen base. Debe ser la primera instrucción.
- `WORKDIR`: Establece el directorio de trabajo para instrucciones posteriores.
- `COPY` / `ADD`: Copia archivos del contexto de build a la imagen.
- `RUN`: Ejecuta comandos durante la construcción de la imagen.
- `ENV`: Define variables de entorno disponibles en tiempo de ejecución.
- `EXPOSE`: Documenta los puertos que el contenedor escuchará (no abre puertos).
- `CMD` / `ENTRYPOINT`: Define el comando por defecto cuando se ejecuta el contenedor.
**Mejores prácticas**:
- Usar imágenes base oficiales y específicas (evitar `latest` para reproducibilidad)
- Minimizar número de capas combinando comandos RUN relacionados
- Ordenar instrucciones de más estáticas a más dinámicas para aprovechar cache
- Usar `.dockerignore` para excluir archivos innecesarios del contexto de build
- No incluir secretos en imágenes: usar variables de entorno o secretos montados
### DevContainers
DevContainers (Development Containers) es una especificación que permite definir entornos de desarrollo completos en contenedores Docker. Esto garantiza que todos los desarrolladores trabajen en el mismo entorno, eliminando problemas de "funciona en mi máquina".
**Ventajas**:
- Consistencia entre desarrolladores
- Fácil onboarding de nuevos desarrolladores
- Aislamiento de dependencias del sistema host
- Reproducibilidad exacta del entorno
### Docker Compose
Docker Compose es una herramienta para definir y ejecutar aplicaciones Docker multi-contenedor mediante un archivo YAML. Permite gestionar múltiples servicios, sus dependencias, redes y volúmenes de forma declarativa.
**Conceptos fundamentales**:
- **Servicio**: Contenedor definido en docker-compose.yml
- **Volumen**: Mecanismo para persistir datos generados por contenedores
- **Red**: Aislamiento de red que permite comunicación entre contenedores por nombre de servicio
**Gestión de secretos**:
Nunca se deben hardcodear credenciales en docker-compose.yml o Dockerfiles. Las mejores prácticas incluyen:
- Usar archivo `.env` para variables de entorno locales (incluir en `.gitignore`)
- Usar `env_file` en docker-compose.yml para cargar variables desde `.env`
- Documentar variables requeridas en `.env.example` sin valores sensibles
**Convención de Docker Compose con archivo `.env`**:
Docker Compose tiene una convención importante respecto al archivo `.env`. Cuando utilizas referencias a variables de entorno en `docker-compose.yml` mediante la sintaxis `${VARIABLE}`, Docker Compose busca automáticamente un archivo llamado `.env` en el mismo directorio donde se encuentra el archivo `docker-compose.yml`. Esta búsqueda automática permite resolver las variables sin necesidad de especificar explícitamente el archivo en la mayoría de los casos.
Por ejemplo, si en tu `docker-compose.yml` tienes `POSTGRES_USER: ${DB_USER}`, Docker Compose buscará automáticamente un archivo `.env` en el mismo directorio y leerá el valor de `DB_USER` desde ese archivo. Si el archivo tiene un nombre diferente (por ejemplo, `.env.dev` o `env.example`), Docker Compose NO lo utilizará automáticamente. Para usar un archivo con nombre diferente, debes especificarlo explícitamente mediante la opción `--env-file` en la línea de comandos: `docker compose --env-file .env.dev up`.
Es importante distinguir entre dos usos diferentes de variables de entorno en Docker Compose:
- **Resolución de variables en docker-compose.yml**: Las referencias como `${DB_USER}` en el archivo `docker-compose.yml` se resuelven automáticamente desde `.env` en el mismo directorio.
- **Carga de variables en contenedores**: La directiva `env_file` carga variables desde un archivo específico (que puede ser `.env` u otro) y las inyecta como variables de entorno dentro del contenedor.
## Prácticas Paso a Paso
### Práctica 1: Introducción a Docker - Servidor Web con Volúmenes y Puertos
**Duración estimada**: 45-60 minutos
**Objetivo**: Familiarizarse con comandos básicos de Docker, gestión de imágenes y contenedores, y entender el uso de volúmenes y publicación de puertos mediante un servidor web Nginx.
#### Paso 1: Comandos básicos de gestión
1. Descarga la imagen de Nginx usando `docker image pull`
2. Lista las imágenes locales usando `docker image ls`
3. Inspecciona la imagen descargada usando `docker image inspect`
4. Ejecuta un contenedor sin publicar puertos usando `docker run -d --name web-test nginx:alpine`
5. Verifica que el contenedor está ejecutándose con `docker container ls`
6. Intenta acceder al servidor web desde el host (debe fallar porque no hay puertos publicados)
7. Detén y elimina el contenedor usando `docker container stop` y `docker container rm`
#### Paso 2: Publicación de puertos
1. Ejecuta un contenedor con mapeo específico de puertos: `docker run -d -p 8080:80 --name web-server nginx:alpine`
2. Verifica el mapeo de puertos usando `docker port web-server`
3. Accede al servidor web desde el host usando `curl http://localhost:8080` o abre un navegador
4. Verifica información detallada del contenedor usando `docker container inspect web-server`
5. Detén el contenedor usando `docker container stop web-server`
#### Paso 3: Volúmenes - Bind Mount
1. Crea un directorio en tu sistema host: `mkdir -p ~/web-content`
2. Crea un archivo HTML simple en ese directorio: `echo "<h1>Hola desde Docker!</h1>" > ~/web-content/index.html`
3. Ejecuta un contenedor con bind mount mapeando tu directorio al directorio de Nginx: `docker run -d -p 8080:80 -v ~/web-content:/usr/share/nginx/html --name web-bind nginx:alpine`
4. Verifica que el contenido del host se sirve accediendo a `http://localhost:8080`
5. Modifica el archivo HTML en el host y verifica que los cambios se reflejan inmediatamente sin reiniciar el contenedor
6. Limpia el contenedor usando `docker container rm -f web-bind`
#### Paso 4: Volúmenes - Named Volume
1. Crea un volumen nombrado usando `docker volume create web-data`
2. Lista los volúmenes usando `docker volume ls`
3. Inspecciona el volumen usando `docker volume inspect web-data`
4. Ejecuta un contenedor con el volumen nombrado: `docker run -d -p 8080:80 -v web-data:/usr/share/nginx/html --name web-volume nginx:alpine`
5. Copia contenido inicial al volumen usando `docker container exec web-volume sh -c "echo '<h1>Datos persistentes</h1>' > /usr/share/nginx/html/index.html"`
6. Verifica el contenido accediendo a `http://localhost:8080`
7. Elimina el contenedor (el volumen persiste): `docker container rm -f web-volume`
8. Recrea el contenedor con el mismo volumen y verifica que los datos persisten
9. Limpia todos los recursos: contenedor y volumen
#### Paso 5: Comandos de gestión avanzados
1. Ejecuta un comando dentro de un contenedor en ejecución usando `docker container exec -it web-server sh`
2. Explora el sistema de archivos del contenedor
3. Sal del contenedor con `exit`
4. Visualiza los logs del contenedor usando `docker container logs web-server`
5. Sigue los logs en tiempo real usando `docker container logs -f web-server`
6. Visualiza el uso de recursos usando `docker stats web-server`
7. Limpia recursos no utilizados usando los comandos `prune` apropiados
**Ejercicios de verificación**:
- ¿Cuál es la diferencia entre `docker container ls` y `docker container ls -a`?
- ¿Qué ocurre si intentas acceder a un contenedor sin publicar puertos?
- ¿Cuál es la diferencia entre un bind mount y un named volume?
- ¿Qué comando usarías para ver qué puertos están mapeados en un contenedor?
### Práctica 2: Construcción de Imágenes Docker
**Duración estimada**: 60-75 minutos
**Objetivo**: Crear Dockerfiles para aplicaciones personalizadas y construir imágenes. Esta práctica incluye dos ejemplos: un servidor web Nginx simple con contenido personalizado y una aplicación Flask.
#### Ejemplo A: Servidor Web Nginx con Contenido Personalizado
1. Crea un directorio para tu proyecto: `mkdir -p mi-servidor-web && cd mi-servidor-web`
2. Crea un archivo `index.html` con contenido HTML personalizado
3. Crea un directorio `assets/` y añade un archivo `style.css` con estilos CSS
4. Crea un `Dockerfile` que:
- Use la imagen base `nginx:1.25-alpine`
- Establezca el directorio de trabajo
- Copie el archivo `index.html` al directorio correcto de Nginx
- Copie el directorio `assets/` al directorio correcto
- Documente el puerto 80 con `EXPOSE`
5. Construye la imagen usando `docker build -t mi-servidor-web:1.0 .`
6. Verifica que la imagen se creó usando `docker image ls`
7. Ejecuta un contenedor con la imagen construida publicando el puerto 80
8. Verifica que el contenido personalizado se sirve correctamente
9. Verifica que los assets (CSS) se cargan correctamente
#### Ejemplo B: Aplicación Flask
1. Crea un directorio para tu aplicación Flask: `mkdir -p flask-app && cd flask-app`
2. Crea un archivo `app.py` con una aplicación Flask simple que tenga al menos dos rutas: `/` y `/health`
3. Crea un archivo `requirements.txt` con las dependencias necesarias (Flask)
4. Crea un `Dockerfile` que:
- Use la imagen base `python:3.11-slim`
- Establezca el directorio de trabajo
- Copie `requirements.txt` e instale las dependencias
- Copie el código de la aplicación
- Exponga el puerto 5000
- Defina el comando para ejecutar la aplicación
5. Construye la imagen usando `docker build -t flask-app:latest .`
6. Ejecuta un contenedor con la imagen construida publicando el puerto 5000
7. Verifica que la aplicación funciona correctamente accediendo a las rutas definidas
8. Visualiza los logs del contenedor para verificar que no hay errores
**Ejercicios de verificación**:
- ¿Por qué es importante especificar una versión específica de la imagen base en lugar de `latest`?
- ¿Qué ocurre si cambias el orden de las instrucciones `COPY` en el Dockerfile?
- ¿Cuál es la diferencia entre `EXPOSE` y el mapeo de puertos con `-p`?
- ¿Cómo podrías optimizar el Dockerfile para aprovechar mejor el cache de Docker?
### Práctica 3: DevContainers
**Duración estimada**: 45-60 minutos
**Objetivo**: Configurar un entorno de desarrollo Python usando DevContainers.
1. Crea un directorio para tu proyecto: `mkdir -p proyecto-python && cd proyecto-python`
2. Crea un directorio `.devcontainer` dentro del proyecto
3. Crea un archivo `.devcontainer/devcontainer.json` con la siguiente configuración básica:
- Especifica la imagen base de Python
- Configura extensiones de VS Code/Cursor para Python
- Define comandos de post-creación si es necesario
4. Abre el proyecto en VS Code/Cursor
5. Usa el comando "Reopen in Container" o "Dev Containers: Reopen in Container"
6. Espera a que el contenedor se cree y se configure
7. Verifica que estás trabajando dentro del contenedor (revisa el indicador en la esquina inferior izquierda)
8. Crea un archivo Python simple y verifica que las extensiones de Python funcionan correctamente
9. Instala dependencias si es necesario y verifica que funcionan dentro del contenedor
**Ejercicios de verificación**:
- ¿Qué ventajas tiene usar DevContainers frente a instalar Python localmente?
- ¿Cómo puedes verificar que estás trabajando dentro del contenedor?
- ¿Qué ocurre si otro desarrollador abre el mismo proyecto con DevContainers configurado?
### Práctica 4: Docker Compose con PostgreSQL y Gestión de Secretos
**Duración estimada**: 75-90 minutos
**Objetivo**: Crear una aplicación Flask con PostgreSQL usando Docker Compose, gestionando secretos de forma segura.
**⚠️ IMPORTANTE**: Antes de comenzar esta práctica, debes completar el **Tutorial Guiado de SQLAlchemy** disponible en `materiales/practica-4/tutorial-sqlalchemy.md`. Este tutorial te enseñará los conceptos fundamentales de SQLAlchemy que necesitarás para esta práctica.
#### Tutorial Previo: SQLAlchemy
**Lee y completa el tutorial** en `materiales/practica-4/tutorial-sqlalchemy.md` que cubre:
1. **Conceptos fundamentales**: ¿Qué es SQLAlchemy y un ORM?
2. **Configuración inicial**: Cómo configurar Flask-SQLAlchemy
3. **Definición de modelos**: Cómo crear modelos con diferentes tipos de columnas
4. **Operaciones CRUD**: Create, Read, Update, Delete con ejemplos
5. **Queries avanzadas**: Filtros, ordenamiento, paginación
6. **Uso en rutas Flask**: Ejemplo completo de API REST
**Archivos de referencia disponibles**:
- `materiales/practica-4/ejemplo-modelo-task.py`: Modelo completo de ejemplo
- `materiales/practica-4/ejemplo-queries.py`: Ejemplos de queries comunes
Una vez que hayas completado el tutorial y entiendas los conceptos básicos, puedes continuar con la práctica.
#### Preparación
1. Crea un directorio para tu proyecto: `mkdir -p flask-postgres && cd flask-postgres`
2. Crea la estructura de directorios necesaria para tu aplicación Flask
#### Configuración de Secretos
1. Crea un archivo `.env.example` con las variables de entorno necesarias (sin valores sensibles):
```
DB_NAME=flaskapp
DB_HOST=db
DB_USER=postgres
DB_PORT=5432
DB_PASSWORD=change_me_in_production
FLASK_ENV=development
SECRET_KEY=change_me_in_production
```
Las variables de entorno listadas en `.env.example` cumplen las siguientes funciones:
- `DB_NAME`: Nombre de la base de datos que usará tu aplicación Flask.
- `DB_HOST`: Dirección del host donde está el servidor PostgreSQL (usualmente el nombre del servicio de Docker Compose, por ejemplo `db`).
- `DB_USER`: Usuario con el que se accederá a la base de datos PostgreSQL.
- `DB_PORT`: Puerto en el que está corriendo PostgreSQL (el valor predeterminado es `5432`).
- `DB_PASSWORD`: Contraseña para el usuario de la base de datos (¡no uses nunca la de producción en desarrollo o en repositorio!).
- `FLASK_ENV`: Entorno de ejecución de Flask (`development`, `production`, etc.).
- `SECRET_KEY`: Clave secreta utilizada por Flask para gestionar sesiones y otros aspectos de seguridad en la aplicación.
Estas variables permiten configurar tu aplicación y la base de datos de manera flexible y segura, evitando guardar información sensible directamente en el código fuente.
2. Crea un archivo `.env` copiando desde `.env.example` y añade los valores reales necesarios. Recuerda que, según la sección **Convención de Docker Compose con archivo `.env`** de este mismo documento, Docker Compose usará automáticamente el archivo `.env` si está en el mismo directorio que tu `docker-compose.yml`.
3. Crea un archivo `.gitignore` que incluya `.env` para proteger tus secretos
#### Configuración de Docker Compose
Comencemos por la creación de la **base de datos**:
- En tu archivo `docker-compose.yml` define el servicio `db` usando la imagen `postgres:15-alpine`.
- Utiliza variables de entorno provenientes del archivo `.env` para configurar PostgreSQL (`POSTGRES_USER`, `POSTGRES_PASSWORD` y `POSTGRES_DB`), que son los nombres esperados por la imagen oficial de PostgreSQL.
- Añade un volumen para la persistencia de los datos de PostgreSQL.
- Añade un healthcheck para asegurarte de que la base de datos está lista antes de que otros servicios dependan de ella.
Ejemplo:
```
services:
db:
image: postgres:15-alpine
container_name: flask_db
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d postgres"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
```
Para verificar que hasta aquí todo va bien, utiliza el siguiente checklist:
- [ ] `docker compose up` inicia la base de datos sin errores.
- [ ] El contenedor (`flask_db`) está corriendo y saludable.
Para verificar rápidamente que tu base de datos fue creada, ejecuta:
```
docker compose exec db psql -U flask_user flaskapp_db -c '\l'
```
Si ves tu base de datos `flaskapp_db` en la lista, la configuración está bien.
Para la configuración de la **aplicación Flask**:
- En el mismo archivo `docker-compose.yml`, define el servicio `app`, que debe construirse usando un `Dockerfile`.
- Asegúrate de que pase las variables de entorno necesarias para Flask desde el archivo `.env`.
- Haz que `app` dependa del servicio `db`, y utiliza la condición de healthcheck para que sólo se inicie cuando la base de datos esté lista.
- Añade el mapeo de puertos necesario para acceder a la aplicación Flask desde el host.
Ejemplo:
```
app:
build: .
container_name: flask_app
env_file:
- .env
environment:
- FLASK_ENV=${FLASK_ENV}
- SECRET_KEY=${SECRET_KEY}
ports:
- "5000:5000"
depends_on:
db:
condition: service_healthy
volumes:
- ./app:/app/app
command: python -m flask run --host=0.0.0.0
```
Crea un `Dockerfile` para tu aplicación Flask
```Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
COPY app.py .
EXPOSE 5000
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"]
```
Crea un archivo `requirements.txt` con las dependencias necesarias (Flask, Flask-SQLAlchemy, psycopg2-binary, python-dotenv)
```
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
psycopg2-binary==2.9.9
python-dotenv==1.0.0
```
Comprobemos que hasta aquí todo está bien. Si intentamos construir la aplicación haciendo `docker compose build app`, dockerfile nos dará un mensaje de error diciendo que no encuentra `app.py` y `/app`, ya que no los hemos creado aún,los crearemos más adelante. Crearemos por lo tanto un archivo `app.py` vacío (dummy) y una carpeta así mismo vacía, solo para verificar que lo que hemos hecho hasta ahora funciona.
```bash
touch app.py ; mkdir app
```
Prueba a construir y levantar el entorno, anticipando que la aplicación `app` no funcionará correctamente.
#### Desarrollo de la Aplicación Flask con SQLAlchemy
La arquitectura de la aplicación Flask que construiremos es modular y está orientada a buenas prácticas para desarrollo web profesional. Se basa en los siguientes componentes principales:
- **Aplicación principal (`app.py`)**: Es el punto de entrada que utiliza el patrón de fábrica de aplicaciones (`create_app`) para facilitar la configuración y pruebas.
- **Paquete de la aplicación (`app/`)**: Contiene el código fuente organizado en módulos:
- `__init__.py`: Inicializa la aplicación Flask, carga la configuración desde variables de entorno, inicializa extensiones como SQLAlchemy y registra blueprints.
- `models.py`: Define los modelos de datos usando SQLAlchemy (ORM); por ejemplo, el modelo `Task`, que representa una tarea en la base de datos.
- `routes.py`: Define las rutas de la API mediante Blueprints, permitiendo operaciones CRUD sobre el modelo Task.
- **Gestión de entorno y configuración**:
- Se utilizan variables de entorno (por ejemplo, en `.env`) para gestionar credenciales y la cadena de conexión a la base de datos PostgreSQL, así como otros parámetros de configuración.
- **Persistencia**:
- Se usa PostgreSQL como base de datos relacional, a la que se accede mediante SQLAlchemy.
- **Contenerización y despliegue**:
- Se emplea Docker para asegurar un entorno consistente en desarrollo y despliegue, con orquestación mediante Docker Compose.
- **Estructura típica del proyecto**:
```
.
├── app.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── .env
└── app/
├── __init__.py
├── models.py
└── routes.py
```
Esta arquitectura separa claramente las responsabilidades, facilita el mantenimiento y crecimiento del proyecto, y permite realizar pruebas y despliegues de manera fiable y repetible.
El contenido de `app.py` será similar al siguiente:
```python
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run()
```
Aquí `app.py` es el punto de entrada de nuestra aplicación Flask. La función `create_app()` se importa desde el paquete `app` (es decir, desde el archivo `app/__init__.py`). Cuando ejecutamos este archivo, se crea la instancia de la aplicación, se inicializan las extensiones (como SQLAlchemy) y se cargan las configuraciones y blueprints.
El archivo `app/__init__.py` define la función `create_app()` y se ejecuta cuando hacemos `from app import create_app` o al importar el paquete `app` en general. Es decir, el código dentro de `__init__.py` se ejecuta automáticamente la primera vez que se importa el paquete o un módulo suyo, permitiendo que se configure la aplicación siguiendo el patrón de fábrica recomendado para aplicaciones Flask modulares y testeables.
##### Configurar Flask-SQLAlchemy**
Crea el archivo `app/__init__.py` que configure Flask-SQLAlchemy:
- Importa Flask, SQLAlchemy y dotenv
- Crea la instancia `db = SQLAlchemy()`
- Crea la función `create_app()` que:
- Configure la cadena de conexión desde variables de entorno
- Inicialice SQLAlchemy con `db.init_app(app)`
- Registre los blueprints
- Cree las tablas con `db.create_all()` dentro de un contexto de aplicación
A continuación mostramos un ejemplo de una posible implementación de `app/__init__.py`
```python
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from dotenv import load_dotenv
# Carga las variables de entorno desde el archivo .env, si existe
load_dotenv()
# Se crea una instancia de SQLAlchemy. db será utilizada para definir modelos y manejar la base de datos
db = SQLAlchemy()
def create_app():
"""
Crea y configura la aplicación Flask usando el patrón de fábrica.
"""
app = Flask(__name__)
# Configuración de la conexión a PostgreSQL usando variables de entorno
app.config['SQLALCHEMY_DATABASE_URI'] = (
f"postgresql://{os.environ.get('DB_USER')}:"
f"{os.environ.get('DB_PASSWORD')}@"
f"{os.environ.get('DB_HOST')}:"
f"{os.environ.get('DB_PORT', '5432')}/"
f"{os.environ.get('DB_NAME')}"
)
# Opcional: desactiva el seguimiento de modificaciones para ahorrar recursos
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# Importar modelos (debe hacerse después de crear db)
from app.models import Task
# Inicializa SQLAlchemy con la app de Flask
db.init_app(app)
# Crea las tablas en la base de datos (solo si aún no existen)
# Esto debe hacerse dentro del contexto de la aplicación
with app.app_context():
db.create_all()
return app
```
Explicación de los elementos clave:
- **load_dotenv()**: Permite cargar automáticamente las variables definidas en el archivo `.env`, facilitando la configuración sin tener que codificar credenciales.
- **db = SQLAlchemy()**: Crea una instancia global de SQLAlchemy, que será utilizada por tus modelos y para integrar la base de datos con Flask.
- **def create_app():** Define la función de fábrica recomendada por Flask, la cual crea la aplicación, realiza todas las configuraciones necesarias y la devuelve lista para usarse.
- **app.config["SQLALCHEMY_DATABASE_URI"] = ...**: Define la cadena de conexión a PostgreSQL utilizando las variables de entorno. Esto facilita portabilidad y seguridad.
- **Formato de la cadena de conexión**:
La variable `SQLALCHEMY_DATABASE_URI` debe tener el siguiente formato para PostgreSQL:
```
postgresql://usuario:contraseña@host:puerto/nombre_base_de_datos
```
Por ejemplo:
```
postgresql://admin:clave123@localhost:5432/mibase
```
En el código de ejemplo anterior, la URI se arma tomando los valores de usuario, contraseña, host, puerto y nombre de la base de datos desde variables de entorno, asegurando que no queden credenciales sensibles en el código fuente.
- **db.init_app(app)**: Asocia la instancia global de SQLAlchemy a tu aplicación Flask.
- **db.create_all()**: Crea todas las tablas definidas en los modelos si aún no existen, dentro del contexto de la app.
Ahora definiremos el Modelo `Task`, para ello crea el archivo `app/models.py`, que debe realizar las siguientes acciones:
- Importa `db` desde `app`
- Define un modelo `Task` que herede de `db.Model`
- Añade las columnas: `id` (Integer, primary_key), `title` (String, nullable=False), `description` (Text), `completed` (Boolean, default=False), `created_at` (DateTime, default=datetime.utcnow)
- Añade el método `to_dict()` para convertir el modelo a diccionario (útil para JSON)
A continuación se muestra el archivo `app/models.py` junto con una breve descripción de los elementos clave:
- **db.Model**: Esta es la clase base de SQLAlchemy para definir modelos (tablas).
- **Columnas**:
- `id`: Identificador único (entero, clave primaria).
- `title`: Título de la tarea (cadena, obligatorio).
- `description`: Descripción de la tarea (texto, opcional).
- `completed`: Estado de la tarea (booleano, por defecto `False`).
- `created_at`: Fecha de creación (fecha y hora, por defecto la actual).
- **to_dict()**: Método para retornar un diccionario con los campos del modelo. Es útil para generar respuestas en formato JSON en la API.
Código ejemplo: `app/models.py`
```python
# app/models.py
from datetime import datetime
from app import db
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(128), nullable=False)
description = db.Column(db.Text)
completed = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
"id": self.id,
"title": self.title,
"description": self.description,
"completed": self.completed,
"created_at": self.created_at.isoformat() if self.created_at else None
}
```
**Puntos clave:**
- El modelo `Task` define la estructura de las tareas en la base de datos.
- El método `to_dict()` permite serializar fácilmente una instancia del modelo para retornar datos en las rutas de la API.
- Se recomienda importar `db` desde el paquete `app` para mantener una única instancia global.
- El campo `created_at` usa `datetime.utcnow` para almacenar automáticamente la fecha y hora actual en cada nueva tarea.
Con todo esto ya estamos preparados para probar que nuestra aplicación arranca, aunque solo sea para conectar con la base de datos y crear la tabla correspondiente al modelo `Task`. Prueba a arrancar la aplicación completa con `docker compose up` y corrige todos los errores que haya hasta que la aplicación arranque correctamente.
Para verificar si la tabla se ha creado correctamente, con el contenedor de `db` en marcha, vamos a ejecutar en él la orden correspondiente del cliente `psql`:
```
docker compose exec db psql -U flask_user flaskapp_db -c '\dt'
docker compose exec db psql -U flask_user flaskapp_db -c '\d task'
```
##### Configuración de los endpoints
Un **endpoint** es una URL específica dentro de una API web a la que se puede enviar una solicitud (por ejemplo, GET, POST, PUT, DELETE) para interactuar con los recursos o datos que ofrece el servidor. Cada endpoint corresponde a una ruta o dirección donde se realiza una acción concreta, como listar tareas, obtener un detalle, crear, actualizar o eliminar un recurso. Por ejemplo, `/tasks` es un endpoint para trabajar con tareas.
En Flask, los endpoints se implementan mediante funciones asociadas a rutas usando decoradores como `@app.route` o dentro de un Blueprint, permitiendo que tu aplicación responda a solicitudes HTTP en esas rutas específicas. En esta aplicación usaremos Blueprints.
Un **Blueprint** en Flask es una forma de organizar y reutilizar conjuntos de rutas, controladores y otros recursos dentro de una aplicación web. Facilita la división de la app en componentes modulares y reutilizables, lo cual es especialmente útil en aplicaciones grandes. Los Blueprints permiten registrar rutas y controladores sin necesidad de adjuntarlas directamente a la app principal, y después se "registran" en la aplicación Flask cuando corresponda.
Por ejemplo, puedes tener un `Blueprint` para las rutas relacionadas con tareas (`tasks`), otro para usuarios, etc. Así, el código de cada módulo se mantiene separado y más fácil de mantener.
Ahora, crea el archivo `app/routes.py` e implementa lo siguiente:
- **Crea un Blueprint para las rutas**
```python
from flask import Blueprint
tasks_bp = Blueprint('tasks', __name__)
```
- **Implementa las siguientes rutas usando SQLAlchemy:**
- `GET /tasks`: Listar todas las tareas (usa `Task.query.all()`)
- `GET /tasks/<id>`: Obtener una tarea específica (usa `Task.query.get_or_404(id)`)
- `POST /tasks`: Crear una nueva tarea (usa `db.session.add()` y `db.session.commit()`)
- `PUT /tasks/<id>`: Actualizar una tarea (modifica atributos y `db.session.commit()`)
- `DELETE /tasks/<id>`: Eliminar una tarea (usa `db.session.delete()` y `db.session.commit()`)
A continuación tienes algunos ejemplos sobre cómo consultar, crear, actualizar y borrar registros usando SQLAlchemy partiendo de que ya tienes definido el modelo `Task` y la instancia `db`.
A continuación tienes el contenido que podría llevar tu archivo `app/routes.py`, seguido de una breve explicación de los elementos más importantes:
```python
from flask import Blueprint, request, jsonify
from app.models import Task, db
# Definimos el Blueprint para las rutas relacionadas con las tareas
tasks_bp = Blueprint('tasks', __name__)
# Ruta para listar todas las tareas
@tasks_bp.route('/tasks', methods=['GET'])
def get_tasks():
tasks = Task.query.all()
return jsonify([task.to_dict() for task in tasks]), 200
# Ruta para obtener una tarea específica
@tasks_bp.route('/tasks/<int:id>', methods=['GET'])
def get_task(id):
task = Task.query.get_or_404(id)
return jsonify(task.to_dict()), 200
# Ruta para crear una nueva tarea
@tasks_bp.route('/tasks', methods=['POST'])
def create_task():
data = request.get_json()
# Asegúrate de que el campo 'title' es obligatorio
if not data or 'title' not in data:
return jsonify({'error': 'Missing title'}), 400
task = Task(
title=data['title'],
description=data.get('description', ''),
completed=data.get('completed', False)
)
db.session.add(task)
db.session.commit()
return jsonify(task.to_dict()), 201
# Ruta para actualizar una tarea existente
@tasks_bp.route('/tasks/<int:id>', methods=['PUT'])
def update_task(id):
task = Task.query.get_or_404(id)
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
task.title = data.get('title', task.title)
task.description = data.get('description', task.description)
task.completed = data.get('completed', task.completed)
db.session.commit()
return jsonify(task.to_dict()), 200
# Ruta para eliminar una tarea
@tasks_bp.route('/tasks/<int:id>', methods=['DELETE'])
def delete_task(id):
task = Task.query.get_or_404(id)
db.session.delete(task)
db.session.commit()
return jsonify({'result': 'Task deleted'}), 200
```
```
**Explicación de los elementos clave:**
- Utilizamos un **Blueprint** (`tasks_bp`) para agrupar las rutas de tareas, permitiendo una estructura modular y escalable.
- Cada endpoint implementa una de las operaciones CRUD para el recurso `Task`:
- `GET /tasks`: Devuelve todas las tareas serializadas.
- `GET /tasks/<id>`: Devuelve una sola tarea según ID o un error 404 si no existe.
- `POST /tasks`: Permite crear una nueva tarea, validando el campo obligatorio `title`.
- `PUT /tasks/<id>`: Modifica los campos de una tarea específica según los datos recibidos.
- `DELETE /tasks/<id>`: Elimina la tarea especificada y retorna una confirmación.
- Todos los endpoints usan SQLAlchemy y el método `to_dict()` del modelo para serializar las instancias de tarea a JSON.
- Se retorna siempre un objeto JSON y el código de estado HTTP correspondiente.
Recuerda registrar el Blueprint en `app/__init__.py` para que las rutas estén activas. Añade lo siguiente justo antes del final de la función `create_app`:
```python
from app.routes import tasks_bp
app.register_blueprint(tasks_bp)
```
De esta manera, separas claramente la lógica de las rutas y mantienes tu aplicación organizada y mantenible.
#### Ejecución y Verificación
1. Construye e inicia los servicios usando `docker compose up --build`
2. Verifica que ambos servicios están ejecutándose correctamente
3. Verifica que la aplicación se conecta a PostgreSQL accediendo a una ruta que consulte la base de datos
4. Crea algunos registros usando las rutas POST de tu API
Por ejemplo, para crear una nueva tarea se puede hacer:
```bash
curl -X POST http://localhost:5000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Estudiar Flask", "description": "Leer la documentación de Flask", "completed": false}'
```
Este comando envía un JSON para crear una nueva tarea con los campos requeridos. Puedes modificar los valores según lo necesites.
5. Lista los registros usando las rutas GET de tu API
6. Verifica que los datos persisten reiniciando los servicios con `docker compose restart`
7. Detén los servicios usando `docker compose down` (los volúmenes persisten)
8. Vuelve a iniciar los servicios y verifica que los datos siguen ahí
**Ejercicios de verificación**:
- ¿Por qué es importante incluir `.env` en `.gitignore`?
- ¿Qué ocurre si no defines un healthcheck para PostgreSQL?
- ¿Cómo puedes verificar que los datos persisten en el volumen?
- ¿Qué comando usarías para ver los logs de ambos servicios simultáneamente?
- ¿Cuál es la diferencia entre `Task.query.get()` y `Task.query.get_or_404()`?
- ¿Por qué es necesario llamar a `db.session.commit()` después de modificar datos?
- ¿Qué ocurre si olvidas llamar a `db.session.commit()` después de `db.session.add()`?
**Referencia adicional**: Si tienes dudas sobre la implementación, puedes consultar el ejemplo completo en `materiales/practica-4/ejemplo-completo/`, pero intenta crear tu propia solución primero usando el tutorial de SQLAlchemy como guía.
## Troubleshooting Básico
### Problemas Comunes
**1. "Cannot connect to Docker daemon"**
- Verifica que Docker Desktop está ejecutándose
- En Linux, verifica que tu usuario tiene permisos para acceder a Docker
**2. Puerto ya en uso**
- Cambia el puerto en el mapeo: `-p 5001:5000`
- O detén el proceso/contenedor que está usando el puerto
**3. Contenedor se detiene inmediatamente**
- Verifica los logs: `docker container logs <contenedor>`
- Asegúrate de que el proceso principal no termina inmediatamente
**4. Volumen no se monta correctamente**
- Verifica que las rutas son correctas (absolutas o relativas)
- Verifica los permisos del directorio host
- Inspecciona el montaje: `docker container inspect <contenedor>`
**5. Variables de entorno no se cargan**
- Verifica que el archivo `.env` existe en el directorio correcto
- Verifica la sintaxis del archivo `.env`
- Usa `docker compose config` para ver la configuración final
**6. Base de datos no está lista**
- Espera a que el healthcheck de PostgreSQL pase
- Verifica los logs de PostgreSQL: `docker compose logs db`
- Implementa lógica de retry en tu aplicación
## Ejercicios Adicionales (Opcionales)
### Ejercicio 1: Optimización de Dockerfile
Toma el Dockerfile de la aplicación Flask y optimízalo:
- Usa multi-stage builds para reducir el tamaño final
- Combina comandos RUN relacionados
- Añade `.dockerignore` para excluir archivos innecesarios
### Ejercicio 2: Configuración Avanzada de DevContainers
Crea un DevContainer que incluya:
- Python con versiones específicas
- Extensiones personalizadas de VS Code/Cursor
- Comandos de post-creación que instalen dependencias
- Configuración de forward de puertos
### Ejercicio 3: Aplicación Multi-Servicio
Extiende la aplicación Flask + PostgreSQL para incluir:
- Un servicio Redis para caché
- Configuración de redes personalizadas
- Healthchecks para todos los servicios
- Documentación completa en README.md
### Ejercicio 4: Gestión de Secretos Avanzada
Implementa:
- Rotación de credenciales
- Diferentes archivos `.env` para desarrollo y testing
- Validación de variables de entorno requeridas
- Documentación de todas las variables necesarias
## Recursos de Referencia
### Documentación Oficial
- **Docker Documentation**: https://docs.docker.com/
- **Docker Compose Documentation**: https://docs.docker.com/compose/
- **DevContainers Specification**: https://containers.dev/
- **Flask Documentation**: https://flask.palletsprojects.com/
- **SQLAlchemy Documentation**: https://www.sqlalchemy.org/
### Tutoriales y Guías
- **Docker Get Started**: https://docs.docker.com/get-started/
- **Docker Best Practices**: https://docs.docker.com/develop/dev-best-practices/
- **Flask-SQLAlchemy Quickstart**: https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/quickstart/
### Herramientas Útiles
- **Docker Desktop**: https://www.docker.com/products/docker-desktop/
- **VS Code Dev Containers Extension**: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers
- **Docker Compose File Reference**: https://docs.docker.com/compose/compose-file/
### Comunidad y Soporte
- **Docker Community Forums**: https://forums.docker.com/
- **Stack Overflow - Docker Tag**: https://stackoverflow.com/questions/tagged/docker
## Notas Finales
- Tómate tu tiempo para entender cada concepto antes de pasar al siguiente
- Experimenta con los comandos y configuraciones
- No tengas miedo de cometer errores: son parte del aprendizaje
- Consulta la documentación oficial cuando tengas dudas
- Practica regularmente para consolidar los conceptos
¡Buena suerte con tu aprendizaje de Docker!