FastAPI + PostgreSQL + Next.js con GitHub Actions, Docker y VPS
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.
Nuestro proyecto tendr谩 la siguiente estructura:
Crea un directorio backend
y configura una aplicaci贸n FastAPI b谩sica.
fastapi==0.95.2
uvicorn==0.22.0
sqlalchemy==2.0.15
psycopg2-binary==2.9.6
python-dotenv==1.0.0
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
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)
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()
Crea una aplicaci贸n Next.js b谩sica en el directorio frontend
.
Configura una p谩gina que consuma la API de FastAPI:
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>
)
}
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' })
}
}
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"]
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]
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:
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.
Crea un directorio .github/workflows
y a帽ade el siguiente archivo:
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
Ve a la configuraci贸n de tu repositorio en GitHub y a帽ade los siguientes secrets:
DB_PASSWORD
: Contrase帽a para PostgreSQL en producci贸nSSH_PRIVATE_KEY
: Clave privada SSH para acceder al VPSSSH_USER
: Usuario SSH del VPS (ej. root)SSH_HOST
: Direcci贸n IP o dominio del VPSAdvertencia: Nunca commits credenciales o informaci贸n sensible en tu repositorio. Siempre usa GitHub Secrets para manejar esta informaci贸n.
Con茅ctate a tu VPS via SSH y ejecuta los siguientes comandos:
Descon茅ctate y vuelve a conectarte para que los cambios de grupo surtan efecto.
Crea un archivo de configuraci贸n para Apache:
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.
Aseg煤rate de que los puertos necesarios est茅n abiertos:
Con todo configurado, cada vez que hagas un push a la rama main:
Puedes monitorear el proceso en la pesta帽a "Actions" de tu repositorio GitHub.
Despu茅s de un despliegue exitoso, puedes verificar que todo funciona:
Deber铆as ver los contenedores de backend, frontend, postgres y apache corriendo.
Deber铆as recibir una respuesta JSON del backend FastAPI.
Deber铆as ver el HTML de tu aplicaci贸n Next.js.
docker logs <container_name>
docker-compose down && docker-compose up -d
Has configurado un pipeline completo de CI/CD que:
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.