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 logsHas 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: