Skip to main content
Everything an operator does after the runtime works on a laptop: self-host it from the distribution bundle, run and observe it, and wire the shipped plugins an install needs.

Deploy

Run a server for real.

Observe

Collect metrics and traces.

Manage a fleet

Read the worker census and drive fleet-wide operations.

Plugin setup

Backends, storage, accounts, identity, and monitoring.

Self-host with the distribution bundle

tai-distribution packages the whole platform as one minimal-core container image plus a Docker Compose bundle. The image ships the platform core, the agents layer, and the baked reference providers — local storage, the arq backend, redis identity, and both sandbox providers — and bakes the built Studio in, so a self-hosted install is one image and the services it talks to. Every other plugin (the channels, the S3/GitHub storages, the celery/rq backends, the connectors, monitoring, accounts, and the extra tools) installs at runtime through the marketplace or bakes into a derived image — see Add plugins. The image entrypoint is tai and its default command is serve. Three long-lived processes come out of the same image: A one-shot db-migrate step runs tai db migrate, and serve and backend wait for it to complete before they start. It applies the ordered migration chain — the core plus each installed plugin that owns one — and is idempotent: a run with nothing pending is a no-op. Each app container also refuses to serve on an out-of-date database, so a missed migration is a loud refusal, not a broken serve.
The official docker.io/tai42/tai image (mirrored at ghcr.io/tai42ai/tai) is what the Compose bundle runs — signed, scanned, and multi-arch. Pin a release tag in production and verify its signature; the quickstart’s build-from-source is a dev-only alternative. See Self-hosted for the image, upgrades, and artifact verification.
Boot it step by step in the Self-hosted quickstartgit clone, the .env secrets, the config/ bind mount, docker compose up, the opt-in profiles, and the dev-only build-from-source. Only serve publishes a port; Postgres, Redis, and the optional MinIO and RabbitMQ services stay on the Compose network.

Configure the bundle

The Compose bundle reads a small set of variables from .env; everything else is fixed in the file and points the services at each other.
POSTGRES_PASSWORD has no default: Compose refuses to start until it is set. The minio and celery service profiles add MINIO_ROOT_USER, MINIO_ROOT_PASSWORD, STORAGE_S3_BUCKET, RABBITMQ_USER, and RABBITMQ_PASSWORD. Those profiles drive the S3 storage and celery backend plugins, which are not in the minimal image — add the plugin before enabling the profile. The bundled postgres and redis services back the default named database and the shared Redis namespace, so every core Postgres store — the access-control policy store, the connector store, the marketplace store, the versioning store, and the tool-metadata overlay — plus their migrations and every Redis-backed feature resolve without further configuration.

Add plugins

The image is the minimal core. A deployment adds the plugins it needs one of two ways. Runtime, through the marketplace. A running server installs, updates, and uninstalls plugins against itself — it pip installs the package into its own environment, patches the manifest, and reloads, so the plugin is live with no restart — router/middleware and channel inbound routes come live on that same reload, whose epoch rebuild re-imports the plugin and re-registers its routes. See Install a plugin. For those installs to survive a container recreation, the server’s plugin-prefix setting must point at a mounted, persistent directory — see the persistent mounts. Build time, in a derived image. Bake a fixed roster into your own image on top of the core, then name the added modules in your manifest:
A marketplace install writes two things a real deployment must keep across a container recreation — the installed plugin code (TAI_PLUGINS_PREFIX) and its registration in the manifest (TAI_MANIFEST_PATH). Persist both on their own mounts, or a recreated container boots with the code and its registration out of sync. The persistent mounts cover the two volumes in full.
.env.example ships ACCESS_CONTROL_ENABLE=false — the quickstart shape. Turn it on before anything but a laptop, and register an identity provider in the same change: with access control on and no provider, the runtime refuses to boot.

Turn on access control

Set ACCESS_CONTROL_ENABLE=true and register a provider in the manifest. The bundled default is the Redis-backed API-key provider, which the image already installs:
manifest.yml
Then mint the first admin key with the one-shot tai keys bootstrap door — gated by ACCESS_CONTROL_BOOTSTRAP_TOKEN or the auto-generated token logged once at startup, and refused as soon as any key exists — and write the policy that key resolves to. Access control covers the policy model, and the access-control guide walks the first key end to end. ACCESS_CONTROL_ALWAYS_PUBLIC_PATH_PREFIXES selects the paths served without a key. The bundle’s default covers the login route and the Studio’s static assets:
Setting it replaces the app’s own default wholesale rather than adding to it. /health and /ready are deliberately absent: the app already serves them public by route-level declaration, and listing them here is rejected at boot as a conflict. For human sign-in — sessions, roles, invites, and the first admin — add the accounts provider described in Accounts.

Run on Kubernetes

The same image runs on Kubernetes with serve and backend as separate workloads and the datastores pointed at managed Postgres and Redis. The marketplace-install persistence model is the platform-side piece to get right:
Marketplace installs on Kubernetes need the plugin-prefix directory on a shared, persistent volume (a ReadWriteMany PVC) so every serve and backend pod sees the same installed code, plus the manifest on persistent storage. An install pip installs into the one pod that handled the request and broadcasts the manifest reload fleet-wide — the other pods only reload the manifest, so unless the prefix is shared they try to import a package they do not have. serve and backend are separate Deployments — two pods even at one replica each — so ReadWriteOnce suffices only when a single node holds every mounting pod (disable backend, or co-schedule the two onto one node); otherwise the shared ReadWriteMany mount is required. Every marketplace install/upgrade ends in a fleet reload broadcast on the worker bus, so already-running peer pods re-import the changed plugin in place — one pod installs, the others pick it up live with no restart — router and middleware plugins and a channel’s inbound webhook route included: the reload rebuilds the serving epoch, which re-imports the plugin package and its route-sibling modules and re-registers the declared routes, so a net-new or upgraded router serves after the swap without a pod restart. Fleets that want fully static rosters can bake them into a derived image instead.

TLS and the front door

serve speaks plain HTTP on its published port, and the Compose bundle puts nothing in front of it. Terminate TLS at your own reverse proxy or load balancer and forward to that port. On Kubernetes a single-host, single-path Ingress is enough — the Studio and the /api surface share one origin, so there is nothing to split — with TLS terminated at the Ingress. Some features need the public origin spelled out rather than inferred: connectors accept an OAuth redirect only when its origin is listed in CONNECTORS_REDIRECT_URI_ALLOWLIST, and tai42-accounts-oidc requires an https TAI_ACCOUNTS_OIDC_PUBLIC_BASE_URL.

See also