Installing Hivemind¶
This is the walkthrough for a fresh install of self-hosted Hivemind. Read it once end-to-end before running anything — there are four prerequisites + one copy-paste command + a short verification step. Plain text, copy-pasteable. American English. The bootstrap path delivers a per-customer signed bundle; the install script generates host secrets locally and never carries any of them over the wire.
Status — what this install places, and what it does not
This install places the Hivemind Hub: the API, the web UI, the Postgres database, the socket proxies, and the managed-update sidecar. After it finishes you have a running, healthy Hub with SSO or one local admin, the config and audit surfaces, metrics, and signed managed updates.
It does not start an agent runner. The component that starts an agent on a machine is separate from the Hub, and none of the four services a standard install runs is one — see First run for the actual service list.
A containerized agent runner is defined in docker-compose.yml behind an
opt-in runner profile. It needs four things, and only the first is done
for you with no involvement: install.sh mints the runner its own
HIVEMIND_RUNNER_API_KEY.
The Anthropic key is not prompted for on the invite path, and this surprises
people. The bootstrap runs install.sh --bundle, and --bundle forces
non-interactive mode — so every prompt is skipped, including that one. You are
never asked, and an install that reaches the end without the key completes
successfully with no agent runner. Supply it explicitly instead, either way:
# pass the path when you install …
curl -fsSL https://s.seglamater.app/i/<invite-id> -o hivemind-install.sh \
&& sudo bash hivemind-install.sh --claude-api-key-file /path/to/anthropic-key.txt
# … or place the file first and install normally
sudo install -d -m 0700 /srv/hivemind/secrets
printf %s 'sk-ant-...' | sudo tee /srv/hivemind/secrets/claude_api_key >/dev/null
sudo chmod 600 /srv/hivemind/secrets/claude_api_key
Pass or place a path, never the key on the command line — anything in a
command line is readable by every local account through /proc. See
Supplying the agent-runner credential.
The third comes from your bundle: a digest-pinned runner image reference.
docker-compose.yml resolves the runner as
${HIVEMIND_RUNNER_IMAGE:-hivemind-runner:dev}. Without that value the
fallback hivemind-runner:dev is used — a local development tag that exists in
no registry — and the pull fails.
The fourth is a git repository on this host, and it is the one people miss.
An agent works inside a repository, so the runner needs one. Nothing supplies
this for you: it is a path on your machine, and install.sh cannot guess which
repository you mean. You name it with one setting in .env:
docker-compose.yml mounts that path into the runner at the same path, so
there is no second value to keep in step and no volumes: block to write. It
must be the repository root — the directory containing .git, not a
subdirectory of it. Left unset, it defaults to /srv/hivemind-workspaces,
which nothing creates, and the runner refuses to start until you point it
somewhere real.
If you skip it, the runner does not start, and the way it fails is quiet.
Docker creates the container and cannot start it, so the entrypoint never runs
and never gets to explain itself. Its logs are empty, and plain
sudo docker compose ps does not list a created-but-never-started container at all
— you have to ask with -a. From the outside it looks like nothing happened.
install.sh checks this before it starts anything and refuses with the
specific reason, so the usual way to hit it is to enable the profile by hand
later.
This is checked when your bundle is minted, not left to you. A bundle is
refused outright if the release it pins publishes no runner image, precisely so
you are not handed one that would send you to hivemind-runner:dev. There is a
deliberate override for older releases that predate the runner; if it was used,
your bundle carries no runner reference and you were told at handover. Either
way the check below is the authority — read it rather than assuming.
How to tell, before you spend an afternoon on it:
- A registry reference, usually with an
@sha256:digest → your bundle carries it. Enabling the profile should pull a real image. hivemind-runner:dev→ your bundle does not carry a runner reference yet. That name exists in no registry, so enabling therunnerprofile will fail at the image pull. Ask your support contact for a bundle that includes a runner reference rather than setting the variable by hand — the value must match a digest you are entitled to pull, and guessing a tag will not get you one.
Ask compose, not a single file. An earlier version of this page said to run
grep '^HIVEMIND_RUNNER_IMAGE=' .env. That gave the WRONG ANSWER on a normal
install: install.sh moves the pin out of .env into
pin/docker-compose.override.yml, so the grep found nothing on exactly the
installs that were correctly pinned. The command above asks compose to resolve
the image the same way it will when it pulls, which is the only check that
cannot disagree with what actually happens.
We would rather state the condition than a date: when a bundle carrying that reference reaches you, the check above starts passing and nothing on this page needs to change.
Configuring an LLM provider (below) does not substitute: a provider key enables the LLM-backed surfaces, not agent execution.
If you enable the runner, know how it fails — the worst case is silent.
| what went wrong | how you find out |
|---|---|
| image pull fails (no runner reference in your bundle) | Loud. docker compose reports the pull failure and the install or up stops. |
| container starts, then crash-loops | Loud. The container restarts visibly; sudo docker compose ps shows it. |
| wrong or stale runner key | Quiet. The container stays Up; the 401 appears only in sudo docker compose logs hivemind-runner. |
| no runner running at all | Silent, and this is the one you will actually hit. /api/v1/health has no runner field, and nothing else in the API or the web UI reports whether a runner is present. A Hub with no runner looks exactly like a healthy Hub. |
So do not use /api/v1/health to answer "is my runner up" — it cannot tell
you. Check the container directly:
And verify the runner's Docker grant is closed before you rely on it — see Verifying the runner cannot read your secrets. That check takes one command and it is the difference between being protected and assuming you are.
Local accounts cap at one person. If more than one human needs an account, you need SSO — see Admin setup before you register.
Prerequisites¶
Hivemind runs as a small Compose stack — a server image, a Postgres, and the managed-update sidecar. You need:
| Requirement | Why | Verify |
|---|---|---|
| Linux host (x86_64) | The published image is glibc / amd64. | uname -m shows x86_64. |
| Docker Engine 24+ and Docker Compose v2 | The stack uses Compose's secrets: + per-service healthchecks. |
docker --version, docker compose version. |
Outbound HTTPS to updates.seglamater.app and registry.seglamater.app |
The bootstrap fetches your bundle and the image its signed manifest pins. | curl -fsS https://updates.seglamater.app/.well-known/keys/hivemind.pub returns a PEM. |
| ~2 GB free disk + ~512 MB RAM | Container + database + room for migrations. | df -h /srv and free -h. |
These figures are guidance, not a preflight
install.sh does not check RAM, disk, or the Docker Engine version
before running — verify them yourself with the commands above. For
Docker specifically, install.sh only detects and prints the installed
version; it never compares it against 24 or refuses an older one. The
numbers and versions here are a floor for a working install, not
something the installer enforces.
| Root on the host (sudo) | The installer hands each file in secrets/ to the container account that reads it (the server runs as uid 1000, the updater as uid 1001), and only root can give a file to another account. | sudo -v |
Run the installer with sudo
As root, the installer uses /srv/hivemind by default and creates it. To
choose another directory, put the variable after sudo, on the command that
runs the bootstrap — not in front of curl, where it would reach curl and
never the installer:
curl -fsSL https://s.seglamater.app/i/<invite-id> -o hivemind-install.sh \
&& sudo HIVEMIND_INSTALL_DIR=/path/of/your/choice bash hivemind-install.sh
After the install, .env and secrets/ belong to root. That is expected:
they hold the install's credentials. Run day-to-day sudo docker compose
commands, and any re-run of the installer, in the install directory with sudo.
If an installer from before this fix refused as an ordinary account.
Installers up to and including 0.1.95 ran as an ordinary account, stopped at
"Provisioning sidecar secrets" and asked for a re-run as root, and that re-run then
refused because the first attempt had left .env behind.
This applies only to an install that never started. Check first:
If it lists no hivemind_pgdata (or <your prefix>_pgdata), the install never
started: remove .env and secrets/ from the install directory (keep a runner key
file you placed in secrets/ yourself), then run the bootstrap again with sudo.
If it does list one, the install HAS run and that volume holds its database: do not
remove .env. Without it, the installer refuses to install over that volume, because
a new .env means a new database password that cannot open the data.
Optional, only if you want them:
- An LLM API key for the LLM-backed surfaces — public chat, the drafting
surfaces, and the agent tool-call loop (Anthropic, OpenAI, Ollama, or any
OpenAI-compatible endpoint). Hivemind boots and runs without one;
LLM-dependent surfaces report
LLM startup ping FAILEDand stay degraded until you configure a provider in.env(seebyok-llm.md). A provider key does not by itself give you running agents — that needs the separate runner component described in the status note above. -
A git repository on this host, if you want agents. An agent works inside a repository, and the runner needs one. Have the path ready before you install — it must be the repository root, the directory containing
.git. You name it with one setting in.env:Nothing supplies this for you and
install.shcannot guess which repository you mean. Skip it and the runner does not start — the container is created and never runs, so its logs are empty andsudo docker compose psdoes not list it without-a. Verify withgit -C /path/to/your/repo rev-parse --git-dir, which prints.gitfor a real repository. - A reverse proxy (Caddy, Traefik, nginx) if you'll terminate TLS in front of Hivemind. The container binds127.0.0.1:8585by default — that is intentional so nothing is reachable off-host during install. See SSO setup for a Caddy snippet with the correct forward-auth header handling. - An OIDC IdP (Authentik / Keycloak / similar) — optional only if exactly one person will ever use this instance. The default is local accounts, and local accounts cap at exactly one, permanently (see the status note above). For more than one person this isn't a nice-to-have, it's the only path — see "SSO in order, for more than one person" under Admin setup below.
Install¶
You should have received an invite URL from your support contact. It
looks like https://s.seglamater.app/i/<invite-id>.
Use the invite URL — a generic release bundle will not install. The
image registry requires authentication; it refuses anonymous callers and
will not issue an anonymous pull token. Your registry credentials arrive
in the per-customer bundle that the invite URL delivers, and nothing else
in the documented path supplies them. The release bundle published on
updates.seglamater.app deliberately carries no credentials, so an
install driven from it stops before the image pull with a message saying
so. If you have no invite URL, ask your support contact for one rather
than working around it.
Run:
curl -fsSL https://s.seglamater.app/i/<invite-id> -o hivemind-install.sh && sudo bash hivemind-install.sh
Your invite has a deadline, and each failure looks different¶
Invites are minted with a time limit and a use limit — 24 hours and 3 installs unless your support contact chose otherwise. Your invitation email states the exact expiry moment; after it, the link cannot be replayed and you need a fresh one.
Fetch to a file and then run it, as above, rather than piping into a shell. Piping is what makes the three ways this can go wrong indistinguishable:
| What happened | What you see | Exit |
|---|---|---|
| Invite expired / used up / not recognized | a plain-English sentence from the server saying which, and that nothing was installed | 1 |
| Cannot reach us at all | curl: (6) or curl: (7) …, and the installer never starts |
6 / 7 |
| Installer itself failed | the installer's own output, after it has clearly started | its own |
Piped into bash, the first case prints bash: line 1: {detail:invite
expired}: command not found and the second prints nothing at all and
exits 0. Neither is a usable signal. Do not use -f here either: it
suppresses the response body, which is where the server's explanation lives.
That bootstrap script:
- Fetches the bundle for your customer (
bundle.json) and the top-levelinstall.sh. - Pins the install layout under
$HIVEMIND_INSTALL_DIR(default/srv/hivemind/). To override it, set it aftersudo, on the command that runs the script — it has to reach the environment the SCRIPT runs in, not curl's:sudo HIVEMIND_INSTALL_DIR=/path/of/your/choice bash hivemind-install.sh. - Invokes
install.shwith the bundle. The bundle tellsinstall.shwhich image digest and which channel to pin, plus yourclient_id, the public URL you should use, and a few customer-specific defaults.
You'll see a sequence of [install] lines as the script:
- Establishes trust in three steps, and it is worth knowing which is which:
- Bundle integrity — verifies your bundle's SHA-256 against the checksum shipped beside it. The bundle itself carries no signature; this is a transfer-integrity check, not provenance.
- Trust anchor — fetches the cosign public key from the publisher and
pins it by SHA into
secrets/cosign_pub. - Signature — verifies the signed release manifest against that pinned
key and refuses to install if it does not match
(
Release manifest signature did NOT verify against the pinned key). Only after that does it read an image digest out of the manifest. So provenance is enforced at install time, not deferred. The updater performs its own cosign verification again at apply time, so a signature gate sits on both the install path and the update path.
- Pulls the digest-pinned
hivemind-serverimage fromregistry.seglamater.app/seglamater/hivemind-server@sha256:…and the Postgres image. - Generates per-host secrets (database password, admin API key, JWT
signing key, cookie secret, the integration encryption key for chatalot
bot tokens) into
./secrets/under your install dir, mode0600. These are written locally — they never leave your host and they are NOT in the bundle. - Renders
.envfrom defaults and the bundle metadata. - Brings up the Compose stack and waits for the server healthcheck to pass.
Expected total runtime: 1-3 minutes on a warm Docker cache, 3-5 minutes for the first pull on a clean host.
The script is idempotent. Re-running it picks up an existing install: it
reuses existing secrets (does not regenerate them), re-renders .env
only for keys it doesn't already see, and a docker compose up -d brings
the stack back to running. If you need a true fresh install, remove the
install dir and start over.
Supplying the agent-runner credential¶
Skip this section if you do not want this instance to execute agents. A standard install without it succeeds and is fully supported — you simply get no agent runner, and the installer says so in its closing summary rather than leaving you to discover it.
The runner needs your own Anthropic API key. It is yours throughout:
Seglamater never receives it, and it is never transmitted anywhere by the
install. It is read once, checked, and written to
<install-dir>/secrets/claude_api_key with mode 0600 on your machine.
The invite path never prompts you for it
The bootstrap runs install.sh --bundle, and --bundle forces
non-interactive mode. Every prompt is skipped, including the one asking
for this key. So on the invite path you cannot type it in — you must
supply it by one of the two routes below, or the install completes with
agents unavailable.
Either pass the path when you run the install:
curl -fsSL https://s.seglamater.app/i/<invite-id> -o hivemind-install.sh \
&& sudo bash hivemind-install.sh --claude-api-key-file /path/to/anthropic-key.txt
or place the file yourself first, and run the normal install command
unchanged — install.sh finds an existing key and reuses it:
sudo install -d -m 0700 /srv/hivemind/secrets
sudo install -m 600 /dev/null /srv/hivemind/secrets/claude_api_key
printf %s 'sk-ant-...' | sudo tee /srv/hivemind/secrets/claude_api_key >/dev/null
Pass or place a path or file — never the key itself on a command line.
Anything in a command line is readable by every local account on the host via
/proc, and lands in your shell history.
A trailing newline in the file is fine. An embedded newline is not, and both the installer and the runner refuse it with a message naming the cause — a stray line break mid-value is the usual way a copied key breaks.
Two more things the runner needs, and one of them is not yours
A credential alone is not enough:
-
A repository to work in.
install.shcannot provision this — it is a path on your host. Set it in.env:
Compose mounts that path into the runner at the same path. The default,
/srv/hivemind-workspaces, is not created for you.
The runner must be able to write it; see The account the runner works as.
-
A project, which a fresh install does not have. Agents belong to a project, and the agent surface refuses to start one while there are none — a fresh install has zero. Creating the first one is a single call. The project is owned by whoever the key belongs to:
curl -X POST http://localhost:8585/api/v1/projects \ -H "X-API-Key: $HIVEMIND_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"name": "my-project"}'ADMIN_API_KEYfrom your.envworks as the key. Full reference: API reference → Getting aproject_id. -
A runner image. This one is ours, not yours. If your bundle carries no runner image reference there is nothing to pull, and the installer will not start the profile — it tells you so explicitly rather than failing the install. A credential you supply is still stored and used automatically once an image is available.
The installer's closing Agent runner section states which of these are satisfied and what to do about any that are not.
The account the runner works as¶
The runner works as the account named by RUNNER_UID and RUNNER_GID in .env,
and it creates each agent's git worktree inside the repository at
HIVEMIND_RUNNER_REPO_PATH, so that account must be able to write the
repository. The installer sets both when it finds a runner credential and .env
has neither yet:
- to the owner of
HIVEMIND_RUNNER_REPO_PATH, when that path exists; - otherwise to the account that ran
sudo; - otherwise it leaves compose's default,
1000:1000, and says so.
It never writes 0 (the runner refuses to run as root) and never changes values
you set yourself. If you set HIVEMIND_RUNNER_REPO_PATH after installing, re-run
the install command, or set both values to the repository's owner, which
stat -c '%u %g' /path/to/your/repo prints. If that account cannot write the
repository, the installer names the problem and does not start the runner.
Verifying the runner cannot read your secrets¶
Run this if you enable the runner profile. It is the only check on this page
whose failure is a live credential disclosure rather than an outage.
What the requirement is¶
The agent runner is the component that executes agent tasks — content it did not
choose and that you did not write. It is deliberately the least-trusted service
in the stack. It is given a scoped Docker authority through its own
docker-socket-proxy container rather than the host's Docker socket, and that
proxy must run CONTAINERS=0:
Both values ship correct in docker-compose.yml and install.sh does not
change them. You only need to act here if something in your environment has
edited that file.
Why CONTAINERS and not something narrower¶
CONTAINERS=1 reads like "allow docker inspect". It is not. The proxy filters
by API path prefix, so the flag grants the whole /containers surface —
including GET /containers/{id}/archive, which returns the contents of any
file inside any container on the host. There is no setting that permits
ps/logs/inspect while denying archive: fine-grained controls exist for
write verbs only, so the flag is all-or-nothing.
Concretely, with CONTAINERS=1 an agent can read every Docker secret on the
box — admin_api_key, db_password, updater_token,
integration_encryption_key — regardless of which networks those services are
on. Moving a credential into a mounted secret file does not hide it from
that path; the endpoint reads through mounts.
The check¶
Run it from inside the runner container, which is the vantage that matters — a probe from your shell tests a path the agent does not use:
# 1. The name of your server container (prefix varies with HIVEMIND_CONTAINER_PREFIX):
sudo docker compose ps --format '{{.Name}}' server
# 2. From inside the runner, ask the proxy for a file out of the server container.
sudo docker compose --profile runner exec hivemind-runner \
curl -s -o /dev/null -w '%{http_code}\n' \
"http://hivemind-runner-socket-proxy:2375/containers/<server-container>/archive?path=/etc/hostname"
| result | meaning |
|---|---|
403 |
Correct. The grant is closed. This is the expected result. |
200 |
Act now. Any file in any container is readable by agent code. Set CONTAINERS: "0" on hivemind-runner-socket-proxy in docker-compose.yml, then sudo docker compose --profile runner up -d hivemind-runner-socket-proxy. |
connection error / 000 |
The probe did not reach the proxy, so it measured nothing. Fix the probe before reading anything into it — see the control below. |
/etc/hostname is used deliberately: it is harmless, it exists in every
container, and it is a bind mount, so a 200 also proves the endpoint reads
through mounts and not merely container layers.
The control — do not skip it¶
A probe that returns 403 because the hostname was wrong looks exactly like a
correctly closed grant. So confirm the proxy is reachable and answering:
sudo docker compose --profile runner exec hivemind-runner \
curl -s -o /dev/null -w '%{http_code}\n' \
"http://hivemind-runner-socket-proxy:2375/_ping"
Expect 200 here — and that is what makes the control work, not a sign that
something is wrong.
_ping is served by the proxy image regardless of the resource grants, and it
returns liveness only — no file contents, no environment values, nothing about
another container. /version is likewise left on and returns daemon version
information only.
/events is a third such default, and we turn it OFF. From 0.1.81 the
shipped compose sets EVENTS: "0" on the runner's proxy. Left at its default it
streams host-wide container names, images, labels and lifecycle transitions —
and the command lines of exec_create / exec_start, which on some hosts carry
secrets passed as arguments. A runner that can no longer call
/containers/json could otherwise enumerate the whole host by subscribing
there instead. If you are upgrading an existing install, set EVENTS: "0"
by hand alongside CONTAINERS: "0" — our updater cannot rewrite your compose.
_ping |
what the 403 above actually told you |
|---|---|
200 |
the proxy is up and refused you — the grant is closed, as intended |
connection error / 000 |
the proxy was never reached, so the 403 proved nothing — fix the path and re-run both |
If _ping returns 403, someone has also set PING: "0". That is harmless in
itself, but this control can then no longer tell "refused" from "unreachable" —
use /version in its place, or restore PING.
What this check does not cover¶
- It tests the containerized runner. If you run the runner as a host binary
under systemd instead, it holds your raw Docker socket with no proxy at
all, and no value in
docker-compose.ymlaffects that. - It tests the runner's proxy. The updater's and provisioner's proxies hold broader grants by design; the runner is kept off their networks, which is what contains them.
- A
403today is not a403after someone edits the compose file. Re-run it after any change todocker-compose.ymlordocker-compose.override.yml.
First run¶
After install.sh exits success, you should see one of these on the host:
curl -fsS http://localhost:8585/api/v1/health
# {"status":"ok","version":"0.2.1","integrations":{"chatalot_bot_ws":{"status":"inactive"}}}
sudo docker compose ps
# A standard install runs FOUR services:
# postgres, server, hivemind-socket-proxy, hivemind-updater
# all "Up", and the health-checked ones "(healthy)".
Why the compose file lists more services than you see running
docker-compose.yml defines nine services, but a standard install
starts four. install.sh brings up the default pair (postgres, server)
together with the updater profile, which adds hivemind-updater and its
hivemind-socket-proxy.
The other five are opt-in and a standard install does not start them:
web (the legacy profile), the two provisioner-profile services, and the
two runner-profile services (the agent runner and its socket proxy). Seeing
four rather than seven is correct. If you set HIVEMIND_DISABLE_UPDATER=1 you
get only postgres and server, and the managed-update path is unavailable.
If /api/v1/health returns ok, Hivemind is running. The server binds
127.0.0.1:8585 — a deliberate default, so nothing is reachable off the host
until you put a proxy in front of it.
Binding to localhost is a network boundary, not the API's authentication.
/api/v1/* is assembled as the router group named authenticated, and the
/api/* web mounts carry a blanket API-key requirement. The endpoints that
genuinely have no authentication are a short, specific list — see
what is not behind authentication.
To expose Hivemind on the internet you need a reverse proxy that terminates TLS,
sets the right Host header, and forwards /api/v1/* to the container. The
Caddy snippet in SSO setup does this and handles the
forward-auth headers correctly — use that one.
Logs live in the container; tail them with:
cd /srv/hivemind # or your install dir
sudo docker compose logs --tail=100 server
sudo docker compose logs --tail=100 postgres
The first time the server starts, look for these specific lines (they're the load-bearing init steps and they each name what's wrong if they fail):
database migrations appliedBYOK LLM provider initialised …(the line below it may be a WARN about the LLM ping failing — that's expected until you setHIVEMIND_LLM_API_KEYin.env, seebyok-llm.md).integration credential key loaded — chatalot/etc. integrations enabledlistening addr=0.0.0.0:3000— this is the port INSIDE the container and is expected. It is not the port you connect to. Compose maps it to127.0.0.1:8585on the host, which is why every command in this guide useslocalhost:8585. Seeing0.0.0.0:3000in the logs does not mean the API is exposed on all interfaces.
Admin setup¶
The first POST to /auth/register becomes the admin account.
Decide which path first — the two have opposite orderings
Going the SSO route (more than one person)? Then you never register at
all: with forward auth enabled /auth/register is closed, and your IdP
creates identities instead. Go straight to SSO setup and
skip the register step entirely. The rest of this box does not apply to you.
Staying on a single local account? Then complete the register step below
before you put a reverse proxy in front and before you set
HIVEMIND_PUBLIC_URL — while the API is still bound to 127.0.0.1, as
the default install leaves it.
Why the order matters: the account that registers first becomes the admin. While the instance is loopback-only, "first" can only mean someone with shell access to that host. Once it is reachable over the network, that is no longer true — so claim the admin account before you widen access, not after.
This is a sequencing requirement, not a preference. If you have already exposed the instance and not yet registered, close it off — stop the proxy or restrict it to your own address — and register before reopening it.
Local accounts cap at exactly one person — permanently
/auth/register succeeds only while the users table has no local account.
The second POST returns 409, and there is no admin route that creates a
second local user — the users API exposes only GET /me. So local accounts
are not a starting point you grow out of; they are a ceiling of one human.
If more than one person will ever sign in, set up SSO before you register here — see SSO setup. Registering first does not lock you out permanently, but it does mean switching to forward-auth and re-establishing identities through your IdP afterwards rather than simply adding a colleague.
SSO in order, for more than one person¶
The full reference — Caddy snippet, troubleshooting, a worked example — is SSO setup. This is the same sequence compressed to the eight steps in the order you do them, with every value's source named so you don't have to cross-reference two documents mid-setup.
- Authentik: create a Proxy Provider.
Applications → Providers → Create → Proxy Provider. Mode: Forward auth (single application) — this mode needs no "internal host": your reverse proxy (Caddy) does the actual proxying, Authentik only answers the auth-check request. External host: the same URL you'll set asHIVEMIND_PUBLIC_URLbelow. - Authentik: create an Application bound to that provider. Name and slug are yours to pick — just don't collide with another application already on the same Authentik instance.
- Authentik: add the provider to your outpost (the embedded outpost is fine for a single-VPS install) and give it ~5 seconds to reload.
- Authentik: create the group(s) you'll map to roles, under
Directory → Groups, and add the intended person as a member of each — at minimum one group for admins. There is no claim or property mapping to configure for this. A Proxy Provider sendsX-Authentik-Groupsautomatically for every authenticated request: the pipe-delimited names of every group that user is a direct member of, with no per-provider setup (authentik's own docs confirm this header format). The value Hivemind reads is exactly the group's name field as you typed it when you created the group — pick names to match what you'll set below (HIVEMIND_GROUP_ADMINetc.), or set those variables to match names you already have. Matching is case-insensitive; the two just need to agree. - Caddy: use the
forward_authsnippet in SSO setup — it includes therequest_header -X-Authentik-*strip lines that stop a client from forging its own identity headers. Use that snippet specifically, not a hand-rolled one; those strip lines are the difference between this being safe and not. - Hivemind
.env— turn it on and point it at your proxy:Everything else in this block has a working default and only needs setting if your IdP differs from Authentik's own defaults: which header carries the username/email/groups, and what delimits multiple groups (Authentik's own default isHIVEMIND_FORWARD_AUTH=true HIVEMIND_FORWARD_AUTH_TRUSTED_NETS=127.0.0.1/32 # or your proxy's address; see SSO setup's threat model|). - Hivemind
.env— the group→role mapping.HIVEMIND_GROUP_ADMINandHIVEMIND_GROUP_USERare established;HIVEMIND_GROUP_READONLYis the newer one (0.1.70, HIVE-1025) — it's what made the ReadOnly role reachable from SSO at all: | Variable | Default if unset | What it does | |---|---|---| |HIVEMIND_GROUP_ADMIN|hivemind-admin| The only route to the Admin role. Point it at a group you actually control. | |HIVEMIND_GROUP_USER|hivemind-user| Doesn't gate anything by itself — an identity in no mapped group already defaults to User (the default-allow floor, below) — but set it so the mapping is explicit rather than incidental. | |HIVEMIND_GROUP_READONLY| (unset) | The only way an SSO identity reaches the read-only role. Leave unset unless you have people who should observe without writing — see Role capabilities. | - Restart
server; confirm the boot log showsforward-auth enabled trusted_nets=[...] .... First login through your IdP auto-provisions the admin (if their group maps toHIVEMIND_GROUP_ADMIN) — no/auth/registercall needed.
If you set none of the three group variables, the instance behaves
exactly as it did before they existed: every SSO identity that doesn't match
HIVEMIND_GROUP_ADMIN resolves to User (the default-allow floor), and
ReadOnly stays unreachable from SSO — an empty HIVEMIND_GROUP_READONLY
can never match a real group name. This is enforced in auth::role::resolve_role
and pinned by two regression tests written for exactly this claim —
tests::absent_readonly_config_never_matches_and_behaves_like_before and
hive672_measurement::without_a_configured_readonly_group_resolve_role_never_returns_readonly,
both in crates/hivemind-server/src/auth/role.rs — not merely the
documented intent.
After install, create your admin account once. The request needs your
installer's ADMIN_API_KEY, which is in the install directory's .env. That
file belongs to root, so read it with sudo:
ADMIN_API_KEY=$(sudo grep '^ADMIN_API_KEY=' /srv/hivemind/.env | cut -d= -f2-) # or your install dir
curl -fsS -X POST -H 'Content-Type: application/json' \
-H "X-API-Key: $ADMIN_API_KEY" \
-d '{"username":"you","email":"you@example.com","password":"<strong password>"}' \
http://localhost:8585/auth/register
# {"id":"...","username":"you","email":"you@example.com","role":"admin"}
username, email and password are all required. The first registered user
is the admin; a second registration returns 409. There is no registration
form in the web UI: the home page opens the sign-in page (email and password).
Register with the command above, then sign in there, at the URL you set as
HIVEMIND_PUBLIC_URL in .env (set it before you enable any
operator-facing surface).
For service-to-service authentication (your other tools calling Hivemind),
use the ADMIN_API_KEY from your .env as the X-API-Key header. install.sh
generates it, and the server creates the admin user from it on first boot, so it
is the only admin API key the install ever has. The local admin account is for
browsers; the API key is for daemons.
SSO is not an upgrade for larger deployments — it is the only multi-user
path. With forward auth enabled, POST /auth/register is closed and every
identity comes from your IdP, which is what makes more than one account
possible. See SSO setup, and do it before registering above.
What lives where¶
<install-dir>/ (default /srv/hivemind)
├── docker-compose.yml # The stack. Don't edit; rerun install.sh to upgrade.
├── docker-compose.override.yml # (optional) Your local additions: reverse-proxy labels,
│ # extra networks, host-specific extra_hosts — AND the
│ # agent runner's repository bind mount, which nothing
│ # else supplies. Survives upgrades — install.sh never
│ # touches it.
├── .env # Configuration. Edit to set LLM keys, OIDC, public URL.
├── scripts/
│ ├── install.sh # Bootstrap also dropped this here; run with --help.
│ └── bundle.json # Your signed bundle. Pins image digest + channel.
├── secrets/ # 0700 dir, 0600/0644 files. Local-only, never shipped.
│ ├── db_password
│ ├── updater_token # HMAC for the in-process updater sidecar
│ ├── integration_encryption_key # ChaCha20-Poly1305 key for chatalot bot tokens at rest
│ ├── cosign_pub # Pinned cosign pubkey (does not rotate without a release)
│ ├── registry_creds # bundle-provided pull credentials, or empty
│ └── claude_api_key # agent runner's Anthropic key, if you set one (optional)
└── (a `postgres-data` Docker volume, not a host path)
The postgres data is a Docker named volume (hivemind_pgdata), not a
host bind mount. Back it up via
sudo docker compose exec postgres pg_dump -U hivemind hivemind --clean --if-exists
or with Borg / Restic against /var/lib/docker/volumes/hivemind_pgdata/.
--clean --if-exists is required: a dump taken without it cannot be replayed
over a database that still has its schema, which is the situation you are in
whenever you actually need the backup. See upgrade.md for the restore flags.
Common follow-ups¶
- Configure your LLM provider (the boot log will be loud until you do)
— see
byok-llm.md. - Turn on Facet, the assistant on every page: choose its provider and give it
a key — see
facet.md. - Set the public URL in
.env(HIVEMIND_PUBLIC_URL=https://hivemind.example.com) before you put a reverse proxy in front. - Switch to SSO — see
sso-setup.md. - Apply updates — managed-update path detects new signed releases and
applies them on operator approval. See
upgrade.md.
If anything in the install errored, the install dir is left in place for
inspection (no auto-teardown on failure). Re-run install.sh after fixing
the cause; it's idempotent.
If the post-install verifications above don't pass, see troubleshooting.md
— it has the boot-time failure modes and their fixes, including the most
common one ("integration routes return 503 integration_key_missing" — almost
always a .env not loading or secrets/ not mounted).
Running a second instance on the same host¶
A staging copy beside production, or a throwaway instance to rehearse an upgrade before applying it for real. Both are supported, and both need three things set — not one.
Docker names containers, networks and volumes on the daemon, not per project. A second instance that reuses any of those names does not get its own copy of that thing; it gets the first instance's.
HIVEMIND_CONTAINER_PREFIX=staging \
API_PORT=8586 \
DB_PORT=5434 \
sudo docker compose -p staging up -d
| Set | Why | Default |
|---|---|---|
HIVEMIND_CONTAINER_PREFIX |
Names the containers, the network, and the Postgres data volume. | hivemind |
API_PORT |
Host port for the API. Two instances cannot bind the same one. | 8585 |
DB_PORT |
Host port for Postgres, same reason. | 5433 |
With the installer, set the same three when you run it, and it does the rest:
install.sh writes them into this instance's .env together with
COMPOSE_PROJECT_NAME (which follows the prefix unless you set it), so every
later docker compose command, and the update service, names the same containers
and the same project. Before it writes anything, it checks the host: if another
directory's install is running under the same compose project, or already owns
<prefix>-server, it refuses and names that install. Pass
--allow-live-project only if you mean to manage that project from here.
On an existing install these values are kept as they are. If you set one to a
different value when re-running install.sh, it refuses and names both, because a
new prefix or project would rename the running containers and a new port would
re-bind them. Change them in .env on purpose, and recreate the stack.
-p staging on its own is not enough, and this is the part worth reading
twice.
HIVEMIND_CONTAINER_PREFIXis what keeps the two databases apart. The Postgres data volume is named from it. Start a second instance without setting it and Compose does not stop you — it prints a warning that the volume "already exists but was created for project …", and then uses the existing volume anyway. You get a second Postgres writing into the first instance's data directory. Nothing errors, and the damage is silent.The same is true of the shared network name and of every container name. Set the prefix, and give the second instance its own ports.
Confirm the two are genuinely separate before trusting either:
docker volume ls --format '{{.Name}}' | grep pgdata
# expect one volume per instance, e.g. hivemind_pgdata and staging_pgdata
If that shows only one volume while two instances are running, stop both now — they are sharing a database.
Leaving HIVEMIND_CONTAINER_PREFIX unset keeps every name exactly as it was, so
an existing single-instance install is unaffected and needs no change.