AnimusDB

Get a cluster running

Run the container image, or build from source with Cargo. There are no distro packages yet. Either way it is a couple of commands, plus a compile if you build.

Run with Docker

The quickest way to try it: one node in a container, with the DynamoDB endpoint and both web consoles published on your machine. Generate a single-node config that binds every interface (the image has no shell, so the config comes from the image's own gen-config), then start the node with a named volume for its data.

$ docker run --rm ghcr.io/animus-db/animusd:latest gen-config --nodes 1 --host 0.0.0.0 > animus.json
$ docker run -d --name animusd \
    -p 7102:7102 -p 7103:7103 -p 7105:7105 \
    -v animus-data:/var/lib/animus \
    -v "$PWD/animus.json:/etc/animus/cluster.json:ro" \
    ghcr.io/animus-db/animusd:latest \
    --config /etc/animus/cluster.json --node 0 --dir /var/lib/animus
PortWhat it serves
7102The DynamoDB endpoint: http://localhost:7102.
7103The operator dashboard: http://localhost:7103.
7105The data console: http://localhost:7105.

Data lives in the animus-data volume, so it survives docker rm. Once it logs ready, use the AWS CLI exactly as in Connect to it, with ANIMUS=http://localhost:7102.

What this image is

latest tracks the main branch; there are no version tags yet. It is built for linux/amd64 only, so Apple-silicon Macs run it under emulation. It is pre-alpha, and both consoles are unauthenticated: publish the ports only on a machine you trust (use -p 127.0.0.1:7102:7102 to keep them off the network).

Prefer to build it yourself, or want a three-node cluster in one process? Use the source build below.

Prerequisites

A Rust toolchain and Git. The repository pins its toolchain, so rustup will fetch the right compiler automatically the first time you build.

  • Rust 1.96 or newer — install with rustup.
  • Git, and a machine with a few gigabytes free for the build.
  • Linux or macOS. Windows is untested.

Optional, for talking to the database once it's up:

  • The AWS CLI or any AWS SDK, for the DynamoDB endpoint.

Build from source

The alternative to the container image, and the way to run a multi-node cluster in one process.

$ git clone https://github.com/animus-db/animus-db.git
$ cd animus-db
$ cargo build --release

That produces two binaries in target/release:

BinaryWhat it is
animusdThe node server. One process per node; it hosts the control plane, the data plane and every wire endpoint.
animusThe operator and client CLI — cluster status, simple get/put, and the whole admin subcommand tree.

A debug build (cargo build) is fine for experimenting and compiles faster, but is markedly slower at runtime. Use --release for anything you intend to measure.

Run a cluster

The fastest way to see AnimusDB work is a whole cluster in one process. Nodes bind OS-assigned ports and print them at startup.

$ ./target/release/animusd --cluster 3
animusd: started 3-node cluster (CP)
  node 0: client 127.0.0.1:52104 — dynamo http 127.0.0.1:52105
          — admin http://127.0.0.1:52107 — console http://127.0.0.1:52109
  node 1: client 127.0.0.1:52111 — dynamo http 127.0.0.1:52112 …
  node 2: client 127.0.0.1:52118 — dynamo http 127.0.0.1:52119 …
animusd: ready — Ctrl-C to stop
Keep the data, or don't

State is written under a fresh, unique-per-process temporary directory by default, so running this command again never picks up the previous run's state. Pass --dir ./cluster-data to reuse a directory of your choosing, or --ephemeral to also swap the tablet-data engine for a volatile in-memory one.

Add --auto-split-bytes 67108864 to watch tablets split as they grow, which is the interesting behaviour to poke at on a laptop.

Connect to it

Any node accepts any request and routes it internally, so you can talk to whichever one you like. The examples below use node 0's printed dynamo port.

The AWS CLI

SigV4 signature verification is opt-in and off by default, but the AWS CLI insists on credentials being present regardless — any placeholder values work unless you've turned verification on.

$ export AWS_ACCESS_KEY_ID=local AWS_SECRET_ACCESS_KEY=local AWS_REGION=us-east-1
$ export ANIMUS=http://127.0.0.1:52105

$ aws dynamodb create-table --endpoint-url $ANIMUS \
    --table-name orders \
    --attribute-definitions AttributeName=id,AttributeType=S \
    --key-schema AttributeName=id,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST

$ aws dynamodb put-item --endpoint-url $ANIMUS --table-name orders \
    --item '{"id": {"S": "a-1"}, "total": {"N": "42"}}'

$ aws dynamodb get-item --endpoint-url $ANIMUS --table-name orders \
    --key '{"id": {"S": "a-1"}}'
{ "Item": { "id": { "S": "a-1" }, "total": { "N": "42" } } }

Python (boto3)

import boto3

db = boto3.resource(
    "dynamodb",
    endpoint_url="http://127.0.0.1:52105",
    region_name="us-east-1",
    aws_access_key_id="local",
    aws_secret_access_key="local",
)

table = db.Table("orders")
table.put_item(Item={"id": "a-2", "total": 99})
print(table.get_item(Key={"id": "a-2"})["Item"])

JavaScript (AWS SDK v3)

import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";

const client = new DynamoDBClient({
  endpoint: "http://127.0.0.1:52105",
  region: "us-east-1",
  credentials: { accessKeyId: "local", secretAccessKey: "local" },
});

await client.send(new PutItemCommand({
  TableName: "orders",
  Item: { id: { S: "a-3" }, total: { N: "7" } },
}));

The animus CLI

# cluster status, against a node's client port
$ ./target/release/animus status 127.0.0.1:52104

# raw key/value access, against a table
$ ./target/release/animus put 127.0.0.1:52104 orders k1 hello
$ ./target/release/animus get 127.0.0.1:52104 orders k1

# operator surface, against a node's admin port
$ ./target/release/animus admin status 127.0.0.1:52107

Open the consoles

Two web UIs are compiled into animusd; neither needs a build step or an internet connection.

Operator dashboard

On the admin port. Cluster and node health, the tablet map, Raft internals, storage and WAL introspection, and gated operator actions like split, flush, drain and decommission.

Data console

On the console port. A DynamoDB-shaped app for application developers: browse tables, inspect and edit items, review a table's configuration and its stream.

Both are printed in the startup banner. Neither has authentication — see the security note. (SigV4 verification, when enabled, applies only to the DynamoDB client port.)

Run a real multi-node cluster

For separate machines, generate a config once, copy it to every node, and start one process per node with a distinct --node index.

  1. Generate a cluster config

    $ animusd gen-config --nodes 3 --host 10.0.0.1 > cluster.json

    The generated config lists every node's identity and its six listener addresses. Edit the hosts so each entry names the right machine, then copy the same file to all of them. Ports start at --base-port (7100 by default).

  2. Start each node

    # on the first machine
    $ animusd --config cluster.json --node 0 --dir /var/lib/animusdb
    
    # on the second, and so on
    $ animusd --config cluster.json --node 1 --dir /var/lib/animusdb
    $ animusd --config cluster.json --node 2 --dir /var/lib/animusdb
  3. Check it came up

    $ animus admin status 10.0.0.1:7103
    $ animus admin health 10.0.0.1:7103

    Or open the dashboard at http://10.0.0.1:7103.

Add and remove nodes

A new node does not need the cluster config. Give it the intra-cluster address of any running node and it registers itself, learns the topology, and starts receiving tablet replicas.

# on the new machine — 7104 is node 0's intra port
$ animusd join --seed 10.0.0.1:7104 --base-port 7100 --dir /var/lib/animusdb

Removing one is a drain followed by a decommission, so replicas move off before the node goes away:

$ animus admin drain 10.0.0.1:7103 n3
$ animus admin drain-status 10.0.0.1:7103 n3
$ animus admin decommission 10.0.0.1:7103 n3

Split control and data

Larger clusters usually want a small, stable control quorum that is not competing with storage for resources. Generate a split config and run each role explicitly.

$ animusd gen-config --control-nodes 3 --data-nodes 5 --host 10.0.0.1 > cluster.json

# control-only: metadata quorum, no storage engine
$ animusd control --config cluster.json --node 0

# data-only: hosts tablets, mirrors metadata from the control plane
$ animusd data --config cluster.json --node 3

# or let a data node join by seed instead of by config
$ animusd data --seed 10.0.0.1:7104 --base-port 7200

To try the shape without eight machines, animusd --cluster-control 3 --cluster-data 5 runs the whole split deployment in one process.

Ports

Each node owns a block of six consecutive ports, starting at its base port (7100 for node 0 in a generated config, then 7106, 7112, and so on).

OffsetNode 0ListenerPurposeExposure
+07100internalRaft traffic between nodesCluster-internal
+17101clientThe native length-prefixed protocol the animus CLI speaksTrusted
+27102dynamoDynamoDB JSON over HTTP, plus GET /metricsTrusted
+37103adminAdmin JSON API and the operator dashboardCluster-internal
+47104intraNode-to-node request forwarding and metadata mirroring; the address to --seedCluster-internal
+57105consoleThe data console (not bound on control-only nodes)Cluster-internal

Only the DynamoDB port can verify a caller, and only if you turn on SigV4 — off by default. Everywhere else "trusted" means a network you control. Keep the internal, intra, admin and console ports off any externally reachable address.

Common flags

FlagEffect
--config FILE --node IRun node I of a generated cluster config. The normal production invocation.
--cluster NRun an N-node cluster in a single process. Development convenience.
--dir DIRWhere this node keeps its write-ahead logs, SSTables and snapshots.
--ephemeralUse a volatile in-memory engine for tablet data instead of the on-disk default. Control-plane state still lives under --dir for that run, but --cluster N/--cluster-control/--cluster-data default --dir to a fresh directory per process either way, so back-to-back runs never rehydrate a previous one.
--auto-split-bytes BSplit a tablet once it exceeds B bytes.
--quiesce-after SECSHow long a tablet group must be idle before it stops ticking. On by default.
--no-heartbeat-batchTurn off per-destination heartbeat batching (coalescing every co-hosted tablet group's heartbeat toward the same peer into one wire frame). On by default.
--no-shared-walTurn off the shared write-ahead log (routing every co-hosted tablet group's writes through one per-node file with one coalesced fsync per round, instead of a private WAL file per group). On by default; flipping this against an existing data directory written under the other layout is a startup error, not a silent reset — start against a fresh --dir instead.
--stream-seal-bytes B
--stream-seal-age SECS
--stream-retention SECS
When change-log segments are sealed into stream shards, and how long they are kept.
--segment-store dir:PATH
--segment-store s3://bucket[/prefix]?endpoint=...
Store sealed stream segments on a filesystem path or an S3-compatible bucket instead of replicating them across the cluster.
--backup-store cluster|fs:PATH|s3://bucket[/prefix]?endpoint=...Where on-demand backups and point-in-time-recovery data live: replicated across the cluster (default), a filesystem path, or an S3-compatible bucket — the actual disaster-recovery option, since it survives a total cluster loss.
--s3-credentials PATH
--allow-insecure-s3
Credentials for an s3:// store above: a JSON file whose source is static (default — access-key-id plus a secret read from a file or environment variable), env (AWS_* variables), web_identity (STS AssumeRoleWithWebIdentity with a rotating token file, as on EKS), container (ECS / EKS Pod Identity) or imds (EC2 instance profile) — temporary credentials are refreshed before they expire; or the ANIMUS_S3_ACCESS_KEY_ID/ANIMUS_S3_SECRET_ACCESS_KEY environment variables for static keys. --allow-insecure-s3 is the explicit opt-in a plaintext, non-loopback S3 endpoint needs. Add &path_style=false to the store URI for virtual-hosted addressing. Objects over 64 MiB are uploaded as S3 multipart uploads (16 MiB parts); add a bucket lifecycle rule AbortIncompleteMultipartUpload so an upload interrupted by a crash does not linger.

Run animusd with no arguments for the full usage summary, and animus for the CLI's.

Data directory

Each node writes everything it owns under --dir: a write-ahead log and SSTables per hosted tablet, control-plane state, and snapshots. Nodes never share a directory — give each process its own, on storage you would be happy to lose a machine's worth of.

Upgrading

Two supported paths, both tested in simulation: stop every node, install the new version, and start them all again; or a node-by-node rolling upgrade (one release to the next only), where animus cluster roll plan gives the order, you restart one node at a time, animus cluster roll wait blocks until it is healthy, and the irreversible animus cluster finalize comes last. The CLI restarts nothing itself. On an operator-managed cluster of at least three nodes, editing spec.image performs the same roll for you, one pod at a time behind the health gate, and finalize stays yours unless you set spec.upgrade.finalize: Auto. Rolling from an older release with multi-item transactions in use carries that release's own abort bug (open). There is no rollback once a node has run the new binary. Procedure: docs/runbook/upgrade.md.

Troubleshooting

The build fails on an old toolchain

The workspace pins Rust 1.96. Run rustup update and build from the repository root so the pin applies.

A write returns "not leader"

Nodes forward to the tablet's leader and retry through an election, so this should be transient. If it persists, check animus admin health — a tablet with no reachable quorum cannot accept writes, by design.

Address already in use

Every node needs its own block of six ports. In a generated config the blocks are contiguous, so two nodes on one host must be given different base ports.

The AWS SDK rejects the endpoint

Set a region and placeholder credentials — the SDKs require them even though AnimusDB doesn't check them unless SigV4 verification is switched on — and use http://, not https://, unless you've explicitly configured TLS on the DynamoDB port (off by default — see the architecture page's ports section).

Something is wrong and it looks like a bug

Please open an issue. Include the animusd invocation and, if a test failed, the seed it printed — that alone is enough to replay the exact failure.

Next: how it actually works.

Tablets, the two planes, the consistency model, and the full API surface.