Skip to content
Production Deployment

Production Deployment

TeamWeb AI is designed for self-hosted, single-server deployment. Each instance runs on its own VPS (e.g. a Digital Ocean droplet) with automatic HTTPS, database backups, and security hardening.

Requirements

  • A VPS running Ubuntu 22.04 or 24.04 (Digital Ocean, Hetzner, AWS EC2, etc.)
  • A domain name pointed at the server’s IP address
  • At least 4 GB RAM and 2 vCPUs (8 GB / 4 vCPUs recommended)
  • An Anthropic API key (or other LLM provider key)

Quick Deploy

All deployment tooling is in the deploy/ directory. Configure your server address once, then use Make targets for everything.

Configure your server address

cd deploy
echo "SERVER=teamwebai@YOUR_SERVER_IP" > .server

This file is gitignored. All make commands below read from it automatically.

Provision the server

make setup

This installs Docker, configures the firewall (UFW), hardens SSH, installs fail2ban, and sets up automatic security updates. After this, SSH as root is disabled — use the teamwebai user going forward.

Configure environment variables

Copy the template to the server and edit it:

scp .env.prod.example teamwebai@YOUR_SERVER_IP:/opt/teamwebai/.env.prod
ssh -t teamwebai@YOUR_SERVER_IP nano /opt/teamwebai/.env.prod

At minimum, set:

  • DOMAIN — your domain name (e.g. app.example.com)
  • PUBLIC_URLhttps://app.example.com
  • SECRET_KEY — generate with python3 -c "import secrets; print(secrets.token_urlsafe(64))"
  • POSTGRES_PASSWORD — generate with python3 -c "import secrets; print(secrets.token_urlsafe(32))"
  • DATABASE_URL — use the password from above
  • SECRETS_ENCRYPTION_KEY — generate with python3 -c "import base64, os; print(base64.b64encode(os.urandom(32)).decode())" (required in every environment — the application refuses to start without it; encrypts LLM provider API keys, OAuth tokens, MCP credentials, channel plugin tokens, and system webhook signing secrets)
  • ANTHROPIC_API_KEY — optional bootstrap key. Only used as a fallback when no LLM provider is configured in the database; most deployments set providers in the Settings UI instead.

Deploy

make deploy

This syncs the source code, builds Docker images, starts services, runs database migrations, and downloads the embedding and reranker models (~1 GB total) into the models_data volume on first deploy. Subsequent deploys re-run the download command, but Hugging Face skips files already cached.

Verify

Open https://your-domain.com in your browser. Complete the setup wizard to create your admin account.

Make Targets

Once .server is configured, all common operations are a single command from the deploy/ directory:

CommandDescription
make deployDeploy or update the application
make statusShow running services
make logsTail application logs
make shellOpen a shell in the web container
make db-shellOpen a psql session
make backupRun a database backup now
make backupsList available backups
make restoreRestore from a backup (interactive)

You can also pass SERVER= on any command to override the .server file:

make logs SERVER=teamwebai@203.0.113.10

Updating

git pull
make deploy

The script rebuilds images, restarts services, and runs any new database migrations automatically.

Backups

Database backups run automatically at 2:00 AM daily (configured by the setup script). Backups are stored at /opt/teamwebai/backups/ with 30-day rotation.

make backup     # run a backup now
make backups    # list available backups
make restore    # interactive restore (prompts for filename)
Restoring a backup will drop the current database and replace it entirely. The restore script will prompt for confirmation.

Rollback

If an update causes problems:

  1. Check out the previous version locally: git checkout v1.2.3
  2. Re-deploy: make deploy
  3. If a database migration needs reversing: make shell then run uv run flask db downgrade

Architecture

The production stack runs six Docker containers behind a Caddy reverse proxy that handles automatic HTTPS via Let’s Encrypt:

ServiceRole
CaddyReverse proxy, automatic HTTPS
WebFlask/Gunicorn application server
WorkerCelery worker for background tasks
BeatCelery Beat for scheduled tasks
DBPostgreSQL 16 with pgvector
RedisMessage broker for Celery

Two Docker networks isolate the database from untrusted containers:

  • backend — DB and Redis are only accessible to application services (web, worker, beat)
  • teamwebai — Caddy, application services, and MCP server containers (no database access)

Sandbox containers for code execution run on Docker’s default bridge network with no access to application services. See Sandboxing and Database Protection for details.

The application images also include Chromium for the built-in browser_action tool. Browser sessions use managed profile storage inside the application runtime rather than the sandbox container system. If you use browser automation heavily, account for the extra RAM and disk usage from browser profiles and live Chromium processes.

Server Sizing

SizeRAMvCPUsMonthly CostNotes
Minimum4 GB2~$24Embedder ~400 MB + reranker ~600 MB resident
Recommended8 GB4~$48Comfortable for concurrent sandbox, MCP, and managed browser sessions

Storage: 80 GB+ SSD recommended (Docker images, database, media, and model cache accumulate).

Security

The setup script configures:

  • UFW firewall — only ports 22, 80, and 443 open
  • SSH hardening — key-only authentication, root login disabled
  • fail2ban — brute-force protection for SSH
  • Unattended upgrades — automatic OS security patches

The production Docker Compose adds:

  • No exposed ports except Caddy (80/443) — DB and Redis are internal only
  • Dual-network isolation — MCP containers cannot reach the database
  • Security headers — HSTS, X-Content-Type-Options, X-Frame-Options
  • Memory limits on all containers
  • Automatic HTTPS with OCSP stapling via Caddy/Let’s Encrypt

Reverse Proxy Trust

TeamWeb AI uses Werkzeug’s ProxyFix middleware configured to trust one level of proxy headers (X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host). This means request.remote_addr returns the real client IP rather than the proxy’s IP, which is critical for rate limiting and logging.

This is correct for the default architecture where Caddy is the single reverse proxy in front of the application. If you place an additional load balancer or CDN in front of Caddy, you will need to adjust the x_for parameter in app/__init__.py to match the number of trusted proxies in the chain — otherwise rate limiting will use the wrong IP address.