Documentation

Self-hosting

Run Katagami with PostgreSQL and storage that you control.

Choose your storage

Production Compose pulls the exact Katagami release image and starts only PostgreSQL beside it. It does not run MinIO. Point Katagami at an S3-compatible bucket that you operate.

cp .env.prod.example .env
chmod 600 .env
docker compose --env-file .env -f docker-compose.prod.yml up -d

KATAGAMI_IMAGE selects the release and is required; .env.prod.example pins it to the exact immutable tag ghcr.io/bro3886/katagami:0.0.2. The production file fails docker compose config if KATAGAMI_IMAGE is unset, so a deployment can never silently fall back to a moving or wrong tag. Upgrade by changing that value to a newer exact tag, pulling, and recreating the renderer; committed examples never consume a moving tag.

The example uses an external MinIO service:

S3_ENDPOINT=https://minio.example.internal
S3_FORCE_PATH_STYLE=true

Use S3_FORCE_PATH_STYLE=false for AWS S3. The storage identity needs HeadBucket, CreateBucket, GetObject, and PutObject permissions. Katagami uses conditional object creation, so the provider must support If-None-Match: *.

Protect the service

The container binds the renderer to 127.0.0.1. Put a TLS reverse proxy and your own access policy in front of it when remote clients need access. Keep PostgreSQL private. The dedicated metrics listener defaults to 0.0.0.0:9090 in the container and has no host port mapping; allow an internal monitoring network to scrape it, never a public proxy or ingress. Treat .env as a secret.

Metrics do not require a collector. If you export traces, set OTEL_EXPORTER_OTLP_ENDPOINT to an internal OTLP HTTP/protobuf base URL. The renderer appends /v1/traces. The collector/operator owns sampling, storage, and retention. A disabled or unreachable exporter does not affect renders, liveness, or readiness.

For a collector that uses a private CA, mount a trusted PEM CA bundle into the renderer and set SSL_CERT_FILE to its container path. Keep certificate verification enabled.

Upgrade safely

  1. Back up PostgreSQL and the object-storage bucket.
  2. Point KATAGAMI_IMAGE at the new exact tag, then pull and start one updated renderer.
  3. Call GET /v1/templates and render a known template version.
  4. Start more renderer replicas only after the first renderer responds successfully.

Schema migrations move forward only. A manifest and the objects it names are one durable unit, so restore both PostgreSQL and object storage together.

Capacity and lifecycle policy

Set these positive-integer environment variables for each renderer deployment:

Variable Default Purpose
REGISTRY_MAX_VERSIONS 128 Per-renderer LRU entry cap
REGISTRY_MAX_BYTES 67108864 Per-renderer LRU byte cap
MAX_ACTIVE_VERSIONS 10000 Deployment-wide active-version cap
DEFAULT_TEMPLATE_ACTIVE_VERSIONS 100 Default cap for a template
MAX_STORAGE_BYTES 1073741824 Catalog-accounted retained-version byte cap
METRICS_ADDR 0.0.0.0:9090 Dedicated Prometheus listener; private only when you do not publish or route it publicly
OTEL_EXPORTER_OTLP_ENDPOINT unset Optional operator-managed OTLP HTTP/protobuf base URL
WORKER_POOL_SIZE 2 Target supervised Typst child workers
RENDER_DEADLINE_MS 10000 Per-worker render deadline
WORKER_MEMORY_BYTES 536870912 Per-worker address-space limit on Unix except macOS; unsupported native hosts fail startup
WORKER_MAX_REQUEST_BYTES 16777216 Maximum worker IPC request
WORKER_MAX_RESPONSE_BYTES 33554432 Maximum worker IPC response
WORKER_RESTART_BUDGET 5 Deaths allowed in the restart window
WORKER_RESTART_WINDOW_MS 60000 Restart-budget window
WORKER_RESTART_BACKOFF_MS 250 Replacement-spawn delay
WORKER_RECOVERY_COOLDOWN_MS 30000 Degraded-pool recovery delay
WORKER_LIVENESS_INTERVAL_MS 5000 Liveness-probe interval
WORKER_LIVENESS_TIMEOUT_MS 1000 Liveness deadline, no greater than interval

WORKER_BINARY defaults to the sibling katagami-worker binary. Production Compose sets it explicitly and applies RENDERER_CPUS (default 2.0) and RENDERER_MEMORY_LIMIT (default 2g) to the server and its entire worker pool together.

Native startup fails closed on macOS and non-Unix hosts because WORKER_MEMORY_BYTES cannot be enforced. Linux containers enforce that per-worker limit in addition to RENDERER_MEMORY_LIMIT for the complete container.

Startup does not preload the catalog. A cold render loads and SHA-256-verifies its stored pack through PostgreSQL and S3. Cache eviction never blocks publication.

Per-template limits are SQL-resident operator policy, never publish input:

UPDATE templates SET active_version_limit = 3 WHERE id = 'invoice';
UPDATE templates SET active_version_limit = NULL WHERE id = 'invoice';

Retirement frees an active slot while preserving objects. Unretirement verifies the stored pack and rechecks active capacity before it becomes renderable.

Retirement does not free MAX_STORAGE_BYTES. A legacy object_bytes = NULL row is repaired by one cold SHA-verified render that records exact bytes; startup does not reload the catalog. New publications require exact bytes. Until all retained rows are accounted, publication fails closed with 503 catalog retained-version byte capacity exceeded or incomplete.