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).
| Aspect | Previous (Oracle) | Current (Hostinger) |
|---|---|---|
| Server | 92.4.66.95 · user ubuntu | 187.127.181.8 · user root |
| Process mgmt | systemd (3 units) | Docker Compose (5 services) |
| Database | SQLite (arb_bot_records.sqlite3) | PostgreSQL 16 (same logical schema, JSONB payloads) |
| Deploy | rsync + pip + systemctl restart | rsync + docker compose up -d |
| TLS | Let'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.
| Service | Image / Build | Command | Role |
|---|---|---|---|
postgres | postgres:16-alpine | — | System of record. All runtime state, metrics, trades, and backtest data. Schema auto-applied on first init from ./db. |
arb-bot | build . | python -m arb_bot | The core trading engine — scan cycle, execution, monitoring, schedulers, command handlers. |
dashboard | build . | uvicorn dashboard_server:app --host 0.0.0.0 --port 8080 | FastAPI backend: live P&L, trade data, docs API, OTP auth. |
token-refresher | build . | python token_refresher.py --interval-sec 300 | Renews the DhanHQ access token independently of the bot loop (PIN + TOTP fallback). |
nginx | nginx:alpine | — | Public entry point on :80/:443. Serves the React SPA and reverse-proxies /api & /ws to dashboard:8080. Terminates TLS. |
certbot | certbot/certbot | run on demand | Obtains and renews the Let's Encrypt certificate (shared volume with nginx). |
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
| Name | Mounted at | Purpose |
|---|---|---|
postgres_data | /var/lib/postgresql/data | Durable database storage. |
./db (bind) | /docker-entrypoint-initdb.d:ro | Schema SQL auto-run when the data volume is first created. |
. (bind) | /app | Source code, mounted live into all Python containers. |
bot_logs | /app/logs | arb-bot rotating logs. |
certbot_certs | /etc/letsencrypt | TLS certs (nginx read-only, certbot read-write). |
certbot_www | /var/www/certbot | ACME 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 useJSONB; identity columns useBIGSERIAL. - Migrations:
db/migrate.pyapplies any pendingdb/*.sqland records them inschema_migrations(idempotent — safe to re-run). - Connection pool:
arb_bot/db.pyholds apsycopg2ThreadedConnectionPoolsingleton withget_conn()/get_cursor()context managers and a JSONB auto-adapter.arb_bot/storage.py(still exposing theSQLiteStoreclass name for call-site compatibility) andexecution/persistence.pyare built on it. - Config:
Config.DATABASE_URL(envDATABASE_URL) — e.g.postgresql://trading:<pw>@postgres:5432/trading.
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.
| Command | What it does |
|---|---|
make docker-setup | One-shot VPS provisioning — installs Docker + Node, creates dirs, opens ufw 22/80/443. |
make docker-deploy | Full deploy: build frontend & HTML → rsync → DB migrate → up -d --build. |
make docker-deploy-code | Code-only deploy (skips frontend rebuild). |
make docker-status | docker compose ps on the server. |
make docker-logs | Tail live logs from all containers. |
make docker-restart | Restart arb-bot dashboard token-refresher (not postgres/nginx). |
make docker-ssh | SSH into the Hostinger server. |
make docker-migrate-data | Copy the SQLite DB up and run the SQLite → PostgreSQL import. |
make ssl-init | Issue 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
--standalonebinds port 80 to validate, then nginx starts with the real certs. (make ssl-inituses the--webrootflow once nginx is already serving the ACME path.) - nginx config:
nginx/dashboard.confredirects:80→:443, serves the SPA from/usr/share/nginx/html, proxies/api&/wstodashboard:8080, and gates/docsbehind anauth_requestto/api/auth/nginx-check. - Auto-renewal: a daily cron runs
certbot renew --webrootand reloads nginx; the cert only renews inside the 30-day window.
Notification Egress
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)
| Variable | Meaning |
|---|---|
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB | Credentials the postgres container initialises with. |
DATABASE_URL | DSN 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.