Deployment

Deployment

Production now runs on Docker + PostgreSQL (Hostinger) As of June 2026 the live stack is Docker Compose on a Hostinger VPS with PostgreSQL — see Infra & Services (Docker) for the current topology, service definitions, and make docker-* targets. The systemd / SQLite material below describes the legacy Oracle setup and the still-present make deploy/sync/restart targets.

Package entry points

# All three are equivalent — pick whichever you prefer
python run.py
python -m arb_bot
# On Linux server the systemd service uses:
ExecStart=venv/bin/python run.py

Makefile shortcuts (from local machine)

Requires .env.deploy with DEPLOY_SSH_KEY, DEPLOY_USER (root), DEPLOY_HOST (187.127.181.8), DEPLOY_REMOTE_DIR (/opt/trading-bot). See Infra & Services for the full Docker reference.

CommandWhat it does
make docker-deployBuild HTML+frontend → rsync → DB migrate → rebuild & restart bot services (and re-resolve nginx)
make docker-deploy-codeCode-only deploy (skip the frontend rebuild)
make docker-statusdocker compose ps on the server
make docker-logsTail live container logs (Ctrl+C to stop)
make docker-restartRestart arb-bot dashboard token-refresher
make docker-sshSSH into the Hostinger server
make docker-setupOne-shot VPS provisioning (Docker + Node + ufw)
make ssl-initIssue the Let's Encrypt certificate (after DNS points here)
make build-htmlRebuild public/index.html + React app locally (run after editing any section file)
Deploys restart nginx on purpose Recreating the dashboard container gives it a new IP. nginx caches the dashboard upstream at startup, so docker_deploy.sh restarts nginx after every deploy — otherwise the proxy serves 502s (and OTP/login stop working) until nginx re-resolves.
Editing the playground Edit the relevant file in public/sections/, then run make build-html. The generated public/index.html is what gets deployed to Netlify. Never edit index.html directly.
Metrics workflow make fetch-metrics → downloads legacy metrics/*.json files locally → run python backtest_recovery.py to analyse partial-fill recovery performance, or use /analyze-trading-metrics in Claude Code for Config tuning recommendations.

Architecture Overview

The dashboard follows a modern decoupled architecture for better performance and interactivity:

  • Frontend: A React Single Page Application (SPA) built with Vite and Tailwind CSS. Located in frontend/, built into public/dist/.
  • Backend: A FastAPI server (dashboard_server.py) that provides real-time PnL, position tracking, and documentation metadata via REST APIs.
  • Nginx Proxy: Acts as the entry point, serving static files and proxying API requests to the FastAPI backend.

Systemd Services

Two separate services run on the production server:

ServicePurposeEntry Point
arb-bot.serviceThe core trading enginerun.py
token-refresher.serviceDhan token renewal independent of the bot looptoken_refresher.py --interval-sec 300
arb-dashboard.serviceFastAPI backend for dashboarddashboard_server.py

The token refresher unit template lives at scripts/token-refresher.service. The dashboard shows its last stored token health through /api/token/health.

Nginx Configuration

Nginx redirects all port 80 traffic, including direct IP requests, to https://portfolioplanner.online. The HTTPS server serves the SPA from /var/www/html/dist/ and proxies /api/* requests to localhost:8080. Configuration is maintained in scripts/nginx_dashboard.conf.

The dashboard server block includes 92.4.66.95, portfolioplanner.online, and www.portfolioplanner.online as accepted host names so domain traffic does not fall through to the default Nginx site.

Deployed documentation routes are not public. Requests to /docs/* use Nginx auth_request against /api/auth/nginx-check, which accepts only a valid dashboard session cookie. Unauthenticated users are redirected to /login before any docs route is served.

Dashboard Session Policy

Dashboard login is per-user Telegram OTP with an HttpOnly session cookie identifying a persisted, revocable session row (not a process-global singleton). Multiple users, and multiple sessions for the same user, remain valid concurrently — logging in again never invalidates another session, and logout revokes only the current one. See Multi-User Auth & RBAC for the full login flow, role model, and admin bootstrap.

MCP & Google linking

The self-service link flow is default-off. Runtime values belong only to the dashboard service; the Google OAuth credentials are bootstrap-time inputs and are deliberately blanked in every application container. Other services also receive blank/pinned-safe values so a shared env_file cannot accidentally enable linking or fail while parsing numeric TTLs.

VariableScopeContract
MCP_GOOGLE_LINKING_ENABLEDdashboardDefault false; enable only after bootstrap.
DASHBOARD_MCP_LINK_CLIENT_IDdashboard + bootstrapDefaults to dashboard-mcp-link.
DASHBOARD_MCP_LINK_CLIENT_SECRETdashboard + bootstrapAt least 32 characters; never log or expose to the SPA.
DASHBOARD_COOKIE_SECUREdashboardMust remain enabled for an HTTPS callback so the step-up grant is never sent over plaintext HTTP.
DASHBOARD_MCP_LINK_OIDC_ISSUERdashboardHTTPS Keycloak realm issuer; must equal AGENT_OAUTH_ISSUER when both are set.
DASHBOARD_MCP_LINK_OIDC_TOKEN_ENDPOINTdashboardOptional override; when set it must have the same origin as the issuer.
DASHBOARD_MCP_LINK_OIDC_JWKS_URIdashboardOptional override; when set it must have the same origin as the issuer.
DASHBOARD_MCP_LINK_REDIRECT_URIdashboard + bootstrapHTTPS URL with exact path /api/auth/mcp-link/callback; must exactly match Keycloak.
DASHBOARD_MCP_LINK_LINK_TTL_MINUTESdashboardPositive integer; default 10.
DASHBOARD_MCP_LINK_STEP_UP_TTL_MINUTESdashboardPositive integer; default 7.
DASHBOARD_MCP_LINK_MCP_URLdashboardHTTPS MCP endpoint copied by the user; default https://agents.portfolioplanner.online/mcp.
BROKER_SECRET_KEYdashboardStable valid Fernet key used to encrypt the PKCE verifier at rest; generate once, store securely, and preserve across restarts.
KEYCLOAK_GOOGLE_CLIENT_IDbootstrap onlyGoogle OAuth Web Application client id.
KEYCLOAK_GOOGLE_CLIENT_SECRETbootstrap onlyGoogle OAuth Web Application secret; never injected into the dashboard.

The dashboard startup validator requires non-placeholder HMAC and client secrets (at least 32 characters), a valid stable Fernet key, secure cookies for HTTPS, secure URLs (localhost HTTP is the development exception), exact callback path, positive TTLs, issuer agreement, and issuer-origin token/JWKS overrides. It reports variable names only and never echoes values.

docker compose cp scripts/keycloak_bootstrap.sh keycloak:/tmp/bootstrap.sh
docker compose exec \
  -e AGENT_KEYCLOAK_ADMIN \
  -e AGENT_KEYCLOAK_ADMIN_PASSWORD \
  -e AGENT_PUBLIC_BASE_URL \
  -e KEYCLOAK_GOOGLE_CLIENT_ID \
  -e KEYCLOAK_GOOGLE_CLIENT_SECRET \
  -e DASHBOARD_MCP_LINK_CLIENT_ID \
  -e DASHBOARD_MCP_LINK_CLIENT_SECRET \
  -e DASHBOARD_MCP_LINK_REDIRECT_URI \
  keycloak bash /tmp/bootstrap.sh

The bootstrap always reconciles the base Agent Gateway realm. Linking resources use an all-or-none group: when all of KEYCLOAK_GOOGLE_CLIENT_ID, KEYCLOAK_GOOGLE_CLIENT_SECRET, and DASHBOARD_MCP_LINK_CLIENT_SECRET are absent, the Google IdP and dashboard client are skipped; partial configuration fails immediately. With all three present, re-running reconciles the Google IdP, rotates both client secrets, enforces PKCE S256, disables direct grants, service accounts, and token exchange, and installs a client protocol mapper that copies the identity_provider session note to the ID-token broker claim.

Secrets handling. Keep Google credentials in the operator secret store and pass them only to the bootstrap command. Keep the dashboard client secret and BROKER_SECRET_KEY in the dashboard runtime secret store. The script does not print secret values.
Rollback. Set MCP_GOOGLE_LINKING_ENABLED=false and restart the dashboard. API routes return 404 and existing mapping rows remain for a later re-enable. The identity-link audit table is append-only; challenge and mapping tables are retained history, not described as append-only. Fully removing the Google IdP or the dashboard client is a deliberate Keycloak admin operation; an all-empty bootstrap run skips these resources and does not delete them.