Installation
This page covers the step-by-step deployment of a self-hosted XTM One instance using Docker Compose. Before starting, read the Overview to understand the deployment models and the architecture.
Infrastructure requirements
Every deployment runs the same five services. What changes is how much CPU, RAM, and storage you give them. Start from the baseline below, then adjust with What drives sizing.
Resource baseline
The official XTM One Helm chart sets the following per-component resource requests and limits. Use them as a starting point and tune to your observed load:
| Component | CPU request | Memory request | CPU limit | Memory limit | Storage |
|---|---|---|---|---|---|
| Platform | 2 | 4 Gi | 4 | 8 Gi | — |
| Worker | 1 | 2 Gi | 2 | 4 Gi | — |
| PostgreSQL (per instance, 2 instances) | 1 | 2 Gi | 2 | 4 Gi | 64 Gi |
| Redis | 0.5 | 512 Mi | — | 2 Gi | 2 Gi |
| MinIO | 0.5 | 1 Gi | — | 2 Gi | sized to your object volume |
The platform and the worker scale differently. See the Scaling model for how to add worker replicas and keep the database connection budget in range.
OS: Any Linux distribution that supports your chosen container runtime. The Docker images are based on python:3.14-slim (Debian).
Disk: 20 GB minimum for the OS, container images, and initial database. Plan for more based on your knowledge base size and file attachment volume — PostgreSQL data and MinIO object storage grow over time.
What drives sizing
The baseline above suits a typical deployment. Whether you should size up or down depends on how the customer uses the platform. Each resource is driven by a different part of the workload, so profile the use case against these drivers — you rarely need to grow everything at once.
The two dimensions that matter most:
- Interactive use — how many people chat with agents at the same time, and how heavy each conversation is (long streams, large tool results). This drives the platform and, indirectly, Redis.
- Automation and data — how many assignments run, how often, and how much content agents ingest and remember. This drives the worker, PostgreSQL, and MinIO.
Platform — CPU / RAM. Driven by concurrent interactive users and live chat streaming (WebSocket/SSE, LLM proxy). Size up when many people use chat at once or you see request latency climb — add CPU/RAM and raise WEB_WORKERS. A mostly-automation deployment with few human chat users can stay at the minimum. Chat is stateless per request, so this scales with concurrency, not total user count.
In numbers
WEB_WORKERS is the number of concurrent HTTP/chat processes. The default is 2; the production rule of thumb is 2 × CPU cores + 1, so a 4-core platform runs 9 — roughly ×4 the default's request capacity.
Worker — replicas / CPU. Driven by background-job volume and concurrency — the number of active assignments, how frequently their triggers fire, and how bursty they are (many schedules landing together), plus evaluations and sub-agent fan-out. Size up when jobs start queuing or embedding lags. A deployment with a few daily assignments needs one worker; hundreds of assignments, tight schedules, or heavy sub-agent use need replicas.
In numbers
Each worker runs up to WORKER_CONCURRENCY jobs at once (default 20). One worker = 20 concurrent jobs; ×3 workers = 60. Throughput scales linearly with replica count, so match the replica count to your peak simultaneous-job count.
PostgreSQL — storage / RAM. Driven by total data volume — knowledge-base documents and their vector chunks (RAG), agent memory, and run/audit history retention. Storage grows with knowledge bases and history; RAM helps vector search stay fast as embeddings grow. Size up for large or many knowledge bases, long retention, or slow semantic search.
In numbers
Storage is roughly proportional to content — ×2 the ingested documents and history is about ×2 the disk. Start from the chart's 64 Gi baseline and scale from your own corpus size; add RAM only if semantic search slows as embeddings grow.
Redis — RAM. Driven by peak job-queue depth and active-session count — pub/sub for the live activity feed and notifications, plus transient agent state (short/medium-term memory, in-flight chat markers, quota and rate-limit counters). It holds working state, not your data, so it stays small; size up only if you run very high job throughput or many simultaneous active agents. Storage never needs to grow with your knowledge bases.
In numbers
The chart baseline is a 512 Mi request with a 2 Gi limit — roughly ×4 headroom for bursts. Most deployments never approach the limit because working state is transient; treat a rising limit as a signal of extreme queue depth or session concurrency, not routine growth.
MinIO / S3 — storage. Driven purely by stored bytes — uploaded knowledge-base documents, chat attachments, and agent-generated files (images, code-interpreter charts). CPU/RAM stay negligible; only disk grows. Size the volume to the customer's expected document and attachment footprint, and prefer a managed S3 (via S3_*) when that footprint is large or must be highly available.
In numbers
Storage equals the sum of stored file sizes — ×2 the uploaded documents and attachments is ×2 the volume. There is no compute multiplier to plan for; provision disk to the customer's raw file footprint plus headroom.
Container runtime
You need Docker Engine 24+ with Docker Compose v2.
The repository includes a ready-to-use docker-compose.yml.
Get the deployment files
Check Releases for the latest version tag.
Option A — clone a release (builds images from source):
git clone --branch <version-tag> --depth 1 https://github.com/XTM-One-Platform/xtm-one.git && cd xtm-one
Option B — use pre-built images (no build required):
VERSION="<version-tag>"
mkdir xtm-one && cd xtm-one
curl -fSLO "https://raw.githubusercontent.com/XTM-One-Platform/xtm-one/${VERSION}/docker-compose.yml"
curl -fSLO "https://raw.githubusercontent.com/XTM-One-Platform/xtm-one/${VERSION}/.env.sample"
Then create an override to pull images instead of building:
# docker-compose.override.yml
services:
platform:
image: xtmone/platform:<version-tag>
build: !reset null
worker:
image: xtmone/worker:<version-tag>
build: !reset null
Configure environment
cp .env.sample .env
chmod 600 .env
touch .gitignore
grep -qxF ".env" .gitignore || echo ".env" >> .gitignore
Edit .env and fill in the required values. At minimum you must set:
SECRET_KEY— generate withpython -c "import secrets; print(secrets.token_urlsafe(48))"ADMIN_EMAILandADMIN_PASSWORDBASE_URLandFRONTEND_URL— the public URL of your instanceDB_PASSWORD— password for the bundled PostgreSQLS3_ACCESS_KEYandS3_SECRET_KEY— credentials for the bundled MinIO
The complete list of available parameters is documented in the Configuration section.
Start
The platform waits for PostgreSQL, Redis, and MinIO health checks to pass, then automatically runs migrations, creates the admin user, and seeds default templates.
Verify
docker compose ps # all services should show "healthy"
curl localhost:4000/api/health/ready # 200 = fully ready
Configure the platform
- Open your
BASE_URLin a browser. - Sign in with
ADMIN_EMAIL/ADMIN_PASSWORD. - Go to
Settings→AI Models— add at least one LLM provider. - Configure authentication if needed — see Authentication.
Network requirements
Inbound
| Port | Purpose |
|---|---|
| 4000/tcp | Platform HTTP + WebSocket |
Exposing the platform
The platform serves plain HTTP on port 4000. In production you put something in front of it that terminates TLS (so users get HTTPS) and forwards traffic to port 4000. Whatever you choose, it must satisfy the same requirements:
| Requirement | Why | Nginx example |
|---|---|---|
| Forward client IP | Needed for rate limiting and audit logs | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| Forward protocol | OAuth callbacks need the real scheme | proxy_set_header X-Forwarded-Proto $scheme; |
| Allow large request bodies | Knowledge base uploads can reach 50 MB | client_max_body_size 50m; |
| WebSocket upgrade | Required for chat streaming | proxy_set_header Upgrade $http_upgrade; |
| Generous read/idle timeout | LLM calls and streams can run 60s–5min | proxy_read_timeout 300s; |
Set BASE_URL to the public https:// URL you expose, regardless of which option you pick.
Option A — Reverse proxy (default). Best for a standard host with a public IP or load balancer. Terminate TLS with any reverse proxy — Nginx, Caddy, Traefik, or a cloud load balancer — and forward to port 4000. Caddy and Traefik obtain and renew certificates automatically; with Nginx, pair it with your own certificate or a tool such as Certbot.
Option B — Connection tunnel. Instead of opening an inbound port, you can front the platform with a tunnel. This fits three cases:
- Evaluation or demo — a tunnel such as Cloudflare Tunnel (
cloudflared) or ngrok gives you a public HTTPS URL in seconds, with no proxy or certificate to configure. - No public IP — for a host behind CGNAT, a home lab, or an edge site, an outbound-only tunnel exposes the platform without any inbound firewall rule.
- Private-only access — a mesh VPN such as Tailscale or WireGuard reaches the platform over a private overlay with no public exposure at all.
A tunnel must meet the same requirements as a reverse proxy above. Two points need particular attention:
Watch the tunnel timeout
XTM One makes long LLM calls and holds long-lived WebSocket/SSE streams for chat. A tunnel with a short origin/idle timeout will cut these off — for example, Cloudflare's proxy enforces a ~100-second origin timeout (HTTP 524). Use a tunnel that allows long-lived connections, and confirm chat streaming works end to end before relying on it.
Data residency with public tunnels
A public tunnel (Cloudflare, ngrok) terminates TLS on that provider's edge, so your traffic transits a third party. If you self-host XTM One specifically for data-residency or air-gapped requirements, prefer a reverse proxy you control or a private mesh VPN (Tailscale, WireGuard), which keeps traffic on your own network.
Internal (between containers)
| From | To | Port |
|---|---|---|
| Platform, Worker | PostgreSQL | 5432 |
| Platform, Worker | Redis | 6379 |
| Platform, Worker | MinIO/S3 | 9000 |
Air-gapped deployments
Air-gapped deployments work if you use a local AI provider (Ollama or any OpenAI-compatible endpoint on your network) and the built-in MinIO for storage. To get Docker images into an air-gapped network:
# On a machine with internet access — export the images (pre-built images setup from Option B):
docker compose pull
docker save xtmone/platform:<version-tag> xtmone/worker:<version-tag> pgvector/pgvector:pg17 redis:8-alpine minio/minio:latest > xtm-one-images.tar
# Transfer xtm-one-images.tar to the air-gapped host, then load:
docker load < xtm-one-images.tar
Scaling model
What drives sizing tells you which resource to grow for a given use case. This section covers the mechanics — the structural reason the platform and the worker scale in fundamentally different ways, and the one constraint that couples them.
Two different \"workers\"
The word worker means two separate things here. The worker service (the worker container / Helm worker replicas) is the background-job runner you scale horizontally. WEB_WORKERS is unrelated: it sets the number of uvicorn HTTP worker processes inside the platform container, which serve the API and chat. Adding worker replicas does not change WEB_WORKERS, and vice versa.
- The platform scales vertically only — it runs as a single replica because it hosts singleton background services (the scheduler, the event poller, and WebSocket sessions) that must not run in parallel. You cannot add platform replicas; instead give the one instance more CPU/RAM and raise
WEB_WORKERSand the connection-pool sizes. - The worker service scales horizontally — it holds no singleton state, so you add replicas for more background-job throughput. Scaling is manual (there is no autoscaler) and the reference deployment starts with one replica. See What drives sizing for when to add more.
- The connection budget couples the two — every platform HTTP worker and every worker replica opens its own PostgreSQL pool, so
max_connectionsmust accommodate(DB_POOL_SIZE + DB_MAX_OVERFLOW) × (WEB_WORKERS + worker replicas) + overhead. Recalculate it whenever you change either count or the pool sizes (see Configuration).
Running multiple workers (Docker Compose). The default docker-compose.yml sets container_name: worker, which limits you to one instance. To run several, remove (or comment out) the container_name: worker line, then scale:
Each worker creates its own connection pools, so recalculate the PostgreSQL connection budget accordingly. On Kubernetes, add replicas through the Helm chart instead.
Kubernetes and Helm
XTM One can be deployed two ways: the Docker Compose stack described above, which runs the full stack on a single host, or the official Helm chart, which runs the same container images on Kubernetes. Choose the one that fits your infrastructure.
The chart deploys the platform and worker images and can optionally provision the backing services. Its prerequisites are:
- Kubernetes 1.28+ and Helm 3.12+
- the CloudNativePG operator for managed PostgreSQL with pgvector
- the MinIO operator for managed S3-compatible storage (or an external S3)
- Redis (deployed separately, or an external Redis)
Wire the health probes as livenessProbe (/api/health) and readinessProbe (/api/health/ready). The worker scales horizontally by adding replicas; the platform runs as a single replica and scales vertically (see the Scaling model above).
Under construction
The Helm chart deployment guide is under construction. The chart location, its release status, the full values.yaml reference, and a validated production topology will be documented here soon.
Next step
Configure the platform parameters in the Configuration section, and read the Upgrade procedure before your first version bump.