Dockerizar una aplicación fullstack completa — FastAPI + PostgreSQL + React/Vite — de forma que cualquier persona pueda ejecutarla con docker compose up sin instalar Python, Node.js ni PostgreSQL en su máquina.
# Clonar o usar una app fullstack existente del curso
# Si no tienes una, puedes usar esta estructura de ejemplo:
mkdir lab-web-docker-fullstack
cd lab-web-docker-fullstack
mkdir backend frontendEstructura objetivo del proyecto:
lab-web-docker-fullstack/
├── docker-compose.yml
├── .env
├── .env.example
├── .dockerignore
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── main.py
│ └── database.py
└── frontend/
├── Dockerfile
├── nginx.conf
├── package.json
└── src/# backend/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from database import engine, Base, SessionLocal
from sqlalchemy import Column, Integer, String, text
# Crear tablas al arrancar (en producción real usarías migraciones)
Base.metadata.create_all(bind=engine)
app = FastAPI(title="Lab Docker API")
# CORS: permitir peticiones desde el frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost", "http://localhost:80"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
def health_check():
"""Endpoint que usa el healthcheck del compose para verificar que la API está viva."""
return {"status": "ok"}
@app.get("/api/items")
def get_items():
"""Devuelve items de ejemplo."""
return {"items": ["manzana", "pera", "naranja"]}# backend/database.py
import os
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Leer la URL de la base de datos desde la variable de entorno
DATABASE_URL = os.environ.get("DATABASE_URL", "postgresql://appuser:apppassword@postgres:5432/appdb")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()# backend/requirements.txt
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
# backend/Dockerfile
# Imagen base ligera
FROM python:3.11-slim
# Evitar archivos .pyc y forzar logs sin buffer
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Crear usuario sin privilegios para ejecutar la app
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
WORKDIR /app
# Instalar dependencias antes que el código (aprovecha la caché)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar el código y asignar propiedad al usuario sin privilegios
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 8000
# --reload solo para desarrollo — en producción quitar el flag
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]<!-- frontend/index.html (en la raíz del proyecto Vite) -->
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8" /><title>Lab Docker</title></head>
<body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body>
</html>// frontend/src/App.jsx
import { useState, useEffect } from "react"
// La URL de la API se inyecta en tiempo de build desde la variable de entorno
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000"
function App() {
const [items, setItems] = useState([])
const [status, setStatus] = useState("cargando...")
useEffect(() => {
// Verificar que la API está viva
fetch(`${API_URL}/health`)
.then(r => r.json())
.then(d => setStatus(d.status))
.catch(() => setStatus("error conectando con la API"))
// Cargar items
fetch(`${API_URL}/api/items`)
.then(r => r.json())
.then(d => setItems(d.items))
}, [])
return (
<div>
<h1>Lab Docker Fullstack</h1>
<p>Estado API: <strong>{status}</strong></p>
<ul>{items.map(i => <li key={i}>{i}</li>)}</ul>
</div>
)
}
export default App# frontend/Dockerfile
# ── Etapa de build ─────────────────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci --silent
COPY . .
# ARG recibe el valor en tiempo de build desde docker-compose
ARG VITE_API_URL=http://localhost:8000
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build
# ── Etapa de producción ────────────────────────────────────────────────────
FROM nginx:alpine AS runner
# Copiar los archivos estáticos construidos
COPY --from=builder /app/dist /usr/share/nginx/html
# Configuración de nginx para que React Router funcione
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# frontend/nginx.conf
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}# docker-compose.yml
services:
postgres:
image: postgres:16-alpine
container_name: lab-postgres
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppassword
POSTGRES_DB: appdb
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
api:
build:
context: ./backend
container_name: lab-api
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://appuser:apppassword@postgres:5432/appdb
SECRET_KEY: ${SECRET_KEY:-clave-desarrollo-insegura}
volumes:
- ./backend:/app # hot-reload en desarrollo
networks:
- app-network
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
frontend:
build:
context: ./frontend
args:
VITE_API_URL: http://localhost:8000
container_name: lab-frontend
ports:
- "80:80"
networks:
- app-network
depends_on:
- api
restart: unless-stopped
volumes:
postgres-data:
networks:
app-network:
driver: bridge# .env (NO subir a git)
SECRET_KEY=mi-clave-secreta-de-desarrollo# .env.example (SÍ subir a git)
SECRET_KEY=# .dockerignore
node_modules/
__pycache__/
*.pyc
.venv/
.env
.env.*
!.env.example
dist/
.git/
*.md
# Construir y levantar todo
docker compose up -d --build
# Verificar que todos los servicios están healthy
docker compose ps
# Ver logs en tiempo real
docker compose logs -f api
# Probar la API
curl http://localhost:8000/health
curl http://localhost:8000/api/items
# Abrir el frontend
# http://localhost en el navegador
# Entrar en la base de datos
docker compose exec postgres psql -U appuser -d appdb
# Parar todo al terminar
docker compose down- El proyecto arranca completamente con
docker compose up -d --build -
docker compose psmuestra los 3 servicios como "running" o "healthy" - El endpoint
http://localhost:8000/healthresponde{"status": "ok"} - El frontend es accesible en
http://localhosty muestra los items de la API - Los datos de PostgreSQL persisten después de
docker compose restart - El archivo
.envno está versionado en git (comprobarlo congit status)
- Añadir un healthcheck al servicio
apique verifique/health - Hacer que
frontenddependa del healthcheck deapi - Añadir el servicio
adminer(puerto 8080) con el profiledevpara administrar la BD visualmente - Añadir un servicio
nginx-proxyque sirva tanto la API como el frontend desde el mismo puerto 80
