complete backend plan

This commit is contained in:
2026-03-03 16:09:13 +01:00
parent 8bfce9da00
commit 7f278c6f63
5 changed files with 211 additions and 22 deletions

View File

@@ -1,21 +1,96 @@
name: Deploy to Proxmox Docker name: Test & Deploy API
run-name: Deploying ${{ gitea.sha }} run-name: ${{ gitea.ref_name }} → Docker LXC
on: on:
push: push:
branches: branches: [main]
- main # O il nome del tuo branch principale tags: ['v*']
pull_request:
branches: [main]
jobs: jobs:
Deploy: # ── 1. Run tests in an isolated Python container ──────────────────
runs-on: ubuntu-latest # Questo dipende dalle label che hai dato al tuo act_runner test:
runs-on: ubuntu-latest
container:
image: python:3.12-slim
steps: steps:
- name: Deploying via SSH - name: Checkout Code
uses: appleboy/ssh-action@v1.0.0 uses: actions/checkout@v4
with:
host: ${{ secrets.SSH_HOST }} - name: Install Dependencies
username: ${{ secrets.SSH_USER }} run: pip install --no-cache-dir -r requirements.txt
key: ${{ secrets.SSH_KEY }}
script: | - name: Run Linter
cd /opt/adiuva-api run: ruff check app/ tests/
git pull origin main
docker compose up -d --build - 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

View File

@@ -194,6 +194,11 @@ This starts two services:
- **app** — FastAPI server on port `8000` - **app** — FastAPI server on port `8000`
- **db** — PostgreSQL 16 (Alpine) on port `5432` with a persistent volume and health checks - **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 ### Dockerfile Details
The Dockerfile uses a multi-stage build: 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 ## Environment Variables
All variables are loaded from a `.env` file via Pydantic Settings. Source: `app/config/settings.py` 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 | | `STRIPE_WEBHOOK_SECRET` | `str` | `""` | Stripe webhook signature secret |
| `S3_BUCKET` | `str` | `""` | S3 bucket for encrypted blobs and backups | | `S3_BUCKET` | `str` | `""` | S3 bucket for encrypted blobs and backups |
| `S3_REGION` | `str` | `us-east-1` | AWS region | | `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_ACCESS_KEY_ID` | `str` | `""` | AWS credentials |
| `AWS_SECRET_ACCESS_KEY` | `str` | `""` | AWS credentials | | `AWS_SECRET_ACCESS_KEY` | `str` | `""` | AWS credentials |
| `PINECONE_API_KEY` | `str` | `""` | Pinecone API key (if set, Pinecone is used for vectors) | | `PINECONE_API_KEY` | `str` | `""` | Pinecone API key (if set, Pinecone is used for vectors) |

View File

@@ -14,6 +14,7 @@ class Settings(BaseSettings):
S3_BUCKET: str = "" S3_BUCKET: str = ""
S3_REGION: str = "us-east-1" S3_REGION: str = "us-east-1"
S3_ENDPOINT_URL: str = ""
AWS_ACCESS_KEY_ID: str = "" AWS_ACCESS_KEY_ID: str = ""
AWS_SECRET_ACCESS_KEY: str = "" AWS_SECRET_ACCESS_KEY: str = ""

View File

@@ -23,12 +23,14 @@ class BlobStore:
""" """
def _client(self) -> Any: def _client(self) -> Any:
return boto3.client( kwargs: dict[str, Any] = {
"s3", "region_name": settings.S3_REGION,
region_name=settings.S3_REGION, "aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID, "aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
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 @staticmethod
def _key(user_id: str, table: str, record_id: str) -> str: def _key(user_id: str, table: str, record_id: str) -> str:

View File

@@ -34,5 +34,36 @@ services:
# image: redis:7-alpine # image: redis:7-alpine
# restart: unless-stopped # 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: volumes:
postgres_data: postgres_data:
minio_data:
qdrant_data: