Skip to main content

Self-host quickstart

Horologium ships as one Docker Compose file (docker-compose.yml at the repository root) — the same artifacts the hosted demo itself runs, just with HOROLOGIUM_READ_ONLY off and no data pre-loaded. There is no separate "cloud build." This is the product.

:::info Self-host is by invitation during the Alpha Per HORO-540, the source repository stays private during this Alpha phase, and there is no public git clone or anonymous docker pull for this quickstart yet. Self-host is available to approved Alpha/design -partner users, who receive access to the artifact through a private channel (repository collaborator access or a direct image handoff) rather than a public link on this page. If you've been given access, resume this quickstart from "What you get" below — every step past that point is accurate and runnable once you have the artifact. If you're reading this page without prior arrangement, the pilot/design-partner form is the way to request access. :::

What you get​

  • horologium-server — the Rust API and the web dashboard, served from the same origin and port. No separate Node process or reverse proxy needed to see the UI.
  • PostgreSQL, with a named volume so your data survives container restarts and docker compose down.
  • Optional horologium-mcp (read-only MCP adapter — see MCP / coding-agent integration) and horologium-intelligence (semantic proposal generation) services, gated behind Compose profiles so they don't start uninvited.

No external Horonomy cloud dependency of any kind is required to run any of this.

Prerequisites​

  • Docker with Compose v2 (docker compose version).

Quick start​

# Get the artifact via whichever private channel you were given (repository
# collaborator `git clone`, or the direct image handoff) -- see the callout
# above; there is no public clone/pull for this quickstart.
cd horologium
cp .env.example .env
# Edit .env: set POSTGRES_PASSWORD to something real, e.g.:
# openssl rand -hex 24
# (hex, not base64 -- see .env.example's comment: base64's `/`, `+`, `=`
# characters break the unescaped postgres:// URL docker-compose.yml builds
# from this value)
docker compose up -d

docker compose up builds horologium-server's image (a multi-stage build: the web dashboard is built first, then embedded into the Rust binary) and starts postgres and horologium-server. horologium-server runs pending database migrations on every startup, before it serves any traffic.

Check it's up:

curl -s http://127.0.0.1:8080/healthz # {"status":"ok"}
curl -s http://127.0.0.1:8080/readyz # {"status":"ready","database":"reachable"}

Open http://127.0.0.1:8080/app/ in a browser — that's the same horologium-server process serving the dashboard, no separate URL or port. (http://127.0.0.1:8080/ also works — it redirects to /app/.)

First data — seed the golden fixture​

There is no operator-facing collection CLI wired up yet in this Public Alpha slice — registering a real source and running a real collect/ collect_and_reconcile job (POST /v1/sources, POST /v1/jobs, see Evidence sources) is possible today only for local-filesystem-backed sources, driven by the separate horologium-runner binary, not this dashboard. For a populated instance to explore with the least setup, seed the same golden fixture the hosted demo runs instead:

docker compose exec \
-e HOROLOGIUM_DATABASE_URL="postgres://horologium:$(grep ^POSTGRES_PASSWORD .env | cut -d= -f2)@postgres:5432/horologium" \
horologium-server /usr/local/bin/seed_golden_fixture

This writes claims, relationships and findings directly — it is dev/demo tooling, not the real evidence-collection pipeline described in Evidence sources. Run it once against a freshly migrated database; re-running it mints duplicate rows. Refresh the dashboard afterward to see a documentation_conflict finding and a released_not_live finding, each with its full evidence citation and timeline.

Confirm the seed worked without relying on the dashboard rendering correctly — query the API directly:

curl -s http://127.0.0.1:8080/v1/entities | jq '.items | length' # > 0
curl -s http://127.0.0.1:8080/v1/findings | jq '.items | length' # > 0
curl -s http://127.0.0.1:8080/v1/findings | jq '[.items[].finding_kind] | unique'
# expect ["documentation_conflict","released_not_live"] among the kinds present

A 0 from either count means the seeder didn't run (or ran against the wrong database) — see Troubleshooting.

Configuration​

All configuration lives in .env (copy .env.example, which documents every variable and contains no secrets — only names and non-sensitive defaults; .env itself is gitignored).

The full, canonical environment variable reference — including horologium-runner's tuning knobs, the egress allowlist, bounded-worker resource limits, rollback limits and the cost/retention note — lives in docs/self-host.md's "Configuration" section in the repository root, not duplicated here.

Security posture — read before exposing the port to anyone but yourself​

Capability-scoped credentials are real and enforced (ADR-0021, HORO-1394) — every /v1/* route resolves a RequestScope from an Authorization: Bearer <token> header. No credential presented resolves to read:truth only (the same anonymous-read posture the hosted demo runs on); a presented credential that's malformed, unknown, revoked, expired, or scoped to a different workspace/product is rejected (401/403), not silently downgraded to anonymous. Mint one with horologium-credential mint --name <name> --caps <cap1,cap2> — see crates/horologium-api/README.md's "Operator credentials" section.

  • HOROLOGIUM_HOST_PORT defaults to 127.0.0.1:8080 — reachable from the host machine only. Do not bind it to a non-loopback address without a reverse proxy in front — auth is per-request, not a substitute for network isolation, and the anonymous read:truth default means an unauthenticated caller can still read every /v1/* GET route.
  • PostgreSQL is never published to the host — there is no ports: entry for it. horologium-server reaches it only over the internal Compose network.
  • The container runs as a non-root user in every image.
  • Neither horologium-mcp nor horologium-intelligence is ever issued a database credential — visible directly in docker-compose.yml's service definitions.

See Security, privacy and data boundaries for the full model, and SECURITY.md in the repository for the complete statement.

Optional services​

horologium-mcp​

Runs over stdio, not as an always-on network daemon — docker compose up never starts it on its own. Launch it per session:

docker compose run --rm horologium-mcp

See MCP / coding-agent integration for wiring this into a coding agent.

horologium-intelligence​

Defined behind the intelligence Compose profile so its topology — in particular, that it holds no database credential — is visible and testable. It has no standalone worker entrypoint yet in this Public Alpha slice; there is nothing to run today beyond building the image.

Stopping, restarting, upgrading​

docker compose down # stops and removes containers; your data (the named
# volume) is untouched
docker compose up -d # starts again -- migrations re-run (a no-op against
# an already-migrated schema)

git pull
docker compose build horologium-server
docker compose up -d # runs pending migrations on startup

If a migration is genuinely incompatible with your data, the container exits non-zero with a clear log message and does not start serving traffic — it does not attempt to run against a schema it doesn't recognize.

Backup and restore​

PostgreSQL-native tooling only — no custom scripts.

# Backup
docker compose exec postgres pg_dump -U horologium -d horologium -Fc \
> horologium-backup-$(date +%Y%m%d-%H%M%S).dump

# Restore (into a fresh, empty database -- this does not merge)
docker compose down
docker volume rm horologium-selfhost_horologium-pgdata
docker compose up -d postgres
docker compose exec -T postgres pg_restore -U horologium -d horologium --clean --if-exists \
< horologium-backup-20260101-120000.dump
docker compose up -d horologium-server

Troubleshooting​

docker compose up fails immediately with a POSTGRES_PASSWORD error. Expected — copy .env.example to .env and set a real password. There is deliberately no default.

horologium-server keeps restarting. Check docker compose logs horologium-server --tail 20. A "database migration failed at startup" message means the schema in your volume doesn't match this version's expectations — this is documented fail-closed behavior, not a bug. Restore from a pre-upgrade backup, or start from a fresh volume.

The dashboard loads but shows nothing. You haven't seeded any data yet — see First data above. There is no production evidence-collection pipeline wired up in this Public Alpha slice (see Evidence sources and Public Alpha limitations).

Port 8080 is already in use. Set a different HOROLOGIUM_HOST_PORT in .env and re-run docker compose up -d.

I need to inspect the database directly. It's never published to the host, by design. Use docker compose exec postgres psql -U horologium -d horologium to get a shell on the internal network instead of exposing the port.

For the full, unabridged version of this guide — kept in sync as the source of truth — see docs/self-host.md in your copy of the repository (the repository is private during this Public Alpha per HORO-540; there is no public URL for this file — approved partners already have repository access).