Infrastructure

Infrastructure & Services (Docker)

As of the June 2026 migration the bot runs on a Hostinger VPS under Docker Compose with a PostgreSQL 16 database. This replaced the previous stack (Oracle VPS + systemd units + SQLite). The whole system is five containers on a single bridge network; source code is rsync'd to the VPS and bind-mounted into the Python containers (no image registry).

AspectPrevious (Oracle)Current (Hostinger)
Server92.4.66.95 · user ubuntu187.127.181.8 · user root
Process mgmtsystemd (3 units)Docker Compose (5 services)
DatabaseSQLite (arb_bot_records.sqlite3)PostgreSQL 16 (same logical schema, JSONB payloads)
Deployrsync + pip + systemctl restartrsync + docker compose up -d
TLSLet's Encrypt (host certbot)Let's Encrypt (certbot container)

Topology

# All services share one bridge network: trading-net
                          ┌───────────── Hostinger VPS (187.127.181.8) ─────────────┐
   Internet ──:80/:443──▶ │  nginx ──┬─ static SPA  (public/dist)               │
                          │          └─ proxy /api,/ws ─▶ dashboard :8080      │
                          │  arb-bot          (python -m arb_bot)               │
                          │  token-refresher  (token_refresher.py)             │
                          │  dashboard        (uvicorn :8080)                   │
                          │       └──────────── all read/write ─▶ postgres :5432 │
                          │  certbot          (run on demand for TLS certs)      │
                          └──────────────────────────────────────────────────────────┘
   Outbound: arb-bot/token-refresher ─▶ DhanHQ API  ·  notifications ─▶ Zoho Cliq / Telegram

Service Definitions

Defined in docker-compose.yml. Every Python service builds from the one Dockerfile (python:3.11-slim + requirements.txt) and bind-mounts the source tree at /app, so a code rsync + restart is a full deploy.

ServiceImage / BuildCommandRole
postgrespostgres:16-alpineSystem of record. All runtime state, metrics, trades, and backtest data. Schema auto-applied on first init from ./db.
arb-botbuild .python -m arb_botThe core trading engine — scan cycle, execution, monitoring, schedulers, command handlers.
dashboardbuild .uvicorn dashboard_server:app --host 0.0.0.0 --port 8080FastAPI backend: live P&L, trade data, docs API, OTP auth.
token-refresherbuild .python token_refresher.py --interval-sec 300Renews the DhanHQ access token independently of the bot loop (PIN + TOTP fallback).
nginxnginx:alpinePublic entry point on :80/:443. Serves the React SPA and reverse-proxies /api & /ws to dashboard:8080. Terminates TLS.
certbotcertbot/certbotrun on demandObtains and renews the Let's Encrypt certificate (shared volume with nginx).
postgres must be on trading-net Every service that talks to the DB resolves it by the Compose service name postgres. The postgres service therefore declares networks: [trading-net] — without it, postgres lands on the default network and the bot/dashboard cannot reach the database at all.

Volumes & Network

NameMounted atPurpose
postgres_data/var/lib/postgresql/dataDurable database storage.
./db (bind)/docker-entrypoint-initdb.d:roSchema SQL auto-run when the data volume is first created.
. (bind)/appSource code, mounted live into all Python containers.
bot_logs/app/logsarb-bot rotating logs.
certbot_certs/etc/letsencryptTLS certs (nginx read-only, certbot read-write).
certbot_www/var/www/certbotACME HTTP-01 challenge webroot.

All services attach to a single bridge network trading-net; only nginx publishes host ports (80, 443).

Database Layer (PostgreSQL)

  • Schema: db/001_initial_schema.sql — 25 tables (trades, metrics, journal, IV snapshots, bhavcopy OHLC, backtests, runtime state, etc.). JSON payloads use JSONB; identity columns use BIGSERIAL.
  • Migrations: db/migrate.py applies any pending db/*.sql and records them in schema_migrations (idempotent — safe to re-run).
  • Connection pool: arb_bot/db.py holds a psycopg2 ThreadedConnectionPool singleton with get_conn() / get_cursor() context managers and a JSONB auto-adapter. arb_bot/storage.py (still exposing the SQLiteStore class name for call-site compatibility) and execution/persistence.py are built on it.
  • Config: Config.DATABASE_URL (env DATABASE_URL) — e.g. postgresql://trading:<pw>@postgres:5432/trading.
Reset sequences after a bulk data import scripts/migrate_sqlite_to_postgres.py inserts explicit id values, which does not advance BIGSERIAL sequences. After a one-off data migration, run SELECT setval(...) per table to MAX(id) or the next insert collides on the primary key.

Deploy Workflow (Makefile)

Docker targets read .env.deploy (DEPLOY_SSH_KEY, DEPLOY_USER, DEPLOY_HOST, DEPLOY_REMOTE_DIR). The deploy scripts live in scripts/server_setup.sh and scripts/docker_deploy.sh.

CommandWhat it does
make docker-setupOne-shot VPS provisioning — installs Docker + Node, creates dirs, opens ufw 22/80/443.
make docker-deployFull deploy: build frontend & HTML → rsync → DB migrate → up -d --build.
make docker-deploy-codeCode-only deploy (skips frontend rebuild).
make docker-statusdocker compose ps on the server.
make docker-logsTail live logs from all containers.
make docker-restartRestart arb-bot dashboard token-refresher (not postgres/nginx).
make docker-sshSSH into the Hostinger server.
make docker-migrate-dataCopy the SQLite DB up and run the SQLite → PostgreSQL import.
make ssl-initIssue the Let's Encrypt certificate (run after DNS points at the VPS).
# Typical update once the stack is live
make docker-deploy-code      # rsync changed code + restart bot services
make docker-status           # confirm all five containers are Up
make docker-logs             # watch for "Entering main loop..."

TLS / Let's Encrypt

  • First issuance: with nginx not yet running, certbot --standalone binds port 80 to validate, then nginx starts with the real certs. (make ssl-init uses the --webroot flow once nginx is already serving the ACME path.)
  • nginx config: nginx/dashboard.conf redirects :80:443, serves the SPA from /usr/share/nginx/html, proxies /api & /ws to dashboard:8080, and gates /docs behind an auth_request to /api/auth/nginx-check.
  • Auto-renewal: a daily cron runs certbot renew --webroot and reloads nginx; the cert only renews inside the 30-day window.

Notification Egress

Region note The India-hosted VPS cannot reach api.telegram.org when Telegram is blocked in India. The bot's channel registry falls back to Zoho Cliq (CLIQ_ENABLED=true, TELEGRAM_ENABLED=false); DhanHQ and Cliq egress are unaffected. Re-enable Telegram by flipping TELEGRAM_ENABLED=true and restarting arb-bot once it is reachable.

Environment Variables (Docker-specific)

VariableMeaning
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DBCredentials the postgres container initialises with.
DATABASE_URLDSN every Python service uses — host is the Compose service name postgres.

A template lives in .env.docker.example; the live .env is rsync-excluded and copied to the server out-of-band. Restart policy is unless-stopped on every service, and Docker is enabled on boot, so the whole stack self-heals across reboots.