Self-host
Run your own o10r hub.
The hub is a single Node process behind a reverse proxy. It speaks WebSocket to agents and clients, persists to a JSON file, and serves the web UI.
Quickstart with docker-compose
services:
o10r-hub:
image: registry.gitlab.com/edwardsalter/o10r/hub:latest
restart: unless-stopped
environment:
# The canonical public URL of this hub. Used to render /install.sh and
# the downloadUrl in latest.json. Refuses to render /install.sh if
# unset — never trust the inbound Host header for these.
O10R_PUBLIC_URL: https://o10r.example.com/hub
# Where the hub looks for tarballs published by CI.
O10R_RELEASES_DIR: /var/lib/o10r/releases
# Persistent data directory (users, agents, session tokens).
HUB_DATA_DIR: /var/lib/o10r/hub
# Optional: leave registration closed by default; only the first
# admin signs up via /setup. Set to "true" if you want a multi-user
# instance with open registration.
HUB_ALLOW_REGISTRATION: "false"
volumes:
- /var/lib/o10r/hub:/var/lib/o10r/hub
- /var/lib/o10r/releases:/var/lib/o10r/releases
expose:
- 19400
networks:
- web
networks:
web:
external: trueOn first boot the hub logs that /setup is required. Visit the URL in a browser, create your admin account, and you're online.
Environment variables
HUB_PORT— listen port. Default19400.HUB_DATA_DIR— persistent state directory. Default./data.O10R_PUBLIC_URL— canonical public URL. Required for/install.shrendering; recommended forlatest.jsondownloadUrl resolution.O10R_RELEASES_DIR— directory CI publishes tarballs to. Default/var/lib/o10r/releases.O10R_RELEASES_POLL_MS— how often to rescan the releases dir. Default 60 000 ms.HUB_ALLOW_REGISTRATION— open self-registration. Defaultfalse; only the first admin is created via/setup.HUB_WEB_DIR— pre-built renderer assets. Defaults to the directory baked into the image.NODE_ENV— set toproductionto disable the Vite dev middleware.
Releases directory layout
The hub serves tarballs out of a bind-mounted directory. CI writes them there with a single SHA256SUMS manifest; the hub watcher synthesises latest.json per channel and platform.
/var/lib/o10r/releases/
└── stable/
├── o10r-agent-0.1.0-linux.tar.gz
├── o10r-agent-0.1.0-win.tar.gz
└── SHA256SUMS # "<sha> <filename>" per lineThe Woodpecker pipeline in the repo (.woodpecker.yml) does this for you on every v* tag — see the architecture page for the wider update flow.
Reverse proxy
The hub speaks plain HTTP + plain WebSocket. Terminate TLS at your proxy and forward both. Below is a Caddy example for the split where the marketing site is at the apex and the hub lives at /hub.
o10r.example.com {
# Marketing site (this static site container) at the apex.
reverse_proxy /* o10r-site:80
# Hub at /hub — strip the prefix before forwarding so the hub still
# thinks it's at root. The hub's O10R_PUBLIC_URL handles the absolute
# URLs (downloadUrl in latest.json, install.sh defaults).
handle_path /hub/* {
reverse_proxy o10r-hub:19400 {
# WebSocket upgrade for /ws/agent and /ws/client.
header_up Connection {>Connection}
header_up Upgrade {>Upgrade}
}
}
}The hub's base-path support (O10R_BASE_PATH) is on the roadmap so the hub becomes self-aware of its mount point. Until then, the proxy strips /hub and the hub uses O10R_PUBLIC_URL to construct absolute URLs that include the prefix.
Backups
Everything stateful lives in two places. Snapshot them on whatever cadence makes sense:
$HUB_DATA_DIR/hub.json— users, agents (incl. ownerUserId), session tokens.$O10R_RELEASES_DIR/— tarballs and SHA256SUMS. CI can re-publish if lost, but if you've pinned an old release you can't.
Upgrading the hub itself
The hub image follows the same tag-based release flow as the agent. Pull, recreate the container, watch agents reconnect automatically — they have built-in backoff-with-reconnect so a 30-second restart is invisible to users.
docker compose pull o10r-hub
docker compose up -d o10r-hubSecurity checklist
- Set
O10R_PUBLIC_URLso the install script doesn't fall back to Host-header trust. - Leave
HUB_ALLOW_REGISTRATION=falseunless you want anyone with the URL to mint themselves an account. - Run the hub behind TLS. The wire protocol carries session tokens in the
Authorizationheader. - Restrict the releases dir to write-by-CI / read-by-hub. The hub never writes there.
- Bind the hub to
127.0.0.1or an internal network — only the reverse proxy needs to reach it.