Guía completa para implementar un pipeline de CI/CD que construye y despliega automáticamente una aplicación FastAPI con PostgreSQL usando GitHub Actions, Docker y un VPS.
Este manual te guiará a través del proceso de creación de un proyecto FastAPI con PostgreSQL, configuración de GitHub Actions para construir imágenes Docker y almacenarlas en GitHub Packages, y finalmente desplegar la aplicación en un VPS usando Docker Compose con actualizaciones automáticas mediante CI/CD.
Primero, crearemos la estructura básica de nuestro proyecto FastAPI con PostgreSQL.
Crea un archivo requirements.txt
con las dependencias:
fastapi==0.95.2 uvicorn==0.22.0 sqlalchemy==2.0.15 psycopg2-binary==2.9.6 python-dotenv==1.0.0
Crea el archivo principal de la aplicación en app/main.py
:
from fastapi import FastAPI, HTTPException from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from pydantic import BaseModel import os from dotenv import load_dotenv load_dotenv() # Configuración de la base de datos DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@db:5432/fastapi_db") engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() # Modelo SQLAlchemy class Item(Base): __tablename__ = "items" id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) description = Column(String, index=True) # Esquema Pydantic class ItemCreate(BaseModel): name: str description: str class ItemResponse(ItemCreate): id: int class Config: orm_mode = True # Crear tablas Base.metadata.create_all(bind=engine) app = FastAPI() @app.post("/items/", response_model=ItemResponse) def create_item(item: ItemCreate): db = SessionLocal() db_item = Item(**item.dict()) db.add(db_item) db.commit() db.refresh(db_item) db.close() return db_item @app.get("/items/{item_id}", response_model=ItemResponse) def read_item(item_id: int): db = SessionLocal() item = db.query(Item).filter(Item.id == item_id).first() db.close() if item is None: raise HTTPException(status_code=404, detail="Item not found") return item @app.get("/") def read_root(): return {"message": "Welcome to FastAPI with PostgreSQL!"}
Crea un archivo .env.example
que servirá como plantilla para las variables de entorno:
# Configuración de la base de datos DATABASE_URL=postgresql://user:password@db:5432/fastapi_db # Configuración de FastAPI APP_HOST=0.0.0.0 APP_PORT=8000
.env
real en Git. Asegúrate de agregarlo a tu .gitignore
.
Crea un Dockerfile
para construir la imagen de tu aplicación:
# Etapa de construcción FROM python:3.9-slim as builder WORKDIR /app # Instalar dependencias del sistema RUN apt-get update && apt-get install -y \ gcc \ python3-dev \ && rm -rf /var/lib/apt/lists/* # Copiar requirements e instalar dependencias COPY requirements.txt . RUN pip install --user -r requirements.txt # Etapa final FROM python:3.9-slim WORKDIR /app # Copiar dependencias instaladas desde la etapa de construcción COPY --from=builder /root/.local /root/.local COPY . . # Asegurarse de que los scripts en .local sean ejecutables ENV PATH=/root/.local/bin:$PATH # Variables de entorno por defecto ENV APP_HOST=0.0.0.0 ENV APP_PORT=8000 # Exponer el puerto EXPOSE ${APP_PORT} # Comando para ejecutar la aplicación CMD ["uvicorn", "app.main:app", "--host", "${APP_HOST}", "--port", "${APP_PORT}", "--reload"]
Crea un archivo docker-compose.yml
para orquestar los servicios:
version: '3.8' services: app: build: . ports: - "${APP_PORT}:${APP_PORT}" environment: - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME} - APP_HOST=${APP_HOST} - APP_PORT=${APP_PORT} depends_on: - db restart: unless-stopped db: image: postgres:13-alpine environment: - POSTGRES_USER=${DB_USER} - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=${DB_NAME} volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" restart: unless-stopped volumes: postgres_data:
${DB_USER}
, ${DB_PASSWORD}
, etc., se pueden definir en un archivo .env
o pasarse directamente al comando docker-compose.
Crea el directorio .github/workflows
y dentro dos archivos:
name: CI Pipeline on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:13-alpine env: POSTGRES_USER: test_user POSTGRES_PASSWORD: test_password POSTGRES_DB: test_db ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest - name: Run tests env: DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db run: | pytest -v build: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v4 with: images: ghcr.io/${{ github.repository }} tags: | type=sha type=ref,event=branch - name: Build and push Docker image uses: docker/build-push-action@v4 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }}
name: CD Pipeline on: workflow_run: workflows: ["CI Pipeline"] branches: [main] types: - completed jobs: deploy: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: SSH to VPS and deploy uses: appleboy/ssh-action@v0.1.10 with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} script: | cd /opt/fastapi-app docker-compose down docker image prune -af docker-compose pull docker-compose up -d
Para que el workflow de despliegue funcione, necesitas configurar los siguientes secrets en tu repositorio GitHub:
VPS_HOST
- Dirección IP o dominio de tu VPSVPS_USER
- Usuario SSH del VPS (normalmente 'root' o un usuario con privilegios)VPS_SSH_KEY
- Clave privada SSH para autenticación en el VPSConéctate a tu VPS via SSH y ejecuta los siguientes comandos:
Crea un archivo docker-compose.yml
similar al de tu proyecto, pero con las configuraciones de producción:
version: '3.8' services: app: image: ghcr.io/tu-usuario/tu-repositorio:main ports: - "8000:8000" environment: - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME} - APP_HOST=0.0.0.0 - APP_PORT=8000 depends_on: - db restart: unless-stopped db: image: postgres:13-alpine environment: - POSTGRES_USER=${DB_USER} - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=${DB_NAME} volumes: - postgres_data:/var/lib/postgresql/data restart: unless-stopped volumes: postgres_data:
Crea un archivo .env
en el VPS con tus configuraciones de producción:
# Configuración de la base de datos DB_USER=prod_user DB_PASSWORD=una_contraseña_segura_y_compleja DB_NAME=fastapi_prod # Configuración de FastAPI APP_HOST=0.0.0.0 APP_PORT=8000
.env
tenga permisos restrictivos (chmod 600) y que no sea accesible por otros usuarios.
Para que tu VPS pueda descargar imágenes de GitHub Packages:
Después del despliegue, puedes verificar que todo funciona correctamente:
Deberías recibir una respuesta como:
{"message":"Welcome to FastAPI with PostgreSQL!"}
Deberías recibir una respuesta como:
{"name":"test","description":"test item","id":1}
docker-compose logs
Has configurado exitosamente un pipeline completo de CI/CD para una aplicación FastAPI con PostgreSQL. Cada vez que hagas cambios y los subas a la rama main, GitHub Actions se encargará de: