Skip to main content

Production Deployment

This guide covers deploying Tevis to a DigitalOcean Droplet running Ubuntu 24.04 LTS. The same steps apply to any Ubuntu/Debian server.

Architecture Overview

In production, Tevis Core API runs behind nginx on a cloud server. The frontend apps are pre-built and served as static files — no Vite dev servers. Tevis Local runs on each user's machine (not on the production server).

Mission Control communicates with two backends:

  • Tevis Core (cloud) — Auth, orchestration, planning. Accessed via relative /api/* paths, proxied by nginx to 127.0.0.1:8081.
  • Tevis Local (user's machine) — KB, instances, sessions, sandbox. Accessed directly from the browser at http://localhost:4082. No cloud proxy or tunnel needed — the browser talks to the user's own machine. The standalone daemon uses port 4082 (distinct from the dev port 8082) to avoid collisions with the development environment.
Browser (user's machine)
├── https://app.tevis.io/api/* → nginx → Tevis Core (cloud)
└── http://localhost:4082/* → Tevis Local daemon (user's machine, direct)

This works because browsers allow http://localhost requests from HTTPS pages (per the W3C Secure Contexts spec — localhost is always considered secure).

Cloud Server (DigitalOcean)

nginx (port 80/443)
├─ tevis.io → static files (Website)
├─ app.tevis.io → static files (Mission Control) + API proxy
├─ gc.tevis.io → static files (Ground Control) + API proxy
├─ api.tevis.io → reverse proxy (direct API access)
└─ docs.tevis.io → static files (Documentation)


┌──────────────────┐
│ Tevis Core API │ ← 127.0.0.1:8081 (auth, orchestration, intelligence)
└──────────────────┘


SQLite database
(data/tevis-core.db)

Droplet Specification

SpecMinimumRecommended
RAM2 GB4 GB
vCPUs12
Storage25 GB SSD80 GB SSD
OSUbuntu 24.04 LTSUbuntu 24.04 LTS

If running Ollama on the same box, use 8 GB+ RAM or a dedicated GPU Droplet.

SSH Setup

Generate a deploy key

ssh-keygen -t ed25519 -C "tevis-droplet" -f ~/.ssh/tevis_do

Add the public key to DigitalOcean during Droplet creation (Settings → Security → SSH Keys).

SSH config

Add to ~/.ssh/config on your local machine:

Host tevis
HostName <DROPLET_IP>
User tevis
IdentityFile ~/.ssh/tevis_do
ForwardAgent no

Create a non-root user

After first login as root:

adduser tevis
usermod -aG sudo tevis
usermod -aG docker tevis

mkdir -p /home/tevis/.ssh
cp ~/.ssh/authorized_keys /home/tevis/.ssh/
chown -R tevis:tevis /home/tevis/.ssh
chmod 700 /home/tevis/.ssh
chmod 600 /home/tevis/.ssh/authorized_keys

# Disable root SSH login
sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart sshd

Server Provisioning

SSH in as the tevis user and install dependencies:

sudo apt update && sudo apt upgrade -y

# Python 3.11+
sudo apt install -y python3 python3-venv python3-pip

# Node.js 22 LTS
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

# pnpm
corepack enable
corepack prepare pnpm@10.28.2 --activate

# Docker
sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable docker

# Nginx + Certbot
sudo apt install -y nginx certbot python3-certbot-nginx

# Build tools
sudo apt install -y git build-essential curl lsof sqlite3

Clone and Deploy

Set up a deploy key on the server

ssh-keygen -t ed25519 -C "tevis-deploy-key" -f ~/.ssh/deploy_key

Add the public key (~/.ssh/deploy_key.pub) as a Deploy Key in your GitHub repo (Settings → Deploy Keys).

First-time setup

GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy_key" \
git clone git@github.com:kadifoo/tevis-v2.git /home/tevis/tevis-v2

cd /home/tevis/tevis-v2
./scripts/deploy setup

The setup command handles everything:

  • Creates Python venv and installs dependencies
  • Installs Node dependencies and builds all frontends
  • Fixes file permissions for nginx
  • Installs systemd service unit for Core API
  • Installs nginx configs for all five subdomains
  • Sets up a database backup cron (every 6 hours, 7-day retention)
  • Starts all services and runs health checks

SSL certificates

After DNS records point to your server:

sudo certbot --nginx \
-d tevis.io -d www.tevis.io \
-d app.tevis.io -d gc.tevis.io \
-d api.tevis.io -d docs.tevis.io

Certbot auto-renews via systemd timer.

Ongoing Deploys

After the initial setup, deploying new code is a single command:

./scripts/deploy

This pulls the latest code from main, rebuilds Python and Node dependencies, rebuilds all frontends, restarts Tevis Core API, and runs health checks.

Other deploy commands

CommandPurpose
./scripts/deploy setupFirst-time server setup
./scripts/deployPull, rebuild, restart (standard deploy)
./scripts/deploy restartRestart services only (no rebuild)
./scripts/deploy statusHealth check all services
./scripts/deploy nginxReinstall nginx configs only

Service Management

Tevis Core API runs as a systemd service:

# View status
sudo systemctl status tevis-core-api

# View logs
sudo journalctl -u tevis-core-api -f

# Restart
sudo systemctl restart tevis-core-api

Application-level logs are also at:

.tevis-runtime/logs/tevis-core-api.log

Subdomain Routing

Each subdomain maps to a specific service:

SubdomainServesAPI Proxy
tevis.ioapps/website/dist/None
app.tevis.ioapps/mission-control/dist//api/* → 8081
gc.tevis.ioapps/ground-control/dist//api/* → 8081
api.tevis.io/* → 8081
docs.tevis.ioapps/docs/build/None

Frontend apps use relative paths (/api/...) for Tevis Core API calls. Nginx proxies these to the backend service, matching the same pattern as the Vite dev server proxy.

For Tevis Local API calls, the production build uses http://localhost:4082 directly (configured via VITE_LOCAL_API_URL in .env.production). In development, these calls go through the Vite proxy at /api/local/* to port 8082. The standalone Tevis Local daemon uses port 4082 to avoid conflicts with the development environment (8082).

Tevis Local's CORS configuration allows requests from https://app.tevis.io and https://gc.tevis.io to enable this direct browser-to-localhost communication.

Database

Auto-initialization

The database creates itself on first startup. When Tevis Core API starts, init_db() automatically:

  1. Creates the data/ directory if missing
  2. Creates all tables from SQLAlchemy models
  3. Applies additive startup migrations (new columns)

No manual migration or seed step is needed.

Backups

The deploy script installs a cron job that backs up the database every 6 hours using SQLite's safe backup API:

/home/tevis/backups/tevis-core-YYYYMMDD-HHMM.db

Backups older than 7 days are automatically pruned.

To manually back up:

sqlite3 /home/tevis/tevis-v2/data/tevis-core.db \
".backup /home/tevis/backups/tevis-core-manual.db"

Push backups to a DigitalOcean Space for disaster recovery:

sudo apt install -y s3cmd
# Configure with your DO Spaces credentials, then:
s3cmd sync /home/tevis/backups/ s3://tevis-backups/

Firewall

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Only ports 22, 80, and 443 are exposed. The Core API port (8081) is bound to 127.0.0.1 and not accessible from the internet.

Troubleshooting

500 errors on frontend subdomains

Usually a file permissions issue. Nginx runs as www-data and needs read access:

chmod 755 /home/tevis
chmod 755 /home/tevis/tevis-v2
chmod -R o+rX /home/tevis/tevis-v2/apps/

API returns 502 Bad Gateway

The backend service isn't running:

sudo systemctl status tevis-core-api
sudo journalctl -u tevis-core-api --no-pager -n 50

Missing Python dependencies

If uvicorn fails with ModuleNotFoundError:

source .venv/bin/activate
pip install -e .
pip install email-validator # Required by pydantic EmailStr

Frontend shows stale content

Rebuild and fix permissions:

pnpm build
chmod -R o+rX /home/tevis/tevis-v2/apps/