sha256:407e5be499d253155ab56fdec072e8295869f80aba1cc610e32e02161966e903
Last pushed
2 days by tessaryai
Type
Compose
Manifest digest
sha256:407e5be499d253155ab56fdec072e8295869f80aba1cc610e32e02161966e903
# Production stack: postgres, backend, frontend (Caddy, TLS on :443 when SITE_DOMAIN is set, HTTP on
# :8000 always) and the sandbox-runner launcher. Every service starts on a default `up` except
# `redpanda`, the opt-in ingest buffer under `--profile kafka`.
#
# Usage: fill .env (.env.example lists every key), then `docker compose pull && docker compose up -d`.
# Set TESSARY_VERSION to pin every image; each *_IMAGE key overrides one.
#
# Also the published install: `docker compose -f oci://docker.io/tessaryai/tessary:compose up -d -y`.
# The release stamps the floating TESSARY_VERSION defaults with its version; no version literal lives
# here (scripts/check-version-consistency.sh).
#
# A remote install has no repository to resolve against, so (scripts/check-compose-artifact.sh):
# 1. No host-path bind in a default-profile service; use named volumes.
# 2. Long-syntax `ports:` and literal memory limits: publish decodes without interpolation.
# 3. `env_file:` is inert remotely, so read keys through ${VAR}. Never publish `--with-env`.
# 4. A profile-gated service uses image, volumes and env only.
# Pinned so the stack is identical from any directory, including git worktrees.
name: tessary
services:
postgres:
# Postgres 16 with pgvector preinstalled; same data directory format as postgres:16-alpine.
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: ${POSTGRES_DB:-tessary}
# Defaulted, not `:?`: Compose refuses to render an unset `:?` key. No host port is published, so the
# credential is reachable only from the `tessary` network; change it if you publish the port.
POSTGRES_USER: ${POSTGRES_USER:-tessary}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tessary}
volumes:
# A named volume by default (rule 1). POSTGRES_DATA_DIR still takes a host path (/ or ./), such as an
# EBS mount. `docker compose down` keeps the volume; `down -v` deletes the data.
- ${POSTGRES_DATA_DIR:-tessary-postgres-data}:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 3s
retries: 30
start_period: 10s
restart: unless-stopped
networks:
- tessary
backend:
# Unset means `-latest`, which the release repoints. Before the first release no tag exists; use
# `docker compose up -d --build`.
image: ${BACKEND_IMAGE:-tessaryai/tessary:backend-${TESSARY_VERSION:-1.11.1}}
# A literal ceiling (rule 2), so -XX:MaxRAMPercentage sizes the heap against a stated number. Raise it
# with an override file (docs/self-hosting/setup.mdx).
mem_limit: 2g
expose:
- "8080"
# OTLP/gRPC, internal only. Inert unless tessary.ingest.otlp.transport is grpc or both.
# Publishing 4317 to the host also needs `EXPOSE 4317` in backend/Dockerfile.
- "4317"
env_file:
- path: ./.env
required: false
environment:
TESSARY_JDBC_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-tessary}
# Repeated, not inherited: an unset ${POSTGRES_USER} would hand the backend an empty credential.
TESSARY_DB_USERNAME: ${POSTGRES_USER:-tessary}
TESSARY_DB_PASSWORD: ${POSTGRES_PASSWORD:-tessary}
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-production}
# Liquibase phones home to config.liquibase.com unless this key is set; Spring's
# `spring.liquibase.analytics-enabled` does not reach it.
LIQUIBASE_ANALYTICS_ENABLED: "false"
# INSECURE PUBLIC DEFAULTS so the quickstart is one command. Anyone can forge a session cookie or decrypt
# sealed provider keys on an instance still using them. PlaceholderSecretGuard refuses to start with a
# real SITE_DOMAIN; replace both with `openssl rand -base64 32`. Each must decode to exactly 32 bytes.
TESSARY_AUTH_COOKIE_PASSWORD: ${TESSARY_AUTH_COOKIE_PASSWORD:-Q0hBTkdFLU1FLWluc2VjdXJlLWRlZmF1bHQtY29va2k=}
TESSARY_SECRET_KEY: ${TESSARY_SECRET_KEY:-Q0hBTkdFLU1FLWluc2VjdXJlLWRlZmF1bHQtc2VhbCE=}
# Puts the instance on a hostname and arms the guard above. PublicOriginGuard derives the sign-in origin
# and WorkOS callback from it. TLS_MODE and ACME_EMAIL are repeated so an ACME boot without an address is refused.
SITE_DOMAIN: ${SITE_DOMAIN:-}
TLS_MODE: ${TLS_MODE:-acme}
ACME_EMAIL: ${ACME_EMAIL:-}
# Derived from the published port; the code default assumes :8000. With SITE_DOMAIN set it becomes
# https://<SITE_DOMAIN>/, and an explicit value on another host refuses the boot.
TESSARY_AUTH_FRONTEND_URL: ${TESSARY_AUTH_FRONTEND_URL:-http://localhost:${HTTP_PORT:-80}/}
# Explicit so an agentic run cannot no-op on a missing URL.
TESSARY_OBSERVER_AGENTIC_LAUNCHER_URL: http://sandbox-runner:8080
# A shipped placeholder, not blank: the launcher rejects every request when its key is empty.
# PlaceholderSecretGuard covers the exposure.
TESSARY_OBSERVER_AGENTIC_LAUNCHER_API_KEY: ${TESSARY_OBSERVER_AGENTIC_LAUNCHER_API_KEY:-Q0hBTkdFLU1FLWluc2VjdXJlLWRlZmF1bHQtbGF1bmNo}
# Required: the sandboxed agent is RCA's only analysis path.
TESSARY_RCA_AGENTIC_LAUNCHER_URL: http://sandbox-runner:8080
# Repeated verbatim: compose interpolates each entry from the host env, so a default in one entry is
# invisible to the others.
TESSARY_RCA_AGENTIC_LAUNCHER_API_KEY: ${TESSARY_OBSERVER_AGENTIC_LAUNCHER_API_KEY:-Q0hBTkdFLU1FLWluc2VjdXJlLWRlZmF1bHQtbGF1bmNo}
# The agents' MCP evidence endpoint. The internal name works while SANDBOX_NETWORK_ISOLATION is off (the
# default). With isolation on, or SANDBOX_BACKEND=e2b, set it to the public origin; PublicOriginGuard
# rejects an unreachable value.
TESSARY_RCA_AGENTIC_MCP_BASE_URL: ${TESSARY_RCA_AGENTIC_MCP_BASE_URL:-http://backend:8080}
TESSARY_CLASSIFIER_TRIAGE_MCP_BASE_URL: ${TESSARY_RCA_AGENTIC_MCP_BASE_URL:-http://backend:8080}
# The groundedness model server (classifiers/groundedness/serve.py) on a GPU outside this stack. Blank
# skips groundedness sweeps. The setup guides in classifiers/groundedness/setup/ write these to .env.
TESSARY_OBSERVER_ENCODER_URL: ${TESSARY_OBSERVER_ENCODER_URL:-}
TESSARY_OBSERVER_ENCODER_API_KEY: ${TESSARY_OBSERVER_ENCODER_API_KEY:-}
TESSARY_GROUNDEDNESS_CLASSIFIER_MODE: ${TESSARY_GROUNDEDNESS_CLASSIFIER_MODE:-}
# `memory` (default) or `kafka` (the `redpanda` service). No depends_on: with kafka set, a broker still
# starting sheds with 503 + Retry-After.
TESSARY_INGEST_SPOOL_MODE: ${TESSARY_INGEST_SPOOL_MODE:-memory}
TESSARY_INGEST_SPOOL_KAFKA_BOOTSTRAP_SERVERS: ${TESSARY_INGEST_SPOOL_KAFKA_BOOTSTRAP_SERVERS:-redpanda:9092}
# Interpolated rather than left to env_file so the registry install still reads .env (rule 3). 0 keeps forever.
TESSARY_RETENTION_ENABLED: ${TESSARY_RETENTION_ENABLED:-true}
TESSARY_RETENTION_TRACE_TTL_DAYS: ${TESSARY_RETENTION_TRACE_TTL_DAYS:-90}
TESSARY_RETENTION_DETECTION_TTL_DAYS: ${TESSARY_RETENTION_DETECTION_TTL_DAYS:-90}
# Interpolated for the same reason: via env_file alone, a registry install setting false keeps sending.
TESSARY_TELEMETRY_ENABLED: ${TESSARY_TELEMETRY_ENABLED:-true}
depends_on:
postgres:
condition: service_healthy
# The runtime image has no curl or wget, so bash's /dev/tcp makes the request. /actuator/health is the
# path AuthFilter admits without a session, and it is 200 only once the database is UP.
healthcheck:
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8080 && printf 'GET /actuator/health HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n' >&3 && grep -q '\"status\":\"UP\"' <&3"]
interval: 5s
timeout: 3s
retries: 60
start_period: 15s
restart: unless-stopped
networks:
- tessary
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
tag: "tessary/backend"
frontend:
image: ${FRONTEND_IMAGE:-tessaryai/tessary:frontend-${TESSARY_VERSION:-1.11.1}}
# Long syntax (rule 2). `published` must stay a quoted string to carry the ${VAR}.
ports:
- mode: ingress
target: 8000
published: "${HTTP_PORT:-80}"
protocol: tcp
- mode: ingress
target: 443
published: "${HTTPS_PORT:-443}"
protocol: tcp
environment:
BACKEND_URL: backend:8080
# frontend/caddy/render.sh builds the Caddyfile from these; blank SITE_DOMAIN means no TLS site.
# TLS_MODE: acme (ACME_EMAIL required), owncert (certs at /certs) or upstream (plain HTTP behind your own
# terminator, listed in TRUSTED_PROXIES).
SITE_DOMAIN: ${SITE_DOMAIN:-}
TLS_MODE: ${TLS_MODE:-acme}
ACME_EMAIL: ${ACME_EMAIL:-}
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-}
TLS_CERT_FILE: ${TLS_CERT_FILE:-/certs/tls.crt}
TLS_KEY_FILE: ${TLS_KEY_FILE:-/certs/tls.key}
volumes:
# Persist ACME state so redeploys do not hit Let's Encrypt rate limits.
- caddy-data:/data
- caddy-config:/config
# Waits on the backend serving, so a healthy frontend means the whole browser path is open.
depends_on:
backend:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "-T", "3", "http://127.0.0.1:8000/"]
interval: 5s
timeout: 3s
retries: 12
start_period: 5s
restart: unless-stopped
networks:
- tessary
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
tag: "tessary/caddy"
# Opt-in single-node Redpanda: `--profile kafka` plus TESSARY_INGEST_SPOOL_MODE=kafka. Internal only.
# The image needs `developer_mode` to start under 1 GiB, which makes rpk bypass fsync, so
# `--unsafe-bypass-fsync=false` is passed explicitly (rpk's warning is about the bypass it would add).
# Write caching and the metrics reporter are off. `--set` must be two tokens.
redpanda:
image: docker.redpanda.com/redpandadata/redpanda:v25.2.1
profiles: ["kafka"]
command:
- redpanda
- start
- --smp=1
- --memory=512M
- --reserve-memory=0M
- --overprovisioned
- --check=false
- --unsafe-bypass-fsync=false
- --set
- redpanda.write_caching_default=false
- --set
- redpanda.enable_metrics_reporter=false
- --set
- redpanda.enable_usage=false
- --kafka-addr=internal://0.0.0.0:9092
- --advertise-kafka-addr=internal://redpanda:9092
volumes:
- redpanda-data:/var/lib/redpanda/data
healthcheck:
test: ["CMD-SHELL", "rpk cluster health | grep -E 'Healthy:.+true' || exit 1"]
interval: 10s
timeout: 5s
retries: 12
start_period: 10s
restart: unless-stopped
networks:
- tessary
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# One-shot: a fresh named volume is root-owned, so this chowns it to the launcher's uid 1000.
sandbox-runner-work-init:
image: busybox:1.36
command: ["sh", "-c", "mkdir -p /launcher-work && chown 1000:1000 /launcher-work"]
volumes:
- sandbox-work:/launcher-work
restart: "no"
sandbox-runner:
depends_on:
sandbox-runner-work-init:
condition: service_completed_successfully
# Internal only, at http://sandbox-runner:8080. Deliberately no `env_file: .env`, so it receives only
# its own minimal secrets. Not profile-gated: RCA reaches it on every run; mem_limit is the guard.
image: ${SANDBOX_RUNNER_IMAGE:-tessaryai/tessary:sandbox-runner-${TESSARY_VERSION:-1.11.1}}
expose:
- "8080"
# The launcher's unauthenticated GET /healthz; a launcher that rejects its config shows as unhealthy.
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "-T", "3", "http://127.0.0.1:8080/healthz"]
interval: 5s
timeout: 3s
retries: 12
start_period: 5s
# Grants the docker.sock group rather than running as root. 0 fits Docker Desktop (root:root 0660). On
# Linux set DOCKER_SOCK_GID to the group as a container sees it: `docker run --rm -v
# /var/run/docker.sock:/var/run/docker.sock busybox:1.36 stat -c %g /var/run/docker.sock`.
group_add:
- "${DOCKER_SOCK_GID:-0}"
volumes:
# SECURITY: read-write docker.sock is host-root-adjacent for the launcher process. Inherent to spawning
# sibling agent containers; mitigated by running non-root with no other host mount. Unconditional so
# switching SANDBOX_BACKEND is one env var.
- /var/run/docker.sock:/var/run/docker.sock
# A named volume (rule 1): the daemon resolves a volume name the same from a container and the host.
# Each sibling mounts its per-run subdirectory via VolumeOptions.Subpath.
- sandbox-work:/launcher-work
environment:
PORT: "8080"
# Same placeholder as the backend's: an empty key makes the launcher reject every request.
SANDBOX_API_KEY: ${TESSARY_OBSERVER_AGENTIC_LAUNCHER_API_KEY:-Q0hBTkdFLU1FLWluc2VjdXJlLWRlZmF1bHQtbGF1bmNo}
# Blank falls through to the code default, `docker`.
SANDBOX_BACKEND: ${SANDBOX_BACKEND:-}
# Cuts agent containers off this network. Turning it on requires SITE_DOMAIN and pointing
# TESSARY_RCA_AGENTIC_MCP_BASE_URL at the public origin.
SANDBOX_NETWORK_ISOLATION: ${SANDBOX_NETWORK_ISOLATION:-}
# Pulled lazily by the launcher, not by `docker compose pull` or `--build`. Before the first release no
# tag exists; set AGENT_IMAGE to a local image.
AGENT_IMAGE: ${AGENT_IMAGE:-tessaryai/tessary:agent-sandbox-${TESSARY_VERSION:-1.11.1}}
SANDBOX_DOCKER_CONCURRENCY: "1"
# Must equal the `sandbox-work` volume's `name:`; the docker backend fails at boot if unset.
SANDBOX_WORK_VOLUME: ${SANDBOX_WORK_VOLUME:-tessary-sandbox-work}
E2B_API_KEY: ${E2B_API_KEY:-}
# Namespaced so any E2B key can reach the public template; unset floats to `:latest`.
E2B_ANALYZER_TEMPLATE: ${E2B_ANALYZER_TEMPLATE:-tessary/tessary-agent-sandbox:${TESSARY_VERSION:-1.11.1}}
# The launcher reads no provider secret from its env; each run carries the org's own credential.
# MANTLE_PROJECT_ID is an attribution scope, not a secret.
MANTLE_PROJECT_ID: ${TESSARY_MANTLE_PROJECT_ID:-}
# Below host RAM so an OOM kills this container alone. Separate from SANDBOX_DOCKER_MEMORY_MB, which caps
# each agent container.
mem_limit: 1536m
restart: unless-stopped
networks:
- tessary
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
tag: "tessary/sandbox-runner"
networks:
tessary:
driver: bridge
volumes:
redpanda-data:
caddy-data:
caddy-config:
# No explicit `name:`, so it is project-prefixed and a clean-room boot cannot touch a developer's database.
tessary-postgres-data:
# Explicit `name:` is load-bearing: the launcher hands this exact string to the Engine API. It is not
# isolated by COMPOSE_PROJECT_NAME, so a clean-room boot must set a run-scoped SANDBOX_WORK_VOLUME.
sandbox-work:
name: ${SANDBOX_WORK_VOLUME:-tessary-sandbox-work}
docker compose -f oci://tessaryai/tessary:compose upUse the above command to pull and run the Compose file. Learn more.
Open-source vector similarity search for Postgres
Pulls
100M+
Stars
112
Last Updated
16 days
Redpanda is the real-time engine for modern apps.
Pulls
10M+
Stars
32
Last Updated
about 23 hours