Skip to content

Latest commit

 

History

History
232 lines (173 loc) · 9.31 KB

File metadata and controls

232 lines (173 loc) · 9.31 KB

Docker Compose Setup for Vatix Backend

This guide explains how to use Docker Compose to run the Vatix backend — either just the data layer (for host-run development) or the fully containerized stack.

Prerequisites

Services

Service Profiles Container name Notes
postgres (default) vatix-postgres PostgreSQL 16
redis (default) vatix-redis Redis 7 — caching + job queues
api app, api vatix-backend Fastify HTTP API, port 3000
indexer app, indexer vatix-indexer Stellar event indexer
finalization-worker app, workers, finalization-worker vatix-finalization-worker Resolution finalization loop
oracle-worker app, workers, oracle-worker vatix-oracle-worker Oracle submission queue consumer
settlement-worker app, workers, settlement-worker vatix-settlement-worker Trade settlement queue consumer
migrate tools, migrate vatix-migrate One-off prisma migrate deploy job
load-test tools, load-test vatix-load-test One-off local order-placement load test (~100 rps)

finalization-worker, oracle-worker, and settlement-worker have no HTTP port to probe, so each declares a healthcheck: that greps /proc/1/cmdline for its entrypoint script — docker compose ps and docker inspect report unhealthy if the process has crash-looped or hung, instead of the workers profile silently going dark (no trades settling, no resolutions finalizing) with every container still showing as "running".

Container names match the ones referenced in docs/runbooks/incident-runbook.md, so commands like docker logs vatix-indexer work as documented there.

postgres and redis have no profiles: entry, so they always start by default — this preserves the original host-run development workflow below. Every application process lives behind a profile so you opt in explicitly.

All application images (api, indexer, finalization-worker, oracle-worker, settlement-worker) run as the non-root vatix user (uid/gid 1001), set in the Dockerfile runtime stage. CI's docker-image-smoke job builds all worker target images and asserts id -u inside each container is non-zero, ensuring non-root execution is consistent across all processes. The api target additionally confirms postgres/redis healthchecks pass with the stack up.

Option A — Data layer only (host-run development)

This is the original workflow: run infra in containers, run the app processes on the host with tsx.

  1. Clone the repository and install dependencies:

    git clone https://github.com/vatix-protocol/vatix-backend.git
    cd vatix-backend
    pnpm install
  2. Copy environment variables:

    cp .env.example .env

    Edit .env if needed (see .env.example for details).

  3. Start the data layer:

    docker compose up -d

    This starts PostgreSQL (on port 5433) and Redis (on port 6379). No app profile is requested, so only postgres and redis come up.

  4. Initialize the database:

    pnpm prisma:generate
    pnpm prisma:migrate dev
  5. Run the backend processes on the host:

    pnpm dev                          # API
    pnpm indexer:dev                  # Indexer
    pnpm workers:finalization:dev     # Finalization worker
    pnpm workers:oracle:dev           # Oracle worker
    pnpm workers:settlement:dev       # Settlement consumer

Option B — Full containerized stack

Build and run every process as a container, using the Dockerfile at the repo root, which defines one build --target per process.

  1. Copy environment variables (same as above):

    cp .env.example .env
  2. Run database migrations before starting the app processes for the first time:

    docker compose --profile migrate up --build migrate
  3. Start everything:

    docker compose --profile app up -d --build

    This builds and starts postgres, redis, api, indexer, finalization-worker, oracle-worker, and settlement-worker.

    To run a subset, use the matching profile instead of app, e.g.:

    docker compose --profile api up -d --build              # postgres + redis + api only
    docker compose --profile workers up -d --build          # postgres + redis + all workers
    docker compose --profile settlement-worker up -d --build # settlement consumer only

Inside the compose network, app containers reach Postgres/Redis via the service DNS names postgres and redis (not the host-mapped localhost:5433 / localhost:6379 from .env.example) — docker-compose.yml overrides DATABASE_URL and REDIS_URL per service for this reason. Every other variable (API keys, Stellar config, log levels, etc.) is read from your local .env via env_file.

Stopping Services

docker compose --profile app down   # stop infra + app containers
docker compose down                 # stop infra only

Add -v to also remove the postgres_data / redis_data volumes.

Useful Commands

  • View running containers:
    docker compose ps
  • View logs for a specific process:
    docker compose logs -f api
    docker logs vatix-indexer --tail 100 --follow
  • Rebuild a single service after a code change:
    docker compose --profile app up -d --build api

Load Testing (local only)

scripts/load-test-orders.ts places signed synthetic orders against POST /v1/orders at a target rate (default ~100 rps) to exercise the API and matching engine under sustained write load.

⚠️ Local use only. This places real rows in whatever database the target API is backed by. It refuses to run against anything other than localhost / 127.0.0.1 / the compose api service unless you pass --allow-remote — never do that against a shared staging or production environment.

Run it via the tools/load-test compose profile once the API is up:

docker compose --profile api up -d --build      # start postgres + redis + api
docker compose --profile tools run --rm load-test

Or against a host-run pnpm dev API, without Docker:

pnpm load-test:orders                    # defaults: ~100 rps for 30s
pnpm load-test:orders -- --rps 50 --duration 10

The target API's default write rate limiter (10 req/60s per IP — see src/api/middleware/rateLimiter.ts) will throttle a single-IP load test almost immediately. To actually sustain the target rps for this local run, raise it just for that process:

RATE_LIMIT_WRITE_MAX=2000 RATE_LIMIT_WRITE_WINDOW_MS=1000 pnpm dev

See the header comment in scripts/load-test-orders.ts for the full option list (--url, --market-id, --traders, etc.) and prerequisites.

SLO gates & the CI nightly job

The run reports a capacity numbercapacityRps, the sustained rate of accepted (201) orders — for tuning the admission-control watermarks (SETTLEMENT_LAG_SHED_THRESHOLD et al., see ADMISSION_CONTROL_CONFIG.md), plus successRate (201s excluding 429s) and p50/p95/p99 latency.

Two optional SLO gates make a regression fail loudly instead of silently:

Flag / env Effect
--max-p95-ms <n> / LOAD_TEST_MAX_P95_MS Exit non-zero if observed p95 latency exceeds nms
--min-success-rate <r> / LOAD_TEST_MIN_SUCCESS_RATE Exit non-zero if the 201 rate drops below r (0..1)

With neither set (a local ad-hoc run) the gates are a no-op and the script always exits 0. .github/workflows/nightly-load-test.yml runs this nightly (cron 0 3 * * *) and on workflow_dispatch: it boots Postgres + Redis, seeds an ACTIVE market, starts the API, and runs pnpm load-test:orders with both gates set (defaults: p95 ≤ 1500ms, success rate ≥ 0.95). It never runs on pull requests.

Graceful shutdown

Every process registers SIGINT/SIGTERM handlers (see Graceful Shutdown), and the Dockerfile sets STOPSIGNAL SIGTERM, so docker compose stop / docker stop <container> triggers the same clean shutdown path used for Ctrl+C in local development.


For more details, see the main README.md and Deployment Runbook.