Manual de Despliegue CI/CD

FastAPI + PostgreSQL + Next.js con GitHub Actions, Docker y VPS

Introducci贸n

Este manual detalla c贸mo configurar un pipeline completo de CI/CD para una aplicaci贸n web que utiliza:

Nota: Este manual asume que tienes conocimientos b谩sicos de Docker, GitHub, Python, JavaScript y administraci贸n de servidores Linux.

Estructura del Proyecto

Nuestro proyecto tendr谩 la siguiente estructura:

  • backend/
    • Dockerfile
    • requirements.txt
    • app/
      • main.py
      • models.py
      • database.py
  • frontend/
    • Dockerfile
    • package.json
    • ... (otros archivos de Next.js)
  • docker-compose.yml
  • docker-compose.prod.yml
  • .github/
    • workflows/
      • ci-cd.yml
  • README.md

Configuraci贸n Inicial

1. Configurar el Backend con FastAPI

Crea un directorio backend y configura una aplicaci贸n FastAPI b谩sica.

backend/requirements.txt

fastapi==0.95.2
uvicorn==0.22.0
sqlalchemy==2.0.15
psycopg2-binary==2.9.6
python-dotenv==1.0.0

backend/app/main.py

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from . import models, database
from .database import SessionLocal, engine

models.Base.metadata.create_all(bind=engine)

app = FastAPI()

# Dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/")
def read_root():
    return {"message": "Welcome to FastAPI backend"}

@app.get("/items/")
def read_items(db: Session = Depends(get_db)):
    items = db.query(models.Item).all()
    return items

backend/app/models.py

from sqlalchemy import Column, Integer, String
from .database import Base

class Item(Base):
    __tablename__ = "items"
    
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    description = Column(String, index=True)

backend/app/database.py

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
from dotenv import load_dotenv

load_dotenv()

SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@postgres:5432/dbname")

engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

2. Configurar el Frontend con Next.js

Crea una aplicaci贸n Next.js b谩sica en el directorio frontend.

npx create-next-app@latest frontend

Configura una p谩gina que consuma la API de FastAPI:

frontend/pages/index.js

import { useState, useEffect } from 'react'

export default function Home() {
  const [items, setItems] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await fetch('/api/items')
        const data = await res.json()
        setItems(data)
      } catch (error) {
        console.error('Error fetching data:', error)
      } finally {
        setLoading(false)
      }
    }
    
    fetchData()
  }, [])

  return (
    <div>
      <h1>Welcome to Next.js Frontend</h1>
      {loading ? (
        <p>Loading...</p>
      ) : (
        <ul>
          {items.map(item => (
            <li key={item.id}>{item.title}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

frontend/pages/api/items.js

export default async function handler(req, res) {
  try {
    const response = await fetch('http://backend:8000/items/')
    const data = await response.json()
    res.status(200).json(data)
  } catch (error) {
    res.status(500).json({ error: 'Error fetching data from backend' })
  }
}

3. Configurar Docker y Docker Compose

backend/Dockerfile

FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

frontend/Dockerfile

FROM node:18-alpine

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install

COPY . .

RUN npm run build

CMD ["npm", "start"]

docker-compose.yml (desarrollo)

version: '3.8'

services:
  postgres:
    image: postgres:13
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: dbname
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  backend:
    build: ./backend
    volumes:
      - ./backend:/app
    environment:
      DATABASE_URL: postgresql://user:password@postgres:5432/dbname
    ports:
      - "8000:8000"
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped

  frontend:
    build: ./frontend
    volumes:
      - ./frontend:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    depends_on:
      - backend
    restart: unless-stopped

volumes:
  postgres_data:

docker-compose.prod.yml (producci贸n)

version: '3.8'

services:
  postgres:
    image: postgres:13
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  backend:
    image: ghcr.io/${GITHUB_REPOSITORY}/backend:${IMAGE_TAG}
    environment:
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - app-network

  frontend:
    image: ghcr.io/${GITHUB_REPOSITORY}/frontend:${IMAGE_TAG}
    depends_on:
      - backend
    restart: unless-stopped
    networks:
      - app-network

  apache:
    image: httpd:2.4
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./apache-config:/usr/local/apache2/conf
      - ./frontend/.next/static:/usr/local/apache2/htdocs/_next/static
    depends_on:
      - frontend
    networks:
      - app-network

volumes:
  postgres_data:

networks:
  app-network:
    driver: bridge

Nota: Necesitar谩s configurar un archivo de configuraci贸n de Apache para manejar las rutas correctamente. Esto se explicar谩 m谩s adelante en la secci贸n de configuraci贸n del VPS.

Configuraci贸n de GitHub Actions

1. Crear el workflow de CI/CD

Crea un directorio .github/workflows y a帽ade el siguiente archivo:

.github/workflows/ci-cd.yml

name: CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  IMAGE_TAG: ${{ github.sha }}
  DB_USER: user_prod
  DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
  DB_NAME: dbname_prod

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
      
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Build and push backend image
        uses: docker/build-push-action@v4
        with:
          context: ./backend
          push: true
          tags: ghcr.io/${{ github.repository }}/backend:${{ env.IMAGE_TAG }}
      
      - name: Build and push frontend image
        uses: docker/build-push-action@v4
        with:
          context: ./frontend
          push: true
          tags: ghcr.io/${{ github.repository }}/frontend:${{ env.IMAGE_TAG }}
  
  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
      
      - name: Install SSH key
        uses: webfactory/ssh-agent@v0.7.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
      
      - name: Copy files to server
        run: |
          scp -o StrictHostKeyChecking=no docker-compose.prod.yml ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/app/docker-compose.prod.yml
          scp -o StrictHostKeyChecking=no -r apache-config ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/app/
      
      - name: Deploy to VPS
        run: |
          ssh -o StrictHostKeyChecking=no ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << EOF
            cd /app
            echo ${{ secrets.DB_PASSWORD }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker pull ghcr.io/${{ github.repository }}/backend:${{ env.IMAGE_TAG }}
            docker pull ghcr.io/${{ github.repository }}/frontend:${{ env.IMAGE_TAG }}
            IMAGE_TAG=${{ env.IMAGE_TAG }} DB_USER=${{ env.DB_USER }} DB_PASSWORD=${{ secrets.DB_PASSWORD }} DB_NAME=${{ env.DB_NAME }} GITHUB_REPOSITORY=${{ github.repository }} docker-compose -f docker-compose.prod.yml up -d
          EOF

2. Configurar secrets en GitHub

Ve a la configuraci贸n de tu repositorio en GitHub y a帽ade los siguientes secrets:

  • DB_PASSWORD: Contrase帽a para PostgreSQL en producci贸n
  • SSH_PRIVATE_KEY: Clave privada SSH para acceder al VPS
  • SSH_USER: Usuario SSH del VPS (ej. root)
  • SSH_HOST: Direcci贸n IP o dominio del VPS

Advertencia: Nunca commits credenciales o informaci贸n sensible en tu repositorio. Siempre usa GitHub Secrets para manejar esta informaci贸n.

Configuraci贸n del VPS

1. Preparar el servidor

Con茅ctate a tu VPS via SSH y ejecuta los siguientes comandos:

sudo apt update && sudo apt upgrade -y
sudo apt install docker docker-compose apache2 -y
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker $USER
mkdir -p /app/apache-config

Descon茅ctate y vuelve a conectarte para que los cambios de grupo surtan efecto.

2. Configurar Apache como reverse proxy

Crea un archivo de configuraci贸n para Apache:

apache-config/httpd.conf

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

ServerRoot "/usr/local/apache2"
Listen 80

<VirtualHost *:80>
    ServerName yourdomain.com
    
    # Configuraci贸n del proxy para el frontend
    ProxyPass / http://frontend:3000/
    ProxyPassReverse / http://frontend:3000/
    
    # Configuraci贸n del proxy para la API
    ProxyPass /api http://backend:8000/
    ProxyPassReverse /api http://backend:8000/
    
    # Configuraci贸n para archivos est谩ticos de Next.js
    Alias /_next/static /usr/local/apache2/htdocs/_next/static
    <Directory "/usr/local/apache2/htdocs/_next/static">
        Require all granted
    </Directory>
    
    ErrorLog /proc/self/fd/2
    CustomLog /proc/self/fd/1 common
</VirtualHost>

Nota: Aseg煤rate de reemplazar yourdomain.com con tu dominio real o la IP del servidor.

3. Configurar firewall

Aseg煤rate de que los puertos necesarios est茅n abiertos:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 22/tcp
sudo ufw enable

Despliegue Autom谩tico

1. Probar el flujo CI/CD

Con todo configurado, cada vez que hagas un push a la rama main:

  1. GitHub Actions construir谩 las im谩genes de Docker
  2. Las subir谩 a GitHub Packages
  3. Conectarse a tu VPS via SSH
  4. Descargar las nuevas im谩genes
  5. Reiniciar los servicios con Docker Compose

Puedes monitorear el proceso en la pesta帽a "Actions" de tu repositorio GitHub.

2. Verificar el despliegue

Despu茅s de un despliegue exitoso, puedes verificar que todo funciona:

ssh youruser@yourvps "docker ps"

Deber铆as ver los contenedores de backend, frontend, postgres y apache corriendo.

curl http://yourvps/api/

Deber铆as recibir una respuesta JSON del backend FastAPI.

curl http://yourvps/

Deber铆as ver el HTML de tu aplicaci贸n Next.js.

Soluci贸n de Problemas

Problemas con Docker

  • Verifica que Docker est茅 instalado y corriendo en el VPS
  • Revisa los logs con docker logs <container_name>
  • Prueba recrear los contenedores con docker-compose down && docker-compose up -d

Problemas con GitHub Actions

  • Revisa los logs del workflow fallido
  • Aseg煤rate que todos los secrets est茅n configurados correctamente
  • Verifica que las rutas en los archivos YAML sean correctas

Problemas con Apache

  • Verifica la configuraci贸n de Apache
  • Revisa los logs de error de Apache
  • Aseg煤rate que los m贸dulos proxy est茅n habilitados

Conclusi贸n

Has configurado un pipeline completo de CI/CD que:

  1. Construye im谩genes Docker para tu aplicaci贸n FastAPI y Next.js
  2. Las almacena en GitHub Packages
  3. Las despliega autom谩ticamente en tu VPS
  4. Configura Apache como reverse proxy

Este flujo te permite desarrollar de manera 谩gil, con la confianza de que cada cambio en la rama principal ser谩 desplegado de manera segura y consistente en producci贸n.

隆Felicidades! Ahora tienes un pipeline profesional de CI/CD configurado para tu aplicaci贸n.