/ Deployment guide

Self-hosted in under 15 minutes

Community Edition

Savvina AI Community Edition ships as a Docker Compose stack. One command starts all services. This guide covers a production deployment on a Linux server with HTTPS.

Prerequisites

  • Linux server (Ubuntu 22.04+ or RHEL 9+ recommended)
  • Docker Engine 24+ with the Compose plugin (docker compose, not docker-compose)
  • At least 4 GB RAM — 8 GB+ recommended under concurrent load
  • At least 30 GB disk — the backend image is ~11 GB (sentence-transformer model baked in); budget additional space for the PostgreSQL data volume, build cache, and OS
  • A domain name with DNS pointing to the server (for HTTPS)
  • At least one LLM API key (Groq and Google Gemini have free tiers)
01

Prepare the environment

Clone the repository, copy the example env file, and generate required secrets.

git clone https://github.com/savvina-ai/savvina
cd savvina
cp .env.example .env
chmod 600 .env

Generate the three required secrets and add them to .env:

ENCRYPTION_KEY

Fernet symmetric key for encrypting stored credentials. Generate once and never change it in production.

python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
JWT_SECRET_KEY

Signs and verifies access tokens. Must be at least 32 characters.

python -c "import secrets; print(secrets.token_hex(32))"
APP_DB_PASSWORD

Password for the internal PostgreSQL app database (local-db profile only).

python -c "import secrets; print(secrets.token_urlsafe(24))"

ENCRYPTION_KEY: Generate once and never change it in production. Rotating the key requires re-encrypting every stored credential. Use a unique key per environment — never reuse a development key in production.

02

Choose a database mode

Option A — Local Docker PostgreSQL (default)

Set COMPOSE_PROFILES=local-db in .env. The DATABASE_URL must use the Docker service name as host:

COMPOSE_PROFILES=local-db
APP_DB_PASSWORD=<strong-password>
DATABASE_URL=postgresql+asyncpg://savvina:<strong-password>@db:5432/savvina_app

Option B — External / Managed PostgreSQL

For AWS RDS, GCP Cloud SQL, Supabase, Neon, Azure, etc.: leave COMPOSE_PROFILES unset (the bundled db container will not start) and point DATABASE_URL at your provider:

DATABASE_URL=postgresql+asyncpg://USER:PASSWORD@your-db-host.example.com:5432/savvina_app
# Append ?ssl=require if your provider enforces SSL (most do)

The database user needs CREATE TABLE, ALTER TABLE, and CREATE INDEX privileges. Migrations run automatically on startup.

03

Build and start

docker compose up --build -d

This starts the core services: backend (FastAPI on port 8000), frontend (React/Nginx on port 3000), and db (PostgreSQL, local-db profile only).

Verify all services are healthy:

docker compose ps

Confirm the backend started correctly (look for Embedding model loaded and Uvicorn running):

docker compose logs backend --tail 30
04

Create the admin account

On a fresh deployment, the first person to open the browser is taken to the Create Admin Account screen. Enter an email and a password (minimum 12 characters). This screen appears only once — the registration endpoint closes permanently as soon as any account exists.

05

Configure HTTPS

TLS is terminated by the Nginx server inside the frontend container. Obtain a certificate from Let's Encrypt (recommended) or your CA, then install it:

# Obtain (Let's Encrypt)
apt install certbot -y
certbot certonly --standalone -d yourdomain.com

# Install
cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem volumes/certs/
cp /etc/letsencrypt/live/yourdomain.com/privkey.pem   volumes/certs/

# Rebuild frontend once to pick up nginx.conf changes
docker compose build frontend && docker compose up -d frontend

Add your domain to CORS_ORIGINS in .env, then restart the backend:

CORS_ORIGINS=["https://yourdomain.com"]
docker compose restart backend

For subsequent cert renewals, no rebuild is needed — copy the new files and reload Nginx in place:

cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem volumes/certs/
cp /etc/letsencrypt/live/yourdomain.com/privkey.pem   volumes/certs/
docker compose exec frontend nginx -s reload
06

Lock down exposed ports

By default, docker-compose.yaml publishes port 3000 (frontend) and port 8000 (backend API). Do not expose port 8000 to the internet. Bind both services to localhost only via an override file:

# docker-compose.override.yaml
services:
  backend:
    ports:
      - "127.0.0.1:8000:8000"
  frontend:
    ports:
      - "127.0.0.1:3000:80"
07

Backups

All application state — user accounts, saved connections (with encrypted credentials), chat history, query cache — lives in volumes/app-db/ (local-db profile). Back it up regularly with pg_dump:

docker compose exec db pg_dump -U savvina savvina_app > backup.sql

If using an external/managed PostgreSQL, backups are your provider's responsibility — enable automated snapshots and PITR in your provider's console.

Also back up your ENCRYPTION_KEY separately. Without it, stored connection credentials cannot be decrypted.

08

Sample databases (optional)

The test-dbs profile starts two pre-seeded databases so you can evaluate Savvina AI without connecting to a real data source:

Service Engine Port Database
sample-postgres PostgreSQL 16 5435 savvina_test
sample-mysql MySQL 8.0 3307 sample_delivery
# With local Docker DB:
docker compose --profile local-db --profile test-dbs up -d

# With external/managed PostgreSQL:
docker compose --profile test-dbs up -d

Set the required passwords in .env before starting:

SAMPLE_POSTGRES_PASSWORD=<strong-password>
SAMPLE_MYSQL_ROOT_PASSWORD=<strong-password>
SAMPLE_MYSQL_PASSWORD=<strong-password>
09

Local LLM with Ollama (optional)

Run a local Ollama instance if you want fully offline inference. A GPU is required for acceptable performance.

docker compose --profile local-llm up -d
docker compose exec ollama ollama pull llama3

The container is reachable from the backend at http://ollama:11434 inside the Docker network. Configure it via Settings → Providers → Add Provider → Ollama.

10

Updating

Pull the latest changes and rebuild. Database schema migrations apply automatically on startup via alembic upgrade head — no manual migration step required. The volumes/ directory is never touched during updates.

git pull
docker compose build backend frontend
docker compose up -d

Production checklist

Variable Production value
ENCRYPTION_KEY Unique Fernet key — never reuse across environments
JWT_SECRET_KEY At least 32 random characters
LOG_LEVEL WARNING or INFO (not DEBUG)
DEBUG false
DEFAULT_ROW_LIMIT 500 or 1000
DEFAULT_QUERY_TIMEOUT 30 (seconds)
VERIFY_SSL true (set false only behind a TLS-inspecting proxy)

Health check

The backend exposes a health endpoint used by Docker Compose and your load balancer:

curl http://localhost:8000/health
# {"status": "ok", "app": "Savvina AI"}

Corporate TLS proxies

If your network uses a TLS-intercepting proxy (Zscaler, Netskope, Palo Alto), LLM API calls may fail with certificate errors. Set in .env:

VERIFY_SSL=false

This affects only LLM provider HTTP clients — not the asyncpg database TLS stack.

Troubleshooting

Backend won't start

docker compose logs backend
  • ENCRYPTION_KEY missing or malformed (must be a valid Fernet key)
  • Port 8000 already in use on the host

Embedding model download fails

  • The model (BAAI/bge-small-en-v1.5) is fetched from HuggingFace Hub on first startup. If the server has no internet access, pre-bake it into the Docker image — see the README for the Dockerfile snippet.

LLM calls fail with SSL errors

  • Set VERIFY_SSL=false in .env (see Corporate TLS section above).

Database connection errors (local-db)

docker compose ps db
docker compose logs db --tail 20
  • APP_DB_PASSWORD not set in .env
  • COMPOSE_PROFILES=local-db missing from .env — the container won't start without it
  • DATABASE_URL still points to localhost — inside Docker the host must be db, not localhost
  • Port 5434 already in use on the host
  • Volume permission issue on Linux — set LOCAL_UID/LOCAL_GID in .env to match your host user

Database connection errors (external PostgreSQL)

docker compose logs backend --tail 30
  • Wrong hostname, port, user, or password in DATABASE_URL
  • Missing ?ssl=require — most managed providers enforce SSL
  • Firewall or security group not allowing the Docker host's outbound IP

Deployment questions or issues? Email info@savvina.ai. For the full configuration reference see the configuration docs on GitHub.