Skip to main content

Run Compose yourself

Assemble Tale's images, stores, networks, secrets, and health probes when your team owns the orchestration.

8 min read

Use this reference when your team maintains the deployment files and rollout process. The CLI quickstart is the shorter path when you want Tale to generate those files and coordinate upgrades. There is no official Helm chart. The sandbox spawner supports Docker and Kubernetes backends; choose the matching runtime and network configuration.

This is a deployment contract with an application-tier example, not a complete Compose file ready to start. Assemble and validate the storage, proxy, and sandbox services before using that example.

Choose the service layout

GroupServicesLifecycle
Replicable applicationplatform, backend-api, backend-workerSame image and release. Keep shared wire contracts compatible during replacement.
Persistent stores and edgedb, object-store, proxyPreserve volumes, credentials, certificates, and stable network names when replacing containers.
Shared execution servicessandbox, sandbox-egress, sandbox-llm-gatewayCoordinate active sessions before replacement; the spawner needs the Docker daemon and matching workspace paths.
Optional video supportbgutil-providerBest-effort token provider; its failure can affect video retrieval.

The packaged layout keeps tale_app and tale_knowledge in one Postgres service with a knowledge-db alias. Separate database services or managed databases are also possible; configure their connections and backup coverage explicitly. Recreating a stateful container does not inherently destroy its data, but removing or replacing its volume can.

Pin compatible images

Set VERSION in Compose's .env to the Tale release you have reviewed and tested. Export that same value in your shell when running the separate runtime-image pull below. Keep Tale images on one release; the two upstream services have their own pinned versions.

ServiceImage
platform, backend-api, backend-workerghcr.io/tale-project/tale/tale-platform:<version>
proxyghcr.io/tale-project/tale/tale-proxy:<version>
dbghcr.io/tale-project/tale/tale-db:<version>
sandboxghcr.io/tale-project/tale/tale-sandbox:<version>
sandbox-egressghcr.io/tale-project/tale/tale-sandbox-egress:<version>
sandbox-llm-gatewayghcr.io/tale-project/tale/tale-sandbox-llm-gateway:<version>
object-storeghcr.io/tale-project/ops/minio:RELEASE.2025-04-22T22-12-26Z
bgutil-providerbrainicism/bgutil-ytdlp-pot-provider:1.3.1

Session containers use the additional image ghcr.io/tale-project/tale/tale-sandbox-runtime:<version>. Set SANDBOX_RUNTIME_IMAGE on the spawner and pull it before startup. Its default local development tag is not sufficient on a host that never built it. If you enable Docker-in-container or shared build caching, also provision the compatible runtime and cache images described in the environment reference.

Prepare secrets and public addresses

Generate unique values before the first start, and persist them in your secret-management system. Do not copy the sample credentials from a development environment into a production stack.

ValueRequirement
BETTER_AUTH_SECRETStable, high-entropy authentication secret.
ENCRYPTION_SECRET_HEXA 32-byte hex value, for example generated with openssl rand -hex 32; preserve it for existing encrypted database values.
DB_PASSWORD or external database credentialsMatch the database role the backend actually uses.
SANDBOX_TOKENThe same high-entropy token in backend and spawner.
SANDBOX_LLM_GATEWAY_ADMIN_PASSWORDStable gateway management credential shared with the backend; the username defaults to admin.
OBJECT_STORE_ACCESS_KEY, OBJECT_STORE_SECRET_KEYCredentials valid for the chosen store. Map them to MINIO_ROOT_USER and MINIO_ROOT_PASSWORD on bundled MinIO.
OBJECT_STORE_PUBLIC_ENDPOINTThe browser-reachable endpoint, usually SITE_URL when Tale's proxy forwards the bundled store.
SOPS age identityRequired to decrypt the configuration sidecars you have encrypted; see Secrets with SOPS.

The backend reconciles an environment-managed default object-store connection at startup. A managedBy: operator file is deliberately left alone. Changing storage credentials does not move or inherently orphan blobs, but the store and backend must agree before reads and writes work. The gateway retains its established password hash; restore the matching secret or follow its credential-rotation procedure rather than deleting its volume as routine troubleshooting.

Set HOST, SITE_URL, and TLS_MODE for the public path. TLS and domains covers certificates, additional origins, and subpaths.

Assemble the application tier

The three roles share an image. TALE_ROLE=api and TALE_ROLE=worker select backend roles; keep that variable unset for the web service. Do not put a fixed container_name on roles you intend to scale.

yaml
# Application-tier fragment; add the stores, proxy, and sandbox services.
# No container_name on replicated services.
services:
  platform:
    image: ghcr.io/tale-project/tale/tale-platform:${VERSION}
    env_file: [.env]
    volumes: ['config-data:/app/data:ro']
    restart: unless-stopped
    stop_grace_period: 45s
    healthcheck:
      test:
        [
          'CMD-SHELL',
          'curl -sf http://localhost:3000/api/health && [ -f /tmp/platform-ready ]',
        ]
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 180s
    networks:
      internal:
        aliases: [platform]
  backend-api:
    image: ghcr.io/tale-project/tale/tale-platform:${VERSION}
    environment:
      TALE_ROLE: api
      PORT: '3005'
      TALE_CONFIG_DIR: /app/data
      DATABASE_URL: ${DATABASE_URL:-postgresql://tale:${DB_PASSWORD:?required}@db:5432/tale_app}
      SANDBOX_URL: http://sandbox:8003
      SANDBOX_HTTP_API_BASE_URL: http://backend-api:3005
      OBJECT_STORE_ENDPOINT: ${OBJECT_STORE_ENDPOINT-http://object-store:9000}
    env_file: [.env]
    volumes: ['config-data:/app/data']
    cap_add: [NET_ADMIN]
    restart: unless-stopped
    healthcheck:
      test: ['CMD-SHELL', 'curl -sf http://localhost:3005/ping']
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 30s
    networks:
      internal:
        aliases: [backend-api]
      sandbox:
        aliases: [backend-api]
  backend-worker:
    image: ghcr.io/tale-project/tale/tale-platform:${VERSION}
    environment:
      TALE_ROLE: worker
      TALE_CONFIG_DIR: /app/data
      DATABASE_URL: ${DATABASE_URL:-postgresql://tale:${DB_PASSWORD:?required}@db:5432/tale_app}
      SANDBOX_URL: http://sandbox:8003
      SANDBOX_HTTP_API_BASE_URL: http://backend-api:3005
      OBJECT_STORE_ENDPOINT: ${OBJECT_STORE_ENDPOINT-http://object-store:9000}
    env_file: [.env]
    volumes: ['config-data:/app/data']
    cap_add: [NET_ADMIN]
    restart: unless-stopped
    healthcheck: { disable: true }
    networks: [internal]
volumes:
  config-data:
networks:
  internal:
  sandbox:
    name: tale-sandbox-net
    internal: true
    enable_ipv6: false

Explicit environment entries override env_file. The fragment therefore preserves external database and object-store overrides instead of silently forcing bundled addresses. Add health-based dependencies in a single Compose project, or enforce startup ordering in your orchestrator when services live in separate projects.

Preserve network names and isolation

AddressTarget and network requirement
platformWeb replicas on the internal application network. They reach the backend at TALE_BACKEND_URL, http://backend-api:3005 unless you set it.
backend-apiAPI replicas on the application and sandbox networks, reachable on port 3005.
knowledge-dbThe knowledge Postgres target, or use KNOWLEDGE_DATABASE_URL.
object-storeBundled MinIO on the application network.
sandboxSpawner reachable by the backend on port 8003.
sandbox-egressEgress proxy reachable by sandbox sessions on port 3128.
sandbox-llm-gateway / llm-gatewayGateway reachable from backend and sandbox sessions on port 8080.
bgutil-providerToken sidecar reachable by the worker on port 4416.

The sandbox network must be isolated from direct outbound access. The generated stack names it tale-sandbox-net; if you choose another name, keep SANDBOX_EGRESS_NETWORK and the actual network consistent. Egress must still go through sandbox-egress.

Configure BACKEND_UPSTREAM=backend-api:3005 on the proxy. For bundled file storage, set OBJECT_STORE_UPSTREAM=object-store:9000 and use the same OBJECT_STORE_BUCKET on proxy and backend. Publish the public proxy's ports 80/443; keep databases, store administration, gateway, and sandbox APIs private. Preserve trusted client forwarding if another proxy sits upstream.

Mount persistent state and required capabilities

ResourceRequired mounts or settings
Organization configurationconfig-data:/app/data read-write on backend roles, read-only on platform; read-only at /app/platform-config on the spawner.
Application and bundled knowledge datadb-data:/var/lib/postgresql/data; separate knowledge service needs its own persistent volume.
Bundled object storageobject-store-data:/data and MinIO command: server /data.
Proxy certificates and statecaddy-data:/data, caddy-config:/config.
Gateway statellm-gateway-data:/app/data.
Spawner/var/run/docker.sock and /var/lib/tale-sandbox mounted at the same host/container paths. Docker-socket access gives control over the host daemon.
Backend rolescap_add: [NET_ADMIN] for the shipped entrypoint's network fence.
Egress serviceThe shipped restricted capability set after dropping all others: NET_ADMIN, DAC_OVERRIDE, CHOWN, SETUID, SETGID, NET_BIND_SERVICE and KILL. Without KILL the root supervisor cannot signal tinyproxy after it has dropped to nobody, so a stop waits out the grace period and ends in exit 137 instead of draining.
Egress IPv6sysctls with net.ipv6.conf.all.disable_ipv6: '1' and net.ipv6.conf.default.disable_ipv6: '1', as in the shipped stack. The egress firewall fails closed: it needs working IPv6 firewall support or IPv6 disabled for the default and every interface, and a container cannot write those sysctls itself through a read-only /proc/sys. Without them the proxy refuses to start on a kernel without the ip6_tables module; see Sandbox infrastructure.
Postgres shutdownstop_signal: SIGINT, stop_grace_period: 60s, shm_size: 256mb in the reference stack.
Web and spawner shutdownAllow the web tier's 45-second and spawner's 30-second stop grace; coordinate active work before stopping.

Keep db-backup if your database tooling writes to /var/lib/postgresql/backup; mounting it alone does not create a backup schedule. Older convex-data configuration volumes need a deliberate transfer into config-data, not deletion. Preserve the old copy until verified.

Use the right health probes

ServiceProbeMeaning
backend-apicurl -sf http://localhost:3005/pingProcess liveness; stays available during drain.
backend-apiGET /ready on port 3005Whether the backend is accepting new work; separate from external-store health.
platformcurl -sf http://localhost:3000/api/health && [ -f /tmp/platform-ready ]Web service startup completed.
backend-workerDisable the image's web healthcheck.No HTTP server; monitor jobs and worker progress separately.
proxycurl -sf http://127.0.0.1:2020/healthProxy process responds.
dbpg_isready -U tale && [ -f /tmp/.db_ready ]Postgres and initialization are ready; adapt the user to your configuration.
object-storemc ready localBundled MinIO readiness.
sandboxcurl -fsS http://127.0.0.1:8003/healthSpawner readiness after runtime-image preparation.
sandbox-egresscurl -sS -o /dev/null --max-time 3 --noproxy '*' http://127.0.0.1:3128/The proxy answers a non-proxy request itself (400 page), so this proves it serves without reaching a third-party website. Do not probe the port with a bare TCP connect: tinyproxy logs every connect-and-close at error level, one line per interval.
sandbox-llm-gatewaywget -q -O /dev/null http://127.0.0.1:8080/healthUse the image's available client; it does not ship curl.

Give cold starts enough time: downloading the sandbox runtime can take longer than a warm-host probe budget. A successful readiness probe does not prove file access, model credentials, or an entire user task. Check those separately.

Connect external stores

External application Postgres replaces DATABASE_URL; external knowledge uses KNOWLEDGE_DATABASE_URL. The latter needs pgvector and, for full hybrid search, pg_search. Supply a session-compatible connection and any required POSTGRES_CA_FILE. Do not assume a transaction pooler preserves the session behavior Tale needs.

For external S3-compatible storage, set the OBJECT_STORE_* values and browser endpoint explicitly. AWS S3 can use an empty custom endpoint; other stores may need path-style addressing. Provision object permissions and browser CORS, then test an actual upload and download. Remove only the bundled services you no longer use, along with their depends_on references; keep old volumes until migration is verified.

Changing URLs does not migrate existing rows or files. Data residency describes the per-organization and deployment-wide choices. External stores require their own coordinated backups, outside tale backup's volume archives.

Start and accept the installation

Bring stores up before dependent services and use restart policies such as unless-stopped. Prepare VERSION in the shell as described above, then validate your complete Compose file:

bash
docker compose config --quiet
docker pull "ghcr.io/tale-project/tale/tale-sandbox-runtime:$VERSION"
docker compose up -d
docker compose ps
docker compose logs --tail=100 backend-api backend-worker

Confirm healthy services, successful backend migrations, and worker progress. Open the public URL, follow First administrator, configure a provider and embedding model, and test a controlled chat, file upload/download, and knowledge query. If harnesses are required, verify a sandbox session too.

Database migrations run at backend boot. Your deployment procedure must keep compatible versions serving during that work, stop on migration failures, drain active work before replacement, and retain a recovery record. The CLI's blue-green coordination, pending-flip recovery, automatic snapshots, and rollback checks do not happen merely because you copied its service layout.

Map the contract to Kubernetes

Deploy on Kubernetes translates this contract into Deployments, Services, StatefulSets, and NetworkPolicies, switches the sandbox spawner to SANDBOX_BACKEND=kubernetes, and lists the checks a cluster must pass before you admit users. Keep this page as the reference for the service names, volumes, probes, and environment the Kubernetes objects must reproduce.

© 2026 Tale by Ruler GmbH — ISO 27001 & SOC 2 certified.

Tale is MIT licensed — free to use, modify, and distribute.