See also: Docker Guide for the complete Docker reference (profiles, building, configuration, troubleshooting).
Deploy Z.E.N. to a cloud VPS for 24/7 operation with automatic HTTPS and persistent uptime.
Navigation: Prerequisites | Server Setup | DNS Configuration | Repository Setup | Environment Config | Database Migration | Caddy Setup | Start Services | Verify
Prerequisites
Required:
- Cloud VPS account (DigitalOcean, Linode, AWS EC2, Vultr, etc.)
- Domain name or subdomain (e.g.,
zen.yourdomain.com) - SSH client installed on your local machine
- Basic command-line familiarity
Recommended Specs:
- CPU: 1-2 vCPUs
- RAM: 2GB minimum (4GB recommended)
- Storage: 20GB SSD
- OS: Ubuntu 22.04 LTS
Generate SSH Key (Required)
Before creating your VPS, generate an SSH key pair on your local machine:
# Generate SSH key (ed25519 recommended)
ssh-keygen -t ed25519 -C "zen"
# When prompted:
# - File location: Press Enter (uses default ~/.ssh/id_ed25519)
# - Passphrase: Optional but recommended
# View your public key (you'll need this for VPS setup)
cat ~/.ssh/id_ed25519.pub
# Windows: type %USERPROFILE%\.ssh\id_ed25519.pubCopy the public key output - you'll add this to your VPS during creation.
1. Server Provisioning & Initial Setup
Create VPS Instance (Examples)
DigitalOcean Droplet
- Log in to DigitalOcean
- Click "Create" -> "Droplets"
- Choose:
- Image: Ubuntu 22.04 LTS
- Plan: Basic ($12/month - 2GB RAM recommended)
- Datacenter: Choose closest to your users
- Authentication: SSH keys -> "New SSH Key" -> Paste your public key from Prerequisites
- Click "Create Droplet"
- Note the public IP address
AWS EC2 Instance
- Log in to AWS Console
- Navigate to EC2 -> Launch Instance
- Choose:
- AMI: Ubuntu Server 22.04 LTS
- Instance Type: t3.small (2GB RAM)
- Key Pair: "Create new key pair" or import your public key from Prerequisites
- Security Group: Allow SSH (22), HTTP (80), HTTPS (443)
- Launch instance
- Note the public IP address
Linode Instance
- Log in to Linode
- Click "Create" -> "Linode"
- Choose:
- Image: Ubuntu 22.04 LTS
- Region: Choose closest to your users
- Plan: Nanode 2GB ($12/month)
- SSH Keys: Add your public key from Prerequisites
- Root Password: Set strong password (backup access)
- Click "Create Linode"
- Note the public IP address
Initial Server Configuration
Connect to your server:
# Replace with your server IP (uses SSH key from Prerequisites)
ssh -i ~/.ssh/id_ed25519 root@your-server-ipCreate deployment user:
# Create user with sudo privileges
adduser deploy
usermod -aG sudo deploy
# Copy root's SSH authorized keys to deploy user
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
# Test connection in a new terminal before proceeding:
# ssh -i ~/.ssh/id_ed25519 deploy@your-server-ipDisable password authentication for security:
# Edit SSH config
nano /etc/ssh/sshd_configFind and change:
PasswordAuthentication noTo get out of Nano after making changes, press: Ctrl + X -> Y -> enter
Restart SSH:
systemctl restart ssh
# Switch to deploy user for remaining steps
su - deployConfigure firewall:
# Allow SSH, HTTP, HTTPS (including HTTP/3)
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443
# Enable firewall
sudo ufw --force enable
# Check status
sudo ufw statusInstall Dependencies
Install Docker:
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Add deploy user to docker group
sudo usermod -aG docker deploy
# Log out and back in for group changes to take effect
exit
ssh -i ~/.ssh/id_ed25519 deploy@your-server-ipInstall Docker Compose, Git, and PostgreSQL Client:
# Update package list
sudo apt update
# Install required packages
sudo apt install -y docker-compose-plugin git postgresql-client
# Verify installations
docker --version
docker compose version
git --version
psql --version2. DNS Configuration
Point your domain to your server's IP address.
A Record Setup:
- Go to your domain registrar or DNS provider (Cloudflare, Namecheap, etc.)
- Create an A Record:
- Name:
zen(forzen.yourdomain.com) or@(foryourdomain.com) - Value: Your server's public IP address
- TTL: 300 (5 minutes) or default
- Name:
Example (Cloudflare):
Type: A
Name: zen
Content: 123.45.67.89
Proxy: Off (DNS Only)
TTL: Auto3. Clone Repository
On your server:
# Create application directory
sudo mkdir -p /opt/zen
sudo chown deploy:deploy /opt/zen
# Clone repository into the directory
cd /opt/zen
git clone https://github.com/Cadence-Intelligence/zen .4. Environment Configuration
Create Environment File
# Copy example file
cp .env.example .env
# Edit with nano
nano .env4.1 Core Configuration
Set these required variables:
# Database - Use remote managed PostgreSQL
DATABASE_URL=postgresql://user:password@host:5432/dbname
# GitHub tokens (same value for both)
GH_TOKEN=ghp_your_token_here
GITHUB_TOKEN=ghp_your_token_here
# Server settings
PORT=3090
ZEN_HOME=/tmp/zen # Override base directory (optional)GitHub Token Setup:
- Visit GitHub Settings > Tokens
- Click "Generate new token (classic)"
- Select scope:
repo - Copy token (starts with
ghp_...) - Set both
GH_TOKENandGITHUB_TOKENin.env
Database Options:
Note: SQLite is the default for local development and requires zero setup. For cloud deployments, PostgreSQL is recommended for reliability and network accessibility.
Recommended for Cloud: Remote Managed PostgreSQL
Use a managed database service for easier backups and scaling.
Supabase (Free tier available):
- Create project at supabase.com
- Go to Settings -> Database
- Copy connection string (Transaction pooler recommended)
- Set as
DATABASE_URL
Neon:
- Create project at neon.tech
- Copy connection string from dashboard
- Set as
DATABASE_URL
Alternative: Local PostgreSQL (with-db profile)
To run PostgreSQL in Docker alongside the app:
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/zenUse the with-db profile when starting services (see Section 7).
4.2 provider Setup
Configure at least one provider.
Claude Code
On your local machine:
# Install Claude Code CLI (if not already installed)
# Visit: https://docs.claude.com/claude-code/installation
# Generate OAuth token
claude setup-token
# Copy the token (starts with sk-ant-oat01-...)On your server:
nano .envAdd:
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxxxxAlternative: API Key
If you prefer pay-per-use:
- Visit console.anthropic.com/settings/keys
- Create key (starts with
sk-ant-) - Set in
.env:
CLAUDE_API_KEY=sk-ant-xxxxxSet as default (optional):
DEFAULT_AI_ASSISTANT=claudeCodex
On your local machine:
# Install Codex CLI (if not already installed)
# Visit: https://docs.codex.com/installation
# Authenticate
codex login
# Extract credentials
cat ~/.codex/auth.json
# On Windows: type %USERPROFILE%\.codex\auth.json
# Copy all four valuesOn your server:
nano .envAdd all four credentials:
CODEX_ID_TOKEN=eyJhbGc...
CODEX_ACCESS_TOKEN=eyJhbGc...
CODEX_REFRESH_TOKEN=rt_...
CODEX_ACCOUNT_ID=6a6a7ba6-...Set as default (optional):
DEFAULT_AI_ASSISTANT=codex4.3 Platform Adapter Setup
Configure at least one platform.
Telegram
Create bot:
- Message @BotFather on Telegram
- Send
/newbotand follow prompts - Copy bot token (format:
123456789:ABCdefGHIjklMNOpqrsTUVwxyz)
On your server:
nano .envAdd:
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHI...
TELEGRAM_STREAMING_MODE=stream # stream (default) | batchGitHub Webhooks
You'll configure this AFTER deployment (need public URL first).
For now, just generate the webhook secret:
# Generate secret
openssl rand -hex 32
# Copy the outputAdd to .env:
WEBHOOK_SECRET=your_generated_secret_hereGitHub webhook configuration happens in Section 9 after services are running.
Save and exit nano: Ctrl+X, then Y, then Enter
5. Database Migration
IMPORTANT: Run this BEFORE starting the application.
Initialize the database schema with required tables:
# For remote database (Supabase, Neon, etc.)
psql $DATABASE_URL < migrations/000_combined.sql
# Verify tables were created
psql $DATABASE_URL -c "\dt"
# Should show: codebases, conversations, sessions, isolation_environments,
# workflow_runs, workflow_events, messagesIf using local PostgreSQL with with-db profile:
You'll run migrations after starting the database in Section 7.
6. Caddy Configuration
Caddy provides automatic HTTPS with Let's Encrypt certificates.
Create Caddyfile
# Copy the example; no manual editing needed
cp Caddyfile.example CaddyfileThe Caddyfile reads {$DOMAIN} and {$PORT} from your .env automatically. Make sure DOMAIN is set:
DOMAIN=zen.yourdomain.comHow Caddy Works
- Automatically obtains SSL certificates from Let's Encrypt
- Handles HTTPS (443) and HTTP (80) -> HTTPS redirect
- Proxies requests to app container on port 3090
- Renews certificates automatically
7. Start Services
Setup Workspace Permissions (Linux Only)
# Create workspace directory and set permissions for container user (UID 1001)
mkdir -p workspace
sudo chown -R 1001:1001 workspaceOption A: With Remote PostgreSQL (Recommended)
If using managed database:
# Start app with Caddy reverse proxy
docker compose --profile cloud up -d --build
# View logs
docker compose --profile cloud logs -f appOption B: With Local PostgreSQL
If using with-db profile:
# Start app, postgres, and Caddy
docker compose --profile with-db --profile cloud up -d --build
# View logs
docker compose --profile with-db --profile cloud logs -f app
docker compose --profile with-db --profile cloud logs -f postgresMonitor Startup
# Watch logs for successful startup (use --profile with-db for local PostgreSQL)
docker compose --profile cloud logs -f app
# Look for:
# [App] Starting Z.E.N.
# [Database] Connected successfully
# [App] Z.E.N. is ready!Press Ctrl+C to exit logs (services keep running).
8. Verify Deployment
Check Health Endpoints
From your local machine:
# Basic health check
curl https://zen.yourdomain.com/api/health
# Expected: {"status":"ok"}
# Database connectivity
curl https://zen.yourdomain.com/api/health/db
# Expected: {"status":"ok","database":"connected"}
# Concurrency status
curl https://zen.yourdomain.com/api/health/concurrency
# Expected: {"status":"ok","active":0,"queued":0,"maxConcurrent":10}Check SSL Certificate
Visit https://zen.yourdomain.com/api/health in your browser:
- Should show green padlock
- Certificate issued by "Let's Encrypt"
- Auto-redirect from HTTP to HTTPS
Check Telegram (if configured)
Message your bot on Telegram:
/helpShould receive bot response with available commands.
9. Configure GitHub Webhooks
Now that your app has a public URL, configure GitHub webhooks.
Generate Webhook Secret (if not done earlier)
# On server
openssl rand -hex 32
# Copy output to .env as WEBHOOK_SECRET if not already setAdd Webhook to Repository
- Go to:
https://github.com/owner/repo/settings/hooks - Click "Add webhook"
Webhook Configuration:
| Field | Value |
|---|---|
| Payload URL | https://zen.yourdomain.com/webhooks/github |
| Content type | application/json |
| Secret | Your WEBHOOK_SECRET from .env |
| SSL verification | Enable SSL verification |
| Events | Select individual events: Issues, Issue comments, Pull requests |
- Click "Add webhook"
- Check "Recent Deliveries" tab for successful delivery (green checkmark)
Test webhook:
Comment on an issue:
@your-bot-name can you analyze this issue?Bot should respond with analysis.
10. Maintenance & Operations
View Logs
# All services
docker compose --profile cloud logs -f
# Specific service
docker compose --profile cloud logs -f app
docker compose --profile cloud logs -f caddy
# Last 100 lines
docker compose --profile cloud logs --tail=100 appUpdate Application
# Pull latest changes
cd /opt/zen
git pull
# Rebuild and restart
docker compose --profile cloud up -d --build
# Check logs
docker compose --profile cloud logs -f appRestart Services
# Restart all services
docker compose --profile cloud restart
# Restart specific service
docker compose --profile cloud restart app
docker compose --profile cloud restart caddyStop Services
# Stop all services
docker compose --profile cloud down
# Stop and remove volumes (caution: deletes data)
docker compose --profile cloud down -vTroubleshooting
Caddy Not Getting SSL Certificate
Check DNS:
dig zen.yourdomain.com
# Should return your server IPCheck firewall:
sudo ufw status
# Should allow ports 80 and 443Check Caddy logs:
docker compose --profile cloud logs caddy
# Look for certificate issuance attemptsCommon issues:
- DNS not propagated yet (wait 5-60 minutes)
- Firewall blocking ports 80/443
- Domain typo in Caddyfile
- A record not pointing to correct IP
App Not Responding
Check if running:
docker compose --profile cloud ps
# Should show 'app' and 'caddy' with state 'Up'Check health endpoint:
curl http://localhost:3090/api/health
# Tests app directly (bypasses Caddy)Check logs:
docker compose --profile cloud logs -f appDatabase Connection Errors
For remote database:
# Test connection from server
psql $DATABASE_URL -c "SELECT 1"Check environment variable:
cat .env | grep DATABASE_URLRun migrations if tables missing:
psql $DATABASE_URL < migrations/000_combined.sqlGitHub Webhook Not Working
Check webhook deliveries:
- Go to webhook settings in GitHub
- Click "Recent Deliveries"
- Look for error messages
Verify webhook secret:
cat .env | grep WEBHOOK_SECRET
# Must match GitHub webhook configurationTest webhook endpoint:
curl https://zen.yourdomain.com/webhooks/github
# Should return 400 (missing signature) - means endpoint is reachableOut of Disk Space
Check disk usage:
df -h
docker system dfClean up Docker:
# Remove unused images and containers
docker system prune -a
# Remove unused volumes (caution)
docker volume prune