Taguru
deploy · docker compose

Deploy with Docker Compose

Taguru is deliberately a single-node, single-writer system, and deploy/docker-compose.yml is that model on a single host: one container, one named volume, a loopback port, a graceful stop. This page walks the file and the operations around it.

Image: ghcr.io/t0k0sh1/taguru (scratch, ~13 MB) Manifest: deploy/docker-compose.yml

Ground rules — one host, one writer

One fsync-then-apply owner per data directory is what makes "200 = durable" simple enough to trust. Everything below follows from it.

  • Never run two containers against one volume. The advisory lock refuses a second writer on local disks; don't rely on being refused — just don't wire it up.
  • Deploys are stop-then-start. docker compose up -d after an image change recreates the container: stop, then start. The downtime is the boot — cold registration is cheap however many contexts exist, and pinned contexts preload in parallel.
  • Availability is promotion time with a replica; restore time without one. With TAGURU_REPLICATE_URL set the data directory ships continuously to object storage (RPO ≈ seconds of lag; recover with taguru restore, or start a server on an empty volume with the same URL and it boots straight from the bucket). Add TAGURU_REPLICA=1 services tailing that bucket — the compose file carries a commented example — and reads scale across them while each doubles as a warm standby: losing the writer then costs the manual promotion walk (watch taguru_replica_behind_seconds drain to 0 on /metrics, start a writer against the bucket with the stated --take-over / TAGURU_TAKEOVER=1, repoint clients) plus the dead writer's un-shipped tail — the RPO those metrics had on display. There is still exactly one writer and no automatic promotion, by design; the runbook is in the architecture page. Rehearse it.
  • Scale writes by sharding behind taguru router. Give independent writer services disjoint sets of contexts and add one stateless router service (the compose file carries a commented example): its map file says which service owns which context, clients get one URL, and groups and cross-context search span every shard with the single-instance merge semantics. The router keeps no volume and no keys — the shards enforce auth (give them identical keyrings). Moving a context, in order: quiesce its writes → taguru export → DELETE it through the router (the old shard drops it, group projections included) → edit the map (the router picks it up by itself within ~5s, or immediately on SIGHUP; a broken edit is refused and the old map keeps serving) → re-import through the router, which now routes it to the new shard.
  • A rollback is a restore. Image formats migrate forward on load and never write the old version back out, so rolling the binary back past a format bump needs the volume rolled back with it (snapshot) or re-imported from an export stream. Check the release notes before downgrading.

The same model on a cluster — with the platform enforcing the single writer — is the Kubernetes page.

The image

The published image is the server binary alone on scratch — no shell, no libc, nothing to patch.

  • Inside a container the server binds 0.0.0.0:8248 by default, so the port reaches as far as you publish it — keep the publish on 127.0.0.1, and set a token.
  • The process runs as uid 65532. A named volume inherits ownership automatically on first use; a bind mount needs chown -R 65532 first (or --user "$(id -u)").
  • It runs with a read-only root filesystem — /data is the only write target. The compose file turns that on.
  • The HEALTHCHECK is taguru health — the binary probes itself; there is no curl on scratch.
  • docker stop is a graceful shutdown (flush + usage-stats sweep).
  • Pin the image version: latest moves, and a surprise binary bump on a restart is exactly what a format-sensitive data directory doesn't want. A digest pin (0.9.0@sha256:…) is stronger still — releases are signed and carry an SBOM and build provenance, and SECURITY.md has the cosign verify command whose output names the digest.

The compose file

Credentials stay out of the manifest — the file demands the token from the caller's environment.

TAGURU_API_TOKENS='ops:CHANGE-ME' docker compose up -d
services:
  taguru:
    image: ghcr.io/t0k0sh1/taguru:0.9.0 # pin; `latest` moves
    ports:
      - "127.0.0.1:8248:8248"
    environment:
      TAGURU_API_TOKENS: ${TAGURU_API_TOKENS:?set TAGURU_API_TOKENS=name:token}
      # Scope keys, budgets, and the embedding tier live here too —
      # `taguru --help` lists every knob; `--env-file` reads the same
      # KEY=VALUE file `taguru --config` does.
    volumes:
      - taguru-data:/data
    read_only: true # /data is the only write target
    restart: unless-stopped
    # `docker stop` is a graceful shutdown (flush + usage sweep); the
    # drain is bounded by the request budget (30s default) — embedding
    # provider calls abort on the stop signal.
    stop_grace_period: 60s

volumes:
  taguru-data:
  • The token is required, not defaulted: ${TAGURU_API_TOKENS:?…} fails the deploy loudly rather than starting an unauthenticated server. Named keys (name:token) mean the access log says which key was used and a leak costs one revocation.
  • The port stays on loopback. Expose it beyond this host only behind a TLS-terminating reverse proxy (a bearer token is the whole credential), and set TAGURU_RATE_LIMIT_PER_MIN when you do.
  • stop_grace_period: 60s covers the drain (bounded by TAGURU_REQUEST_TIMEOUT_SECS, 30s default — in-flight embedding calls abort on the stop signal) plus the final flush; restart: unless-stopped covers crashes without fighting an intentional stop.
  • All other configuration is ordinary environment: add TAGURU_EMBED_*, cache and rate budgets, etc. right in environment:, or via --env-file — the same KEY=VALUE file taguru --config reads, so one file can drive a local binary and this container alike. Add OTEL_EXPORTER_OTLP_ENDPOINT the same way to get a full retrieval span tree exported to a collector — see Tracing for a one-container Jaeger to point it at.

Backups and restore

On this model, availability is restore time. Two rungs: a byte-exact snapshot, and a portable export stream.

  1. POST /flush — write out everything dirty.
  2. Snapshot the volume (every writer is fsync+rename, so a filesystem snapshot is safe at any instant) — or take the portable stream with taguru export / GET /contexts/{name}/export, which restores anywhere through taguru import / POST /import, across versions and machines.
  3. Verify with taguru inspect — the same fully-validating load + WAL replay the server does; non-zero exit means acknowledged data is corrupt.
# verification needs no local toolchain — run the image against the volume
docker run --rm -v taguru-data:/data ghcr.io/t0k0sh1/taguru inspect /data

Back up each context's file family as a set, always — a naive rsync/cp against a live server does not guarantee cross-file consistency; stop the server or take a real snapshot. And rehearse the restore. Details in operational basics.