Deployment
Docker image, docker-compose stack, systemd unit, incident runbooks, BCP template.
Production-oriented configuration for running Horizon as a service. The deploy/ directory in the repo holds these files; they are not imported by the package. Copy, adapt, ship.
What’s in deploy/
| Path | Purpose |
|---|---|
Dockerfile | Multi-stage Python 3.12 image |
docker-compose.yml | Horizon + Prometheus + Grafana stack |
prometheus.yml | Scrape config for horizon:9100 |
systemd/horizon.service | Systemd unit for bare-metal or VM deployments |
runbooks/kill-switch.md | Incident playbook: kill switch fired |
runbooks/feed-stale.md | Incident playbook: feed stopped ticking |
runbooks/credential-rotation.md | Rotating broker credentials |
runbooks/disaster-recovery.md | Full host-loss recovery |
bcp.md | Business Continuity Plan template (Rule 206(4)-4) |
.env.example | Template for deployment secrets |
Container
docker build -f deploy/Dockerfile --build-arg HORIZON_EXTRAS=equity,options \
-t horizon:prod .
HORIZON_EXTRAS maps to pip install horizon[...]. Pick the subset you need. Empty = core only.
The image:
- Python 3.12 slim base.
- Builder stage: compiles and resolves optional deps.
- Runtime stage: no toolchain, just the venv + the
horizonpackage. - Runs as non-root
horizonuser (uid 1000, gid 1000). - Expects
/statemounted from the host for the audit log, DLQ, and any SQLite files. - Reads credentials from env vars (fed by
docker-composeor the systemd unit).
docker-compose stack
cd deploy
cp .env.example .env # fill in credentials
docker-compose up -d
Services:
- horizon: the trading daemon, exposes
/metricson port 9100. - prometheus: scrapes
horizon:9100with 30-day retention. - grafana: dashboards on port 3000 (change the admin password).
Mount ./state for durable storage:
audit.dbaudit_archive.dbdlq.dbdestruction.jsonl- any preset files
The healthcheck on the horizon service runs horizon version every 30s; docker restarts the container on three consecutive failures.
systemd
For non-container deployments:
sudo cp deploy/systemd/horizon.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now horizon.service
The unit hardens the service:
NoNewPrivilegesPrivateTmpProtectSystem=strictProtectHome- Empty
CapabilityBoundingSet - Write access only to
/var/lib/horizonand/var/log/horizon
Put credentials in /etc/horizon/credentials.env (mode 0600, owned by horizon:horizon).
Runbooks
Four playbooks ship. Wire them into the alerting bridge so an on-call page links straight to the right one.
- Kill switch fired: acknowledge, identify root cause, decide halt or restart.
- Feed stale: confirm connectivity, decide manual unwind or auto-reconnect.
- Credential rotation: staged rotation with dry-run verify + revocation.
- Disaster recovery: full host-loss restore from backup.
Business Continuity Plan
deploy/bcp.md is a skeleton compliant with Advisers Act Rule 206(4)-4. Counsel signs off on the final.
Structure:
- Governance (owner, sponsor, review cycle)
- Scope (covered systems)
- Risk assessment (scenario + impact + mitigation table)
- Recovery objectives (RTO / RPO)
- Procedures (linked to the runbooks)
- Communication plan (internal, clients, SEC, custodian)
- Data retention (links to Retention)
- Vendor management
- Testing (annual drill, quarterly kill-switch test, monthly audit verify)
- Sign-off
What’s intentionally missing
- No embedded TLS termination. Use a reverse proxy (Caddy, Traefik, Nginx) if you want to expose anything beyond
/metrics. - No built-in HA. Two Horizon instances writing to the same SQLite sink will collide. For multi-writer, migrate to Postgres (planned).
- No embedded Vault. Secrets come from the environment. The docker-compose file points at an
.envfile; production should read from Vault or AWS Secrets Manager directly via the Secrets Protocol. - No K8s manifests. Trivial to derive from the Dockerfile + envs; beyond the scope of this template.