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 to127.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
| Spec | Minimum | Recommended |
|---|---|---|
| RAM | 2 GB | 4 GB |
| vCPUs | 1 | 2 |
| Storage | 25 GB SSD | 80 GB SSD |
| OS | Ubuntu 24.04 LTS | Ubuntu 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
| Command | Purpose |
|---|---|
./scripts/deploy setup | First-time server setup |
./scripts/deploy | Pull, rebuild, restart (standard deploy) |
./scripts/deploy restart | Restart services only (no rebuild) |
./scripts/deploy status | Health check all services |
./scripts/deploy nginx | Reinstall 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:
| Subdomain | Serves | API Proxy |
|---|---|---|
tevis.io | apps/website/dist/ | None |
app.tevis.io | apps/mission-control/dist/ | /api/* → 8081 |
gc.tevis.io | apps/ground-control/dist/ | /api/* → 8081 |
api.tevis.io | — | /* → 8081 |
docs.tevis.io | apps/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:
- Creates the
data/directory if missing - Creates all tables from SQLAlchemy models
- 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"
Off-site backups (recommended)
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/