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

# Exact install command pending HORO-540 — see the callout above.
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 provider-collection CLI wired up yet in this Public Alpha slice. For a populated instance to explore, seed the same golden fixture the hosted demo runs:

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):

VariableDefaultPurpose
POSTGRES_PASSWORD(required)docker compose up fails loudly if unset
POSTGRES_DB / POSTGRES_USERhorologiumDatabase/role name
HOROLOGIUM_HOST_PORT127.0.0.1:8080Host address:port the API + dashboard are published on
HOROLOGIUM_API_BASE_URLhttp://horologium-server:8080What horologium-mcp calls, inside the Compose network

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

This Public Alpha implements no authentication of any kind — no session, no token. Any process that can reach the published port can read every /v1/* endpoint, including evidence excerpts, and submit proposals.

  • 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 performing real authentication.
  • 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).