complete backend plan
This commit is contained in:
@@ -1,21 +1,96 @@
|
||||
name: Deploy to Proxmox Docker
|
||||
run-name: Deploying ${{ gitea.sha }}
|
||||
name: Test & Deploy API
|
||||
run-name: ${{ gitea.ref_name }} → Docker LXC
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # O il nome del tuo branch principale
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
Deploy:
|
||||
runs-on: ubuntu-latest # Questo dipende dalle label che hai dato al tuo act_runner
|
||||
# ── 1. Run tests in an isolated Python container ──────────────────
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: python:3.12-slim
|
||||
|
||||
steps:
|
||||
- name: Deploying via SSH
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
script: |
|
||||
cd /opt/adiuva-api
|
||||
git pull origin main
|
||||
docker compose up -d --build
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
- name: Run Linter
|
||||
run: ruff check app/ tests/
|
||||
|
||||
- name: Run Tests
|
||||
run: pytest tests/ -v --tb=short
|
||||
|
||||
# ── 2. Deploy to Docker LXC (only main branch & tags) ─────────────
|
||||
deploy:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Sync to deploy directory
|
||||
run: |
|
||||
DEPLOY_DIR="/opt/adiuva-api"
|
||||
mkdir -p "$DEPLOY_DIR"
|
||||
|
||||
# Sync source, preserve .env and volumes
|
||||
cp -rf app/ alembic/ alembic.ini Dockerfile docker-compose.yml requirements.txt "$DEPLOY_DIR/"
|
||||
|
||||
- name: Build & restart services
|
||||
run: |
|
||||
cd /opt/adiuva-api
|
||||
docker compose up -d --build --remove-orphans
|
||||
|
||||
- name: Run database migrations
|
||||
run: |
|
||||
cd /opt/adiuva-api
|
||||
docker compose exec -T app alembic upgrade head
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
echo "Waiting for app to be ready..."
|
||||
sleep 5
|
||||
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/api/v1/health)
|
||||
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||
echo "✅ API is healthy (HTTP ${HTTP_CODE})"
|
||||
else
|
||||
echo "❌ Health check failed (HTTP ${HTTP_CODE})"
|
||||
docker compose -f /opt/adiuva-api/docker-compose.yml logs app --tail=50
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create Gitea Release (tags only)
|
||||
if: startsWith(gitea.ref, 'refs/tags/')
|
||||
run: |
|
||||
GITEA_URL="http://10.0.0.119:3000"
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
REPO="${GITHUB_REPOSITORY}"
|
||||
TOKEN="${{ gitea.token }}"
|
||||
|
||||
RELEASE_ID=$(curl -sf \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}" \
|
||||
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
curl -sf \
|
||||
-X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Adiuva API ${TAG}\",\"body\":\"Deployed to Docker LXC\"}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases"
|
||||
echo "✅ Release ${TAG} created"
|
||||
else
|
||||
echo "ℹ️ Release ${TAG} already exists (ID: ${RELEASE_ID})"
|
||||
fi
|
||||
80
README.md
80
README.md
@@ -194,6 +194,11 @@ This starts two services:
|
||||
- **app** — FastAPI server on port `8000`
|
||||
- **db** — PostgreSQL 16 (Alpine) on port `5432` with a persistent volume and health checks
|
||||
|
||||
The compose file also includes optional services for fully local deployments:
|
||||
|
||||
- **minio** — S3-compatible object storage on ports `9000` (API) and `9001` (console)
|
||||
- **qdrant** — Vector search engine on ports `6333` (HTTP) and `6334` (gRPC)
|
||||
|
||||
### Dockerfile Details
|
||||
|
||||
The Dockerfile uses a multi-stage build:
|
||||
@@ -209,6 +214,80 @@ gunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 --timeout 120 -b 0.0
|
||||
|
||||
---
|
||||
|
||||
## Homelab / Self-Hosted Deployment
|
||||
|
||||
You can run the entire stack locally on a homelab with **no cloud dependencies except the LLM provider**. The compose file includes MinIO (S3 replacement) and Qdrant (vector store) out of the box.
|
||||
|
||||
### 1. Start all services
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
This starts PostgreSQL, MinIO, and Qdrant alongside the app.
|
||||
|
||||
### 2. Create the MinIO bucket
|
||||
|
||||
Open the MinIO console at [http://localhost:9001](http://localhost:9001) (login: `minioadmin` / `minioadmin`) and create a bucket named `adiuva`, or use the CLI:
|
||||
|
||||
```bash
|
||||
docker compose exec minio mc alias set local http://localhost:9000 minioadmin minioadmin
|
||||
docker compose exec minio mc mb local/adiuva
|
||||
```
|
||||
|
||||
### 3. Configure your `.env`
|
||||
|
||||
```bash
|
||||
# Database (uses the compose PostgreSQL)
|
||||
DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/adiuva
|
||||
|
||||
# S3 → MinIO
|
||||
S3_BUCKET=adiuva
|
||||
S3_REGION=us-east-1
|
||||
S3_ENDPOINT_URL=http://minio:9000
|
||||
AWS_ACCESS_KEY_ID=minioadmin
|
||||
AWS_SECRET_ACCESS_KEY=minioadmin
|
||||
|
||||
# Vector store → local Qdrant (leave PINECONE_API_KEY empty)
|
||||
QDRANT_URL=http://qdrant:6333
|
||||
QDRANT_API_KEY=
|
||||
PINECONE_API_KEY=
|
||||
|
||||
# Billing — leave empty to stub (no Stripe needed)
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# LLM — the only external service
|
||||
OPENAI_API_KEY=sk-...
|
||||
LLM_MODEL=gpt-4o
|
||||
LLM_ROUTER_MODEL=gpt-4o-mini
|
||||
|
||||
# Auth
|
||||
JWT_SECRET=your-secret-here
|
||||
ENV=dev
|
||||
```
|
||||
|
||||
### 4. Run migrations
|
||||
|
||||
```bash
|
||||
docker compose exec app alembic upgrade head
|
||||
```
|
||||
|
||||
### What runs where
|
||||
|
||||
| Service | Runs on | Port | Notes |
|
||||
|---|---|---|---|
|
||||
| FastAPI app | Docker | 8000 | API server |
|
||||
| PostgreSQL | Docker | 5432 | Auth, billing, metadata |
|
||||
| MinIO | Docker | 9000 / 9001 | S3-compatible blob & backup storage |
|
||||
| Qdrant | Docker | 6333 / 6334 | Vector search (replaces Pinecone) |
|
||||
| Stripe | — | — | Stubbed when keys are empty |
|
||||
| OpenAI / LLM | Cloud | — | Only external dependency |
|
||||
|
||||
> **Want fully offline AI too?** Set `LLM_MODEL=ollama/llama3` and `LLM_ROUTER_MODEL=ollama/llama3`, then add an Ollama container or point at a local Ollama instance. See the [LLM provider switching](#switching-llm-providers) section.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All variables are loaded from a `.env` file via Pydantic Settings. Source: `app/config/settings.py`
|
||||
@@ -224,6 +303,7 @@ All variables are loaded from a `.env` file via Pydantic Settings. Source: `app/
|
||||
| `STRIPE_WEBHOOK_SECRET` | `str` | `""` | Stripe webhook signature secret |
|
||||
| `S3_BUCKET` | `str` | `""` | S3 bucket for encrypted blobs and backups |
|
||||
| `S3_REGION` | `str` | `us-east-1` | AWS region |
|
||||
| `S3_ENDPOINT_URL` | `str` | `""` | Custom S3 endpoint (e.g. `http://minio:9000` for MinIO). Leave empty for AWS. |
|
||||
| `AWS_ACCESS_KEY_ID` | `str` | `""` | AWS credentials |
|
||||
| `AWS_SECRET_ACCESS_KEY` | `str` | `""` | AWS credentials |
|
||||
| `PINECONE_API_KEY` | `str` | `""` | Pinecone API key (if set, Pinecone is used for vectors) |
|
||||
|
||||
@@ -14,6 +14,7 @@ class Settings(BaseSettings):
|
||||
|
||||
S3_BUCKET: str = ""
|
||||
S3_REGION: str = "us-east-1"
|
||||
S3_ENDPOINT_URL: str = ""
|
||||
AWS_ACCESS_KEY_ID: str = ""
|
||||
AWS_SECRET_ACCESS_KEY: str = ""
|
||||
|
||||
|
||||
@@ -23,12 +23,14 @@ class BlobStore:
|
||||
"""
|
||||
|
||||
def _client(self) -> Any:
|
||||
return boto3.client(
|
||||
"s3",
|
||||
region_name=settings.S3_REGION,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
kwargs: dict[str, Any] = {
|
||||
"region_name": settings.S3_REGION,
|
||||
"aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
|
||||
"aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
|
||||
}
|
||||
if settings.S3_ENDPOINT_URL and isinstance(settings.S3_ENDPOINT_URL, str):
|
||||
kwargs["endpoint_url"] = settings.S3_ENDPOINT_URL
|
||||
return boto3.client("s3", **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _key(user_id: str, table: str, record_id: str) -> str:
|
||||
|
||||
@@ -34,5 +34,36 @@ services:
|
||||
# image: redis:7-alpine
|
||||
# restart: unless-stopped
|
||||
|
||||
# ── Local S3-compatible storage (MinIO) ──
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
command: server /data --console-address ":9001"
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
# ── Local vector store (Qdrant) ──
|
||||
qdrant:
|
||||
image: qdrant/qdrant:latest
|
||||
ports:
|
||||
- "6333:6333"
|
||||
- "6334:6334"
|
||||
volumes:
|
||||
- qdrant_data:/qdrant/storage
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
qdrant_data:
|
||||
|
||||
Reference in New Issue
Block a user