setup-animus-node

Official
v2 · by Animus · 2026-10-14

Procedure for deploying Animus on a host, with two independent decision axes: deployment mode (full instance vs external node) and installation method (Docker vs direct install). Includes federation of full instances into a synchronized network.

animus deployment docker mesh nodes federation

Setting Up an Animus Node on a Host

Two Decisions Before You Start

Decision 1 — Full Instance or External Node?

Full instance External node
What it is The whole daemon: admin UI, LLM providers, agents, scheduler, channels A stateless satellite daemon (animusd --node)
Needs a model backend? Yes — an Ollama server (see setup-ollama-server) or cloud provider keys No — no LLM anywhere on it
Needs another Animus? No — this is a hub Yes — connects to an existing instance
State Persistent (state DB, config, vault) None — reconnects and re-registers
Network Listens on :8080 (admin + API) Outbound-only WebSocket, no open ports
Effort ~1–2 h with provider wiring ~15 min
Use when You're standing up an agent host, or the first Animus in a topology You want an agent to run tools on this machine from elsewhere

Rule of thumb: if in doubt, you want a full instance. An external node only makes sense when a hub already exists and this host is being added as tool surface.

A full instance can also be a member of a network. Instances can peer with each other (see Federation under Mode A): state converges across peers, so agents on any member see the same agent-global memory, and if one instance drops, consumers can be pointed at another member that already holds the converged state — and lease-based scheduler failover keeps scheduled work running on a surviving member. The federation phases are merged and tested (sync transport, anti-entropy healing, scheduler leases); the tracking issue remains open for remaining scope such as config replication.

Decision 2 — Docker or Direct Install?

Docker Direct install
Best when Container fleet, shared hosts, isolation matters Hardware integration; edge and IoT hosts
Hardware access Needs explicit --device / /dev passthrough Native — GPIO, USB, cameras, sensors just work
State Named volumes --config-dir / --data-dir on the host
Upgrades Pull new image, recreate git pull + rebuild
Prerequisites Docker (+ toolkit for GPU) Build toolchain; builds from source (no binary releases yet)

For hosts whose purpose is touching hardware — cameras, sensors, test benches — direct install avoids the container boundary entirely and is usually the right call. For everything else, Docker's reproducibility wins.

Quick map: Instance+Docker → A1 · Instance+Direct → A2 · Node+Docker → B1 · Node+Direct → B2.


Mode A — Full Instance

Prerequisites

  • A model backend reachable from the daemon: an Ollama server (complete setup-ollama-server first) or cloud provider credentials
  • Admin port free on 8080 (configurable)
  • Persistent storage planned: config dir, data dir (Docker: animus-data and animus-home volumes)

A1 — Docker

docker volume create animus-data
docker volume create animus-home

docker run -d --name animus \
  -p 8080:8080 \
  -v animus-data:/data \
  -v animus-home:/home/animus \
  --memory=4g --memory-swap=4g \
  --restart unless-stopped \
  mrsommer/animus-sudo:dev
  • Image variants: mrsommer/animus-sudo (animus user has passwordless sudo — for agents that manage their host OS) and the standard image without sudo. Prefer the standard image unless the agent genuinely needs host modification; sudo variants must never be publicly exposed.
  • Tags track branches: :dev builds from dev (via PUSH=1 ./scripts/build-animus-docker-sudo.sh dev), release tags from releases.
  • The memory limit is not optional — see Common Issues.
  • Hardware hosts: add --device /dev/video0 (etc.) per device; direct install (A2) avoids this friction entirely.

Verify: docker logs animus shows the daemon booting with no ERROR lines; curl -s http://<host>:8080/ returns the admin UI.

A2 — Direct Install

  1. Build from source (no binary release artifacts yet):
   git clone https://github.com/railstracks/animus.git
   cd animus && ./scripts/build.sh

Produces dist/bin/animusd (plus the admin UI). Requires a C++ toolchain and CMake; the build fetches its dependencies.

  1. Run it — either the helper (pidfile under dist/run, logs under dist/log, env-driven: ANIMUS_ADMIN_HOST, ANIMUS_ADMIN_PORT):
   ./scripts/start-daemon.sh

or the daemon directly with flags:

   ./dist/bin/animusd --daemon \
     --admin-host 0.0.0.0 \
     --admin-port 8080 \
     --config-dir /etc/animus \
     --data-dir /var/lib/animus
  1. Storage backend — default SQLite (--sqlite-path, default under the data dir); or PostgreSQL for shared/robust storage:
   --storage-backend postgresql --pg-host <host> --pg-database <name> \
     --pg-username <user> --pg-password <pass>
  1. Survive reboots — run under systemd. Minimal unit ⚠ (field-verify user/path conventions before production use):
   [Unit]
   Description=Animus daemon
   After=network-online.target

   [Service]
   User=animus
   ExecStart=/opt/animus/dist/bin/animusd --daemon --admin-port 8080 \
     --config-dir /etc/animus --data-dir /var/lib/animus
   Restart=on-failure

   [Install]
   WantedBy=multi-user.target

Verify: same as A1 — daemon log clean, admin UI on :8080.

First-boot bootstrap (both install methods)

On fresh state, set the initial admin credentials via the admin UI's first-run flow or --admin-password on first start.

⚠ If the instance is already initialized, --admin-password is silently ignored — check the Users page in the admin UI rather than trusting the flag.

Then create an API key for programmatic access: admin UI → API keys, or POST /api/users/{id}/tokens — note it requires the account password re-entered in the request body.

Verify: log into the admin UI; the API key works on GET /api/v1/agents with Authorization: Bearer <key>.

Wire the model backend (both install methods)

Admin UI → Providers:

  • Ollama: base URL http://<ollama-host>:11434, model tag copied verbatim from that server's ollama list (quant suffixes included), context window set to what the model actually supports. Verify connectivity from the daemon's network position.
  • Cloud providers: API key + base URL. Beware dual-billing endpoints (e.g. one key, two billing systems, different base URLs) — wrong pairing fails auth in confusing ways.

Verify: the Providers page shows the provider healthy; a test completion returns.

End-to-end verification

Create or pick a test agent, send a turn from the admin UI's session view, and watch a full round trip — prompt → provider → response rendered. If channels are planned, follow the configure-channel SOP from here.

Federation — peering full instances

Full instances can join a synchronized network. Configuration lives in the kernel config's node section (⚠ verify exact file format for your version — the keys are):

"node": {
  "id": 1,
  "peers": ["https://animus-b.example.com", "https://animus-c.example.com"],
  "sync_token": "<shared-secret>"
}

How it behaves:

  • Pull-based convergence — each node fetches peer outboxes past its per-peer cursor and applies last-writer-wins. There is no inbound push channel: the only listener is the existing admin API, so no new firewall surface.
  • Heartbeat rides the pull — every successful fetch is bidirectional liveness.
  • Degraded peers never block — a down peer means "no sync with that peer" (flagged in status, retried slower); live peers sync independently. A mixed-version peer is marked incompatible and skipped, not guessed at.
  • Agent-global state converges — agents on any member see the same memory; a dropped member's consumers can move to another member.
  • Leases (schedule_leases) coordinate scheduled work across members — schedule takeovers when a member drops.

Status: multi-phase work under issue #78 ("Node federation: multi-master Animus network"). Landed and merged: node identity + node-scoped id ranges (P1a, #87); replication outbox with LWW apply on every node (P1b, #88 — single-node deployments are federation-ready by default); pull-based sync transport with membership heartbeat (P1c,

89); anti-entropy — per-table digests, divergence heal, version-stamp

recovery (P1d, #90); and the scheduler line — scheduler tables replicate (P2a, #91) plus schedule leases with epochs and two-phase fenced dispatch (P2b, #92). The issue tracks remaining scope (e.g. config replication); verify against your version. Inspect state via the sync admin routes (AdminServerRoutesSync: outbox, handshake, status).

Ops notes

  • Upgrades: Docker — pull the new tag, recreate, same volumes. Direct — git pull + ./scripts/build.sh + restart via scripts/stop-daemon.sh / start-daemon.sh. State compatibility is generally forward-safe within a minor line; ⚠ snapshot config + data dirs first.
  • Portainer users: a UI "restart" of a git-backed stack redeploys from the stored branch's compose file — any externally-set spec is silently clobbered. Change the source of truth, not the running spec.
  • --log-level values are UPPERCASE only (DEBUG, not debug — the lowercase enum crash-loops the daemon).
  • Direct installs: --log-file (or ANIMUS_LOG_FILE) redirects all output to a file; the systemd unit's journal captures it otherwise.

Mode B — External Node (Satellite)

Prerequisites

  • A running Animus instance (the hub) with admin access to it
  • The remote host — anything that runs the binary or Docker; no GPU, no inbound ports

B1 — Docker

docker run -d --name animus-node \
  -e SERVER_URL=ws://<hub>:8080/ws/node \
  -e NODE_TOKEN=*** \
  -e NODE_SIGNING_KEY=*** \
  -e NODE_NAME=workstation \
  -e NODE_TOOLS=exec,file \
  --restart unless-stopped \
  mrsommer/animus-node:latest

Stateless — no volumes, outbound-only. wss:// when the hub is behind TLS. The node reconnects automatically (exponential backoff, 1s→60s cap).

Hardware hosts: add --device flags per device — but for a host whose job is hardware, prefer B2.

B2 — Direct Install

Same binary as a full instance — node mode is a command-line flag, not a separate build. Build per A2 step 1, then:

./dist/bin/animusd --node \
  --server-url ws://<hub>:8080/ws/node \
  --token *** \
  --signing-key *** \
  --node-name bench-camera \
  --node-tools exec,file

This is the natural fit for edge hardware (cameras, sensors, GPIO, USB instruments): no container boundary, native device access, and the node loads only the tool handlers in its allowlist — a small footprint for small machines.

Run it under systemd with the same pattern as A2 step 4 (Restart=always suits satellites).

Verify: hub admin UI → Nodes shows the node connected with hostname, OS, uptime, and its tool list.

Generate the token (hub side)

curl -X POST http://<hub>:8080/api/v1/nodes/tokens \
  -H "Content-Type: application/json" \
  -d '{"description": "bench-camera"}'

Returns token (nt_...) and signing_key (sk_...) — shown exactly once, store both securely. (Admin UI → Nodes → Generate Token works too.)

Grant agent access (default deny)

Agents see no nodes until granted:

curl -X PUT http://<hub>:8080/api/v1/agents/<agent>/allowed_nodes \
  -H "Content-Type: application/json" \
  -d '{"allowed_nodes": ["bench-camera"]}'

Verify end-to-end: from an allowed agent, run any tool with "__node": "bench-camera" added to its arguments (e.g. shell_exec of uname -a). The call executes on the remote host and returns. An agent without the grant must get an error referencing the disallowed node.

Security notes

  • Tool calls are HMAC-SHA256 signed per-token (signature + timestamp + nonce) — supply the signing key or the node can't verify command authenticity.
  • Revoking a token (DELETE /api/v1/nodes/tokens/{id}) disconnects its nodes immediately.
  • The node executes with the permissions of its daemon process — the operator of the remote host controls what agents can do there; the tool allowlist is the second layer.

Common Issues

Instance grows to multi-GB RSS and the host thrashes/OOMs. (Production incident: an email-channel reconnect storm ballooned RSS to 7.3 GB in ~40 min on a zero-swap host; the OOM kill was the cure.) Always set memory limits on instance containers, and prefer hosts with swap or watch RSS after enabling email channels.

Provider "healthy" but every turn silently fails. Provider type vs provider instance name confusion: some config paths resolve types (openai, zai, ollama), others expect the instance name. A mismatch yields empty provider responses, not an error. Check the provider entry's type field and the daemon log's LLM lines.

Node shows connected but agents can't use it. Default deny — the agent lacks an allowed_nodes grant, or the __node name doesn't match NODE_NAME exactly.

Token authentication fails on a new node. Token copied incompletely (they're long), or the token was revoked. Tokens are stored hashed server-side; regenerate rather than debugging a transcription.

Direct build fails on missing libraries. ./scripts/build.sh fetches its dependencies but needs the C++ toolchain and CMake present first. Check the first error, install the named package, re-run — the build resumes where it left off.

Federation peers not syncing. Check node.id uniqueness across members, identical sync_token, and peer URLs reachable on the admin API. A peer reporting a different protocol major is incompatible by design — align versions. Status and per-peer cursors are exposed via the sync admin routes.

Related SOPs

  • setup-ollama-server — the model backend a full instance consumes
  • configure-channel — channels on a running instance
Back

Version History

Version Date Author Description
2 2026-10-14 Animus Procedure for deploying Animus on a host, with two independent decision axes: deployment mode (full instance vs external node) and installation method (Docker vs direct install). Includes federation of full instances into a synchronized network.
1 2026-10-14 Animus Procedure for deploying Animus on a host, with two independent decision axes: deployment mode (full instance vs external node) and installation method (Docker vs direct install). Includes federation of full instances into a synchronized network.