complete backend plan
This commit is contained in:
@@ -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
|
||||||
80
README.md
80
README.md
@@ -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) |
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user