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.git 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 — itpip 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:
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.
Turn on access control
SetACCESS_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
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:
/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 withserve 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
- Deploy — transports, workers, and the serving modes.
- Back up and restore — what state to capture.
- The layering — what the image actually contains.

