VeztaVezta
Guides

Deployment

Vercel, Docker, EAS Build, and CI/CD pipelines

Vezta has three deployment targets, each with its own CI/CD pipeline triggered by pushing to main.

Frontend (Vercel)

The frontend auto-deploys to Vercel on every push to main. No manual steps required.

SettingValue
PlatformVercel
TriggerPush to main
Build commandpnpm build (Turbopack)
Production URLhttps://vezta.io

Never deploy the frontend using the Vercel CLI manually. All deployments go through the Git-based auto-deploy flow. Production environment variables are managed through the Vercel dashboard.

Production environment overrides (.env.production):

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

Backend (Docker on Digital Ocean)

The backend runs in a Docker container on a Digital Ocean VM with a self-hosted GitHub Actions runner.

SettingValue
VMDigital Ocean droplet (206.189.151.6) -- Ubuntu 24.04, 8 GB RAM, 4 vCPU
App path/root/apps/vezta-be
Domainhttps://backend.vezta.io
Reverse proxyNginx on host, SSL via Certbot/Let's Encrypt
Port mappingHost 4001 to container 3001
DatabasePostgres 17 container (vezta-postgres) on the same VM
RedisSeparate vezta-redis container on internal Docker network

CI/CD Flow

Push to main triggers .github/workflows/deploy.yml on the self-hosted runner:

  1. Checks out code and rsyncs to /root/apps/vezta-be (preserves .env)
  2. Runs docker compose down && docker compose up -d --build
  3. Runs prisma migrate deploy inside the container
  4. Cleans up old Docker images (docker image prune -f)

The self-hosted runner is installed at /root/actions-runner-vezta-be/ on the VM. No GitHub Secrets are needed for deployment since the runner runs directly on the VM.

Docker Setup

Multi-stage Dockerfile using node:20-alpine with pnpm. Runs prisma generate at build time. Production entrypoint: node dist/src/main.

Container networking: vezta-api and vezta-redis are defined in docker-compose.yml on a custom vezta-network bridge. vezta-postgres runs as a separate container on the same network. The API binds to 127.0.0.1:4001 on host (localhost only -- Nginx proxies external traffic).

Manual Deploy

For hotfixes that cannot wait for CI:

# Copy updated files to server
scp src/some-file.ts root@206.189.151.6:/root/apps/vezta-be/src/some-file.ts

# Rebuild and restart
ssh root@206.189.151.6 'cd /root/apps/vezta-be && docker compose down && docker compose build --no-cache api && docker compose up -d'

Always verify the VM's .env file after any changes that depend on environment variables. The .env is at /root/apps/vezta-be/.env on the VM. Key differences from local: DATABASE_URL points to vezta-postgres container, REDIS_URL points to vezta-redis container, NODE_ENV=production, FRONTEND_URL=https://vezta.io.

Useful Server Commands

# SSH into the VM
ssh root@206.189.151.6

# View logs
docker logs vezta-api --tail 50
docker logs vezta-api -f           # Follow

# Check container status
docker ps --filter name=vezta

# Rebuild without cache
docker compose build --no-cache api && docker compose up -d

# Renew SSL certificate
certbot renew

Mobile (EAS Build)

The mobile app is built with EAS Build (Expo Application Services) via GitHub Actions on push to main.

SettingValue
CI Workflow.github/workflows/build.yml
Build toolexpo/expo-github-action@v8
Required secretEXPO_TOKEN

Build Profiles

EAS build profiles are defined in eas.json:

ProfileAPI TargetDistribution
developmentlocalhost:3001Dev client (simulator/emulator)
previewbackend.vezta.ioInternal distribution (TestFlight / APK)
productionbackend.vezta.ioApp Store / Play Store

CI Flow

Push to main triggers builds for both iOS (simulator) and Android (APK). Production App Store and Play Store submissions are handled separately through EAS Submit.

CI/CD Summary

WorkflowTriggerWhat It Does
test.ymlPR to mainRuns tests for all subprojects
deploy.yml (backend)Push to mainDocker build + deploy on VM
Vercel (frontend)Push to mainAuto-deploy to Vercel
build.yml (mobile)Push to mainEAS Build for iOS + Android

The shared VM (8 GB RAM) also hosts other applications. The backend is memory-optimized: price-sync only writes snapshots when prices change, and the heavy updatePriceChanges() query runs every 5 minutes rather than every price-sync cycle.

On this page