VeztaVezta
Guides

Deployment

Docker + CI/CD + branching policy

Vezta deploys frontend and backend as Docker containers on self-hosted VMs with a defined branch promotion policy.

Branch Policy

Promotion path is dev → staging → main (each subproject — vezta-be, vezta-fe, vezta-docs — is its own git repository with its own workflows):

  • dev -- integration. PRs merge here.
  • staging -- release candidate. PRs from dev (or release/*, hotfix/*) merge here.
  • main -- production. PRs from staging (or hotfix/*) only.

main requires 2 approvals + passing CI + Security Gates. staging requires 1 approval. Branch protections are enforced by the governance.yml workflow in each subproject repo.

Frontend (Docker on VMs)

The frontend deploys as a Docker container (ghcr.io/morcalabs/vezta-fe) on the same VMs as the backend.

EnvironmentDomainHost PortBranch
Productionvezta.io / www.vezta.io3005 → container 3000main (manual promote)
Stagingdev.vezta.io3002 → container 3000staging

Key env vars per environment:

# Production
NEXT_PUBLIC_API_URL=https://backend.vezta.io/api/v1
NEXT_PUBLIC_WS_URL=wss://backend.vezta.io

# Staging
NEXT_PUBLIC_API_URL=https://staging.vezta.io/api/v1
NEXT_PUBLIC_WS_URL=wss://staging.vezta.io

Production deploys (frontend)

frontend-deploy-production.yml takes a pinned image_ref as input (a ghcr.io/...:sha-... tag) and deploys that promoted image — there is no rebuild. Staging auto-deploys on push to the staging branch (with an optional image_ref input to skip the build). Rollback = re-run the workflow with a previous known-good image_ref.

Backend (Docker on self-hosted VMs)

The backend runs in 9 application containers per VM (1 API + 8 specialized workers) with self-hosted GitHub Actions runners.

Production VM

ComponentDetails
SSHssh agent@<production-vm> (key in team 1Password)
App path~/vezta/
Domainhttps://backend.vezta.io
Reverse proxyNginx → host 127.0.0.1:4001 (SSL via Certbot)
Port mappingHost 4001 → container 3001
Postgresvezta-postgres container (separate from compose), data in Docker volume
Redisvezta-production-redis container
Composedocker-compose.yml

Staging VM

ComponentDetails
SSHssh root@<staging-vm> (key in team 1Password)
App path/root/apps/vezta-be/
Domainhttps://staging.vezta.io
Reverse proxyNginx → host 127.0.0.1:4002
Port mappingHost 4002 → container 3001
Composedocker-compose.staging.yml
Containersvezta-staging-api, vezta-staging-worker-traders, vezta-staging-worker-market, vezta-staging-worker-misc, vezta-staging-worker-ai, vezta-staging-worker-websocket, vezta-staging-worker-rtds, vezta-staging-worker-sports, vezta-staging-worker-combos, vezta-staging-redis

Worker Topology

Each VM runs 9 application containers (1 API + 8 workers) sharing the same ghcr.io/<owner>/vezta-be-api image:

ContainerRoleMemory / CPULiveness
vezta-production-apiHTTP + WebSocket1.5 GB / 1.5 CPUport 3001
vezta-production-worker-tradersOrder execution, confirmations, fee collection, copy-trade, sniper, insider-signals2.5 GB / 2.0 CPUport 3002
vezta-production-worker-marketmarket-sync, trade-sync, short-duration, catalog-sync, holders sync2.0 GB / 1.5 CPUport 3003
vezta-production-worker-miscnotifications, rewards, share-card, leaderboard, alerts, spread-scanner2.0 GB / 1.0 CPUport 3004
vezta-production-worker-aiAgent chat + research queues (gated by FF_AGENT_QUEUE_SPLIT)2.0 GB / 1.0 CPUport 3006
vezta-production-worker-websocketPolymarket CLOB WS, Kalshi WS, Sports WS, User WS1.0 GB / 1.5 CPUport 3005
vezta-production-worker-rtdsPolymarket RTDS (Chainlink crypto prices)384 MB / 0.25 CPUport 3007
vezta-production-worker-sportsESPN sports-stats (scores, standings, resolve)384 MB / 0.25 CPUport 3008
vezta-production-worker-combosCombo-trade catalog + eligibility / settlement384 MB / 0.25 CPUport 3009

Rollback: scale specialized workers to 0 and run a single fallback container with WORKER_DOMAIN=all. Same image works in both modes.

Backend production deploys use a promoted-image model:

  1. Push to staging triggers build.yml + deploy-staging.yml -- builds and tags ghcr.io/<owner>/vezta-be-api:sha-<sha12>
  2. Production deploy workflow (deploy.yml) takes that image_ref as input -- no rebuild
  3. Rollback via scripts/rollback-to-image.sh <env> <image>

CI/CD Flow

Workflow locationWorkflowTriggerWhat it does
Each subproject .github/workflows/security.ymlPRCodeQL, dependency audit, secret scan
Each subproject .github/workflows/governance.ymlPRBranch promotion policy
Each subproject .github/workflows/benchmark-cicd.ymlWeeklyCI performance tracking
vezta-be/.github/workflows/test.ymlPR to main/stagingUnit tests + E2E
vezta-be/.github/workflows/build.ymlPush to staging/mainImage build, tag, push
vezta-be/.github/workflows/deploy-staging.ymlPush to stagingDeploy to staging VM
vezta-be/.github/workflows/deploy.ymlManual + image_ref inputProduction deploy with promoted image
vezta-fe/.github/workflows/frontend-ci.ymlPRVitest + Playwright
vezta-fe/.github/workflows/frontend-deploy-staging.ymlPush to stagingDocker build + deploy to staging VM
vezta-fe/.github/workflows/frontend-deploy-production.ymlManual + image_refDocker deploy to prod VM (promoted image, no rebuild)

Both production and staging CI auto-run prisma migrate deploy -- never run migrations manually on the VM.

Post-deploy health check

Verifies:

  • /api/v1/health (port 4001/4002 via Nginx)
  • In-container liveness probes for each worker (ports 3002 / 3003 / 3004 / 3005 / 3006 / 3007 / 3008 / 3009)

Server .env

Located at the app path on each VM (e.g. /root/apps/vezta-be/.env on staging, ~/vezta/.env on production). Always back up before edits:

ssh root@<staging-vm> 'cd /root/apps/vezta-be && cp .env .env.bak.$(date +%s)'

After .env changes, recreate containers (a docker compose restart is not enough — env is loaded at create time):

ssh root@<staging-vm> 'cd /root/apps/vezta-be && docker compose up -d'

Env changes must be applied to both the staging and production VM .env files. They are not synced automatically.

Telegram bot env divergence: TELEGRAM_BOT_TOKEN, the 3 TELEGRAM_*_CHANNEL_ID vars, VEZTA_BOT_TOKEN, VEZTA_BOT_WEBHOOK_SECRET, and BOT_PROD_ENABLED MUST hold different values on staging vs prod by design. Putting a prod bot token on staging causes staging signal-dispatch to pollute production user-facing channels.

Nginx-Layer Concerns

Nginx (not NestJS) owns several concerns in production:

  • CORS -- map $http_origin $cors_origin whitelist for vezta.io, dev.vezta.io, admin.vezta.io, localhost:3000. NestJS CORS headers are stripped via proxy_hide_header; Nginx adds its own with add_header ... always. OPTIONS preflight returns 204 directly from Nginx. Modifying enableCors() in main.ts has no effect in production.
  • Rate limits -- api_general zone (30r/s, burst 50), api_auth zone (5r/s, burst 10) on /api/v1/auth/, 30 conn/IP, 10s body/header timeouts, 2MB body max
  • SSL -- Auto-renewing via Certbot/Let's Encrypt
  • Backups -- *.cors-backup files preserve previous configs

The Polymarket CLOB proxy runs on the dedicated vezta-proxy VM (Tinyproxy on port 8888), routing CLOB API calls from the production and staging VMs past geo-restrictions. Its address is supplied to the backend via environment variable, never hardcoded.

CI Runner and Proxy VMs

CI and the CLOB geo-bypass proxy run on two separate VMs (previously a single combined host):

  • vezta-runner — hosts the self-hosted GitHub Actions runners. Jobs target these runner labels:
Runner labelUsed by
vezta-be-ci-stagingBackend staging build/deploy + tests
vezta-be-ci-prodBackend production build + deploy
vezta-fe-ci-stagingFrontend staging build/deploy + PR CI
vezta-fe-ci-prodFrontend production build + deploy
vezta-docs-builderDocs image build + production deploy
  • vezta-proxy — runs Tinyproxy on port 8888, routing Polymarket CLOB API calls from the production and staging VMs past geo-restrictions.

Manual deploy (hotfix only)

scp src/some-file.ts agent@<production-vm>:~/vezta/src/some-file.ts
ssh agent@<production-vm> 'cd ~/vezta && docker compose down && docker compose build --no-cache api && docker compose up -d'

This bypasses the promoted-image model — only use it when CI/CD is broken and the situation is urgent. Always reconcile by re-running CI as soon as possible.

Key Conventions

  • Never prisma migrate resolve --applied without running the migration. If a migration conflicts with manual DB changes, make the SQL idempotent (IF NOT EXISTS).
  • Never run manual ALTER TABLE on production via psql. Schema changes go through Prisma migrations.
  • Always dedup existing rows before adding unique constraints -- staging and production may have different data.

On this page