o10r

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

docker-compose.ymlyaml
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: true

On 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

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.

O10R_RELEASES_DIRtext
/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 line

The 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.

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

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.

bash
docker compose pull o10r-hub
docker compose up -d o10r-hub

Security checklist