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) andhorologium-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):
| Variable | Default | Purpose |
|---|---|---|
POSTGRES_PASSWORD | (required) | docker compose up fails loudly if unset |
POSTGRES_DB / POSTGRES_USER | horologium | Database/role name |
HOROLOGIUM_HOST_PORT | 127.0.0.1:8080 | Host address:port the API + dashboard are published on |
HOROLOGIUM_API_BASE_URL | http://horologium-server:8080 | What 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_PORTdefaults to127.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-serverreaches it only over the internal Compose network. - The container runs as a non-root user in every image.
- Neither
horologium-mcpnorhorologium-intelligenceis ever issued a database credential — visible directly indocker-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).