AI Initiative logoAI Initiative
Reading room / Manthan

Manthan compose stack — self-hosted observability

Manthan compose stack — self-hosted observability. Covers Why this exists; What's in the stack; First run; from the repo root.

Deployment / operations method; execution not repeated
This is the retained source text, not a new validation of its claims. Numerical results and comparative claims require the accompanying source review before public reuse.

Read the connected case study or article →

Not explicitly stated in source · Date not established

Provenance, authorship and editorial notes

Manthan compose stack — self-hosted observability

Id

manthan-infra-compose-readme

Project

Manthan

Source path

manthan/infra/compose/README.md

Source sha256

089c2413f45e46932f7824e3a9753fe28d5fd7bc1c100ce1921d921362b740fd

Local copy sha256

089c2413f45e46932f7824e3a9753fe28d5fd7bc1c100ce1921d921362b740fd

Repo head

3a94084024a3434cef331fecac91c2c3b43f0313

Date
Not recorded
Date basis

Original publication date not established; Git revision date retained separately.

Author

Not explicitly stated in source

Author basis

Unknown; Git committer not substituted for document author.

Last source commit

8b75d1b0157c20a0a463f0e31008ef9f73099beb

Last source commit date

2026-06-08T07:40:14Z

Document status

Deployment / operations method; execution not repeated

Summary

Manthan compose stack — self-hosted observability. Covers Why this exists; What's in the stack; First run; from the repo root.

Topics
  • Operating the system
  • Why this exists
  • What's in the stack
  • First run
Theme

Operating the system

Priority

reference

Related ids

No entries recorded.

Evidence concerns
  • Recovered source text is preserved verbatim. Assertions, comparisons, commands and results belong to its original context; they were not re-executed or externally re-verified during this corpus review.
Word count

1848

Bytes

13247

Source relative

infra/compose/README.md

Remote mirror

manthan/infra/compose/README.md

Full text preserved

True

Manthan compose stack — self-hosted observability

Open-source alternative to the AWS Fargate + Amazon Managed Prometheus stack
in ../terraform/. Same image, same alerts.yaml, same PagerDuty/Teams/Slack
receivers — just running on docker-compose instead of Fargate, with vanilla
Prometheus + Alertmanager instead of AMP.

Why this exists

The Terraform stack is the production roadmap target: managed services,
HA, auto-scale, paid SLAs. For an internal/research stage where the budget
is "don't spend much", that's overkill. This compose stack runs the entire
testnet on a single Linux host (the existing genesis EC2 is the obvious
target) for ~$0/mo on top of whatever you're already paying for that VM.

What's in the stack

ServiceImagePortRole
alpha beta gammamanthan-testnet-node:0.2.09120-9122 / 9220-9222The 3 genesis nodes; gossip ring formed via MANTHAN_PEERS env
prometheusprom/prometheus:v2.55.119090Scrapes each node's /metrics every 15s, evaluates alerts.yaml
alertmanagerprom/alertmanager:v0.27.09093Routes SEV-1/2 → PagerDuty, SEV-2/3 → Teams + Slack
grafanagrafana/grafana-oss:11.3.013000Pre-loaded with a Prometheus datasource and a Manthan overview dashboard
backupmanthan-backup:0.2.0 (built from ./backup)—restic backups on cron; SQLite online-backup for ledger consistency. See Backup & restore below.
Prometheus is on 19090 (not 9090) and Grafana on 13000 (not 3000) so they
don't collide with anything else already running on the host. Edit the
compose file's ports: if you want different host ports.

First run

# from the repo root
docker build -f infra/docker/Dockerfile -t manthan-testnet-node:0.2.0 .

cp infra/compose/.env.example infra/compose/.env
$EDITOR infra/compose/.env       # paste real PagerDuty / Teams / Slack URLs

docker compose -f infra/compose/docker-compose.yml up -d

Verify

# Each node responds to /healthz
curl -s local-service/healthz | jq
curl -s local-service/healthz | jq
curl -s local-service/healthz | jq

# Each node /canary is signed
curl -s local-service/canary | jq

# Prometheus has discovered all 3 targets
curl -s 'local-service/api/v1/targets' | jq '.data.activeTargets[] | {job, health, instance}'

# Alertmanager has the routing tree
open local-service

# Grafana dashboard ('Manthan — Overview' under General)
open local-service     # admin / admin first login

Receiver test

To prove an alert actually reaches Teams and Slack, kill one node:

docker stop manthan-alpha
# Wait 5 minutes — the ManthanLivenessLost / ManthanPeerCollapse rules fire
# (SEV-1, hits PagerDuty too if PD is enabled).

The same alerts fire on the AWS path; only the transport layer changes.

Cost comparison (3-node testnet, low traffic)

ComponentAWS path (infra/terraform/)Compose path (this dir)
ComputeFargate × 4 tasks ≈ $40-60/mo$0 (uses existing VM)
PrometheusAMP $5-15/mo for 3 targets$0 (vanilla Prometheus)
Alertmanagerbundled with AMP$0 (vanilla Alertmanager)
Load balancerALB ~$20/mo$0 (host-port binding)
SecretsSecrets Manager ~$5/mo for ~10 secrets$0 (.env file)
StorageEFS (small)local volumes (free)
KMS$1/CMK/mo$0 (no encryption-at-rest layer)
TLSACM (free for public certs) + Route 53self-signed or out-of-band reverse proxy
Total~$80-120/mo~$0/mo (host VM only)

When to switch back to the AWS path

The Terraform stack is preserved untouched at ../terraform/ for that
transition. Both paths consume the same manthan-testnet-node:0.2.0 image
and the same alerts.yaml, so moving between them is roughly:

1. Build and push the image to ECR (already documented in ../terraform/README.md).
2. terraform apply — stack comes up.
3. aws secretsmanager update-secret for each receiver URL.
4. Repoint your DNS at the new ALB DNS name.

Backup & restore (D-005-OSS)

The backup sidecar runs restic on a cron schedule (default every 6h) and
captures the full persistent-state inventory:

WhatSourceMethod
Per-node SQLite ledgersalpha-data beta-data gamma-data volumesSQLite .backup API → consistent snapshot, no writer pause
Alertmanager state (silences, nflog)alertmanager-data volumefile copy
Grafana state (user-edited dashboards, datasource creds)grafana-data volumefile copy
Charters / LOTS / privacy-policyrepo treefile copy
infra/compose/.env (operator secrets)repo treefile copy

Snapshots are encrypted with RESTIC_PASSWORD before they leave the host.
Lose that password → backups are unreadable. Store it where you keep other
recovery secrets.

Configure

In infra/compose/.env:

# Local repo — fine for dev/test, but NOT off-host (host loss = backups lost):
RESTIC_REPOSITORY=/backups/manthan

# OR off-host examples (pick one and set the matching creds):
# Demo-server SFTP — second EC2 host in the same AWS account/region. Not
# truly geo-redundant but materially better than single-host. Already
# staged on this genesis host; see "Demo-server SFTP backup" below.
# RESTIC_REPOSITORY=sftp:ec2-user@<backup-host-private-ip>:manthan-backups/repo
# RESTIC_REPOSITORY=s3:s3.amazonaws.com/<bucket>/manthan        # + AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
# RESTIC_REPOSITORY=s3:<r2-endpoint>/<bucket>/manthan           # Cloudflare R2 via S3 API
# RESTIC_REPOSITORY=b2:<bucket>:manthan                         # + B2_ACCOUNT_ID / B2_ACCOUNT_KEY

RESTIC_PASSWORD=<long random string — change from the placeholder>

BACKUP_SCHEDULE=0 */6 * * *      # default every 6 hours
BACKUP_RETENTION_HOURLY=24
BACKUP_RETENTION_DAILY=30
BACKUP_RETENTION_WEEKLY=12

Demo-server SFTP backup (the operator's poor-man off-host)

If you have a second EC2 host you can SSH to (the operator's existing
demo-server is the obvious one), restic can use it as an SFTP repo. This
gives two-server resilience — genesis loss no longer loses backups —
without any cloud-storage spend. Caveat: same AWS account / same region,
so partial failure correlation remains. Don't treat it as DR-grade.

What's already wired on this host:

To enable, in infra/compose/.env:

RESTIC_REPOSITORY=sftp:ec2-user@<backup-host-private-ip>:manthan-backups/repo
RESTIC_PASSWORD=<a long random string — change from the placeholder>

The remote directory was created at first init (~/manthan-backups/repo on demo-server with mode 700). Verify with:

ssh demo-server 'ls ~/manthan-backups/repo'      # should show config / data / index / keys / locks / snapshots

For a fresh clone on a different host: drop your private key at
infra/compose/backup/ssh-key.pem (mode 600), then
ssh-keyscan -H <demo-server-ip> > infra/compose/backup/known_hosts,
and you're set.

Verify backups exist

docker exec manthan-backup restic snapshots --tag manthan

Trigger a one-shot backup (for drills / between cron runs)

docker exec manthan-backup /opt/backup/backup.sh

Restore drill

A full restore drill validates that backups are usable, not just present.
Procedure:

# 1. Note the latest snapshot id
docker exec manthan-backup restic snapshots --tag manthan --latest 1

# 2. Pull a snapshot into the restore-out volume
docker run --rm \
  -v $(pwd)/.env:/opt/.env:ro \
  --env-file ./.env \
  -v manthan-compose_backup-local-repo:/backups \
  -v manthan-compose_backup-cache:/restic-cache \
  -v manthan-compose_restore-out:/restore-target \
  -e BACKUP_MODE=restore \
  manthan-backup:0.2.0 latest

# 3. Tear down the running stack
docker compose -f infra/compose/docker-compose.yml down

# 4. For each node, copy the restored ledger into a fresh volume
for n in alpha beta gamma; do
  docker volume rm manthan-compose_${n}-data 2>/dev/null || true
  docker volume create manthan-compose_${n}-data
  docker run --rm \
    -v manthan-compose_${n}-data:/data \
    -v manthan-compose_restore-out:/restore-out \
    alpine sh -c "cp /restore-out/tmp/backup-staging/ledgers/${n}-ledger.sqlite /data/ledger.sqlite"
done

# 5. Bring the stack back up
docker compose -f infra/compose/docker-compose.yml up -d

# 6. Verify the ledger has the pre-restore envelope counts
for p in 9120 9121 9122; do
  curl -s local-service:$p/sth | jq '.tree_size'
done

Run this drill at least monthly. Document the outcome in
herald-protocol/services/testnet/incident-response/drill-log.md (per D-007).

What's deferred / out of scope

publicly-reachable deploy, put it behind Caddy / Traefik / nginx with
Let's Encrypt; that's a one-page reverse-proxy config, not a code change.

required, it's docker compose down && rsync volumes && up on the new host.

from node names; for production, set MANTHAN_NODE_SEED_HEX in .env,
which IS captured by the backup sidecar's .env snapshot.

from scrape data after the next scrape cycle, and including it would balloon
snapshot size for no recovery benefit.

Stop

# Keep volumes (ledgers, TSDB, dashboards):
docker compose -f infra/compose/docker-compose.yml down

# Or full reset:
docker compose -f infra/compose/docker-compose.yml down -v

Production-readiness checklist

The compose stack is the default for internal use. If it accumulates
real users / live agents over time and starts inheriting production
responsibilities (which the planning doc warns is the silent-drift risk),
walk this checklist before treating it as production:

Let's Encrypt in front of every public host:port.

it's a cloud bucket the host cannot also lose. Restore drill ran
successfully within the last 30 days.

.env on the host.

(the backup sidecar does this if RESTIC_REPOSITORY is off-host;
verify it's covered).

rather than implicitly deterministic — so a re-deploy on a new host
keeps node identity.

placeholders. Test alerts have been observed reaching all three.

search-and-replace done (per
herald-protocol/services/testnet/nodes/README.md) and reflect the
actual jurisdiction the operator is in.

is sized for your investigation window; default 30d may be insufficient.

(D-007 procedure peer-collapse).

onto a fresh VM, verified end-to-end.

runaway agent can't OOM the host.

secrets — secrets only in .env, which is gitignored.

still on deterministic / env-supplied seeds, the testnet
"internal use" framing is honest. For production, operator seeds
should move to age/sops-encrypted secret or a real HSM.

When a majority of these aren't ticked, the system is "running in
production" but not "production-ready." That's a real risk; the AWS-managed
path (infra/terraform/) ticks most of them by construction, which is what
the production roadmap is for.

Continue exploring