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" > .serverThis file is gitignored. All make commands below read from it automatically.
Provision the server
make setupThis 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.prodAt minimum, set:
DOMAIN— your domain name (e.g.app.example.com)PUBLIC_URL—https://app.example.comSECRET_KEY— generate withpython3 -c "import secrets; print(secrets.token_urlsafe(64))"POSTGRES_PASSWORD— generate withpython3 -c "import secrets; print(secrets.token_urlsafe(32))"DATABASE_URL— use the password from aboveSECRETS_ENCRYPTION_KEY— generate withpython3 -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 deployThis 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:
| Command | Description |
|---|---|
make deploy | Deploy or update the application |
make status | Show running services |
make logs | Tail application logs |
make shell | Open a shell in the web container |
make db-shell | Open a psql session |
make backup | Run a database backup now |
make backups | List available backups |
make restore | Restore from a backup (interactive) |
You can also pass SERVER= on any command to override the .server file:
make logs SERVER=teamwebai@203.0.113.10Updating
git pull
make deployThe 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)Rollback
If an update causes problems:
- Check out the previous version locally:
git checkout v1.2.3 - Re-deploy:
make deploy - If a database migration needs reversing:
make shellthen runuv 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:
| Service | Role |
|---|---|
| Caddy | Reverse proxy, automatic HTTPS |
| Web | Flask/Gunicorn application server |
| Worker | Celery worker for background tasks |
| Beat | Celery Beat for scheduled tasks |
| DB | PostgreSQL 16 with pgvector |
| Redis | Message 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
| Size | RAM | vCPUs | Monthly Cost | Notes |
|---|---|---|---|---|
| Minimum | 4 GB | 2 | ~$24 | Embedder ~400 MB + reranker ~600 MB resident |
| Recommended | 8 GB | 4 | ~$48 | Comfortable 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.