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.
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 -dafter 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_URLset the data directory ships continuously to object storage (RPO ≈ seconds of lag; recover withtaguru restore, or start a server on an empty volume with the same URL and it boots straight from the bucket). AddTAGURU_REPLICA=1services 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 (watchtaguru_replica_behind_secondsdrain 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:8248by default, so the port reaches as far as you publish it — keep the publish on127.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 65532first (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 stopis a graceful shutdown (flush + usage-stats sweep).- Pin the image version:
latestmoves, 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 thecosign verifycommand 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_MINwhen you do. stop_grace_period: 60scovers the drain (bounded byTAGURU_REQUEST_TIMEOUT_SECS, 30s default — in-flight embedding calls abort on the stop signal) plus the final flush;restart: unless-stoppedcovers crashes without fighting an intentional stop.- All other configuration is ordinary environment: add
TAGURU_EMBED_*, cache and rate budgets, etc. right inenvironment:, or via--env-file— the same KEY=VALUE filetaguru --configreads, so one file can drive a local binary and this container alike. AddOTEL_EXPORTER_OTLP_ENDPOINTthe 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.
POST /flush— write out everything dirty.- 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 throughtaguru import/POST /import, across versions and machines. - 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.
Taguru