Deploy
Run a server for real.
Infrastructure by feature
Which features need Postgres, Redis or another service.
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 runs tai under tini (ENTRYPOINT ["tini", "--", "tai"]) and its
default command is serve. tini is the container’s init: it forwards signals to
tai and reaps every orphaned process, such as what a host-sandbox exec leaves
behind. A container that runs the server itself as PID 1 needs an init of its own
(docker run --init, or an init as the entrypoint). Three long-lived processes come
out of the same image:
A one-shot
config-init step hands the bind-mounted config/ directory to the
image’s user (uid 1000), since the runtime writes its saved settings there. A
one-shot db-migrate step runs tai db migrate, and serve and backend wait
for both to complete before they start. It applies the ordered migration chain
— the core, and each plugin that owns one, installed or loaded by the
manifest — 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 — it builds the package’s environment under the plugins directory and switches to it, 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,TAI_PLUGINS_DIR must point at
a mounted, persistent directory — see the persistent
mounts. A packaged install refuses
while it is unset.
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_DIR) 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 setup
door — gated by TAI_SETUP_TOKEN or the auto-generated token logged once at
startup, and refused as soon as the owner exists — which creates the owner and
prints its first admin key once. 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.
No token is logged when you pin TAI_SETUP_TOKEN, when no configured provider can
mint keys, when the access-control Redis is unset, or when access control is off — the
last three refuse the door with a 501; initializing a deployment whose logins are OIDC
covers the case where every human signs in through OIDC.
ACCESS_CONTROL_ALWAYS_PUBLIC_PATH_PREFIXES selects further paths served without a
key. The login routes need no entry — each declares itself a public
pre-authentication route — and
the app sets none itself. The bundle’s value names the Studio’s static assets (and
/api/login, which the declarations already cover):
/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. serve is
probed on /health and /ready; a backend process serves neither and is
probed with tai42-backend-probe or over its own HTTP path, as
Backend processes lays out. The
marketplace-install persistence model is the platform-side piece to get right:
Marketplace installs on Kubernetes need the
TAI_PLUGINS_DIR 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 builds the plugin’s environment from the one pod that handled
the request and broadcasts the manifest reload fleet-wide — the other pods only
reload the manifest, so unless the directory is shared they try to import a
package they do not have. A pod whose directory does not match the install store
refuses to start (see the plugin check at
boot). 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.
See also
- Deploy — transports, workers, and the serving modes.
- Back up and restore — what state to capture.
- The layering — what the image actually contains.

