Skip to main content
Trinity

Single-Server Deployment

Run Trinity on a Linux VPS or dedicated server with a stable URL. Two install methods share one .env contract, one installer and one set of day-two procedures:

  • •Option A — prebuilt images (./scripts/deploy/start.sh --hosted): every platform image and the agent base image are pulled from GHCR. A fresh VM is serving in about two minutes. This is the path you want on a server.
  • •Option B — build from source (docker compose -f docker-compose.prod.yml): the server compiles its own images, including the ~1.9 GB agent base image (5–10 minutes). Use it when you carry local patches or the enterprise overlay.

Both use the production compose shape: no hot-reload, health checks and unless-stopped restart policies on every service, Redis off the agent network. Two more paths are Option A on a DigitalOcean Droplet: the Marketplace 1-Click (a snapshot with the images already pulled) and the installer script (creates a stock Droplet from your terminal). Both provision Docker, Caddy with an HTTPS certificate for the Droplet's own IP, and a host firewall before running the same start.sh --hosted.

Prerequisites

  • •Linux server (Ubuntu 22.04 LTS or later recommended), 8 GB RAM minimum — below that the agent containers and the platform services contend and turns start failing under load
  • •Docker Engine 24+ and the Docker Compose plugin (docker compose — no hyphen)
  • •A domain or subdomain pointing to your server's IP (e.g., trinity.your-domain.com), or a plan for private access — see TLS on a bare VM
  • •openssl on the server for secret generation
  • •Outbound HTTPS access from the server (image pulls from ghcr.io, Anthropic API calls)

Which compose files go together

Trinity ships three complete stacks, not one base file plus overlays. docker-compose.prod.yml and docker-compose.hosted.yml are standalone: each restates the hardening (security_opt, group_add, cap_drop) because nothing else supplies it.

InstallCommandWhere /data lives
Dev (source build, localhost)./scripts/deploy/start.sh — i.e. docker compose up -d (auto-merges docker-compose.override.yml if present)named volume trinity-data
Production (source build)docker compose -f docker-compose.prod.yml up -d (+ -f docker-compose.prod.enterprise.yml with the enterprise submodule)bind mount ${TRINITY_DATA_PATH:-./trinity-data}
Hosted (prebuilt GHCR images)./scripts/deploy/start.sh --hosted — day-two: docker compose -f docker-compose.hosted.yml …bind mount ${TRINITY_DATA_PATH:-./trinity-data}

The remaining files are narrow overlays and never a third stack: docker-compose.override.example.yml (the Docker Desktop Vector log source — start.sh copies it to docker-compose.override.yml on Docker Desktop and appends it by name under --hosted), docker-compose.gitea.yml (dev-only: a local Gitea for git-sync testing, layered on the dev file), docker-compose.prod.enterprise.yml (layers the private enterprise submodule onto prod), and docker-compose.sibling.yml (a Redis-only stack for integration tests).

Never stack the dev file under prod or hosted (-f docker-compose.yml -f docker-compose.prod.yml). Compose concatenates list-type keys, so the combination either fails validation on the duplicated hardening entries (Compose ≥ 2.24) or, on older versions, silently gives the frontend two host mappings for one port and it never joins its network.

Never run a bare docker compose up -d on a production host. It loads the dev file, whose /data is the named volume, and boots a healthy-looking backend on an empty database while the real one sits untouched in TRINITY_DATA_PATH. start.sh refuses this crossing in both directions and prints the file set the host was installed with (quickstart.sh is an alias for start.sh, so it inherits the refusal). If both stores already exist — the state a wrong-file start leaves behind — start.sh warns which one it is about to use rather than staying silent.

Option B: Build from source

1. Clone the Repository

git clone https://github.com/abilityai/trinity.git
cd trinity

2. Configure .env

cp .env.example .env

start.sh has no production-compose mode: without --hosted it starts the dev stack. A source-built production install is therefore brought up with docker compose -f docker-compose.prod.yml directly, and you generate the secrets yourself. Every variable in the tables below is forwarded by docker-compose.prod.yml (and by the hosted file — the two agree by construction).

Security-critical (must be set before first boot)

VariableHow to generateNotes
SECRET_KEYopenssl rand -hex 32JWT signing key. Never reuse across instances.
ADMIN_PASSWORDChoose a strong passwordMinimum 12 characters. Drives both admin login and the MCP server's legacy auth path. Required — docker-compose.prod.yml refuses to render if it is unset or blank. (docker-compose.hosted.yml refuses only an unset one; a blank is accepted solely for the marketplace browser-claim path, marked ADMIN_PASSWORD_SOURCE=browser.)
ADMIN_USERNAMEOptional, default adminThe admin account's username. Forwarded by all three compose files.
CREDENTIAL_ENCRYPTION_KEYopenssl rand -hex 32Encrypts OAuth tokens, channel bot tokens, subscription credentials and the credential-bearing platform settings. If lost, all encrypted credentials become unrecoverable.
INTERNAL_API_SECRETopenssl rand -hex 32Authenticates scheduler-to-backend calls. Set it explicitly — do not rely on the SECRET_KEY fallback.
AGENT_AUTH_SECRETopenssl rand -hex 32Master secret the backend derives each agent's in-container auth token from. Never rotate — every running agent's token stops working until the agent is recreated.
REDIS_PASSWORDopenssl rand -hex 24Admin/default ACL user. Used for recovery and ad-hoc ops.
REDIS_BACKEND_PASSWORDopenssl rand -hex 24Runtime ACL user for backend and scheduler containers. Embedded in REDIS_URL at compose render time. Required — compose refuses to render without it.
DOCKER_GIDGID of /var/run/docker.sock as a container sees itCompose falls back to 999 (the Debian/Ubuntu docker group). Set it if your host's socket group differs (stat -c '%g' /var/run/docker.sock); start.sh detects it for hosted installs.

Generate the hex secrets at once:

echo "SECRET_KEY=$(openssl rand -hex 32)"
echo "CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -hex 32)"
echo "INTERNAL_API_SECRET=$(openssl rand -hex 32)"
echo "AGENT_AUTH_SECRET=$(openssl rand -hex 32)"
echo "REDIS_PASSWORD=$(openssl rand -hex 24)"
echo "REDIS_BACKEND_PASSWORD=$(openssl rand -hex 24)"

Paste the output into .env.

Redis security note: Trinity uses two separate Redis passwords by design. REDIS_BACKEND_PASSWORD is the runtime credential embedded in REDIS_URL for the backend and scheduler containers. Even if a platform container were compromised and this password leaked, it does not grant access to destructive Redis commands (FLUSHALL, CONFIG, SHUTDOWN, etc.) — those require REDIS_PASSWORD. See docs/migrations/REDIS_AUTH.md for details on the ACL design.

Required for agent functionality

VariableNotes
ANTHROPIC_API_KEYRequired for agents to run Claude. Can be left blank and configured in Settings after login (or connect a Claude subscription there).
GITHUB_PATRequired to clone private GitHub template repos.

Required for production access

VariableNotes
FRONTEND_URLYour public-facing domain (e.g., https://trinity.your-domain.com). Used for OAuth redirect callbacks and email verification links.
PUBLIC_CHAT_URLThe externally reachable URL for public chat links and webhooks. Often the same as FRONTEND_URL. Leave blank if all users access via VPN.
FRONTEND_PORTHost port the web UI binds (default 80). Honoured by the prod and hosted compose files as well as dev.
TRINITY_DATA_PATHBind-mount directory for /data (see Data path).

Email authentication

Email login is enabled by default. Set at least one email provider:

VariableNotes
EMAIL_PROVIDERresend (recommended), sendgrid, smtp, or console (logs codes — dev only). .env.example ships console; the compose default when the line is absent is resend.
RESEND_API_KEYRequired when EMAIL_PROVIDER=resend. Get from resend.com.
SENDGRID_API_KEYRequired when EMAIL_PROVIDER=sendgrid.
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASSWORDRequired when EMAIL_PROVIDER=smtp.
SMTP_FROMFrom address for verification emails (e.g., noreply@your-domain.com).

Optional integrations

VariableNotes
SLACK_CLIENT_ID / SLACK_CLIENT_SECRET / SLACK_SIGNING_SECRETSlack OAuth and channel adapter. The client id/secret are read by the prod and hosted files only.
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETGoogle Workspace OAuth.
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRETGitHub OAuth (prod and hosted only).
NOTION_CLIENT_ID / NOTION_CLIENT_SECRETNotion OAuth (prod and hosted only).
GEMINI_API_KEYPlatform image generation, voice chat, voice transcription.
TUNNEL_TOKENCloudflare Tunnel token — see Public Access.

Data path

The prod and hosted compose files use a bind-mount directory for trinity.db instead of a named Docker volume:

TRINITY_DATA_PATH=./trinity-data

The default ./trinity-data is relative to the repo root. Use an absolute path on a server for clarity:

TRINITY_DATA_PATH=/srv/trinity-data

Create the directory before starting, owned by UID 1000 (the backend and scheduler run as a non-root user and cannot create trinity.db in a root-owned directory; start.sh does this step for hosted installs):

mkdir -p /srv/trinity-data && sudo chown -R 1000:1000 /srv/trinity-data

The automatic database backups land in backups/ under this directory — see Backup and Restore.

Database backend

Trinity stores platform state in SQLite by default, in trinity.db under your TRINITY_DATA_PATH bind mount (/data/trinity.db inside the container). SQLite works with zero configuration — but PostgreSQL is the recommended backend for production, and SQLite support ends September 1, 2026 (after that date it stops receiving schema migrations and fixes).

To run on PostgreSQL, set one variable in .env:

DATABASE_URL=postgresql://trinity:your-postgres-password@your-db-host:5432/trinity

Both the backend and the scheduler pick it up (DB_POOL_SIZE and DB_MAX_OVERFLOW tune the pool; both default sensibly). Notes for the prod and hosted compose:

  • •Neither ships a bundled PostgreSQL service — point DATABASE_URL at an operator-managed instance (a managed cloud database or your own PostgreSQL server). The bundled --profile postgres container exists only in the dev compose.
  • •Selection is non-sticky and non-destructive: comment DATABASE_URL out and the next restart is back on SQLite.
  • •A fresh PostgreSQL database is initialized automatically on first boot (Alembic-managed migrations).
  • •Migrating an existing SQLite instance? Use the Trinity Ops Agent's /migrate-to-postgres skill (ops-agent guide) — a validate-then-cutover flow that never writes to your SQLite file, so rollback is one line. New-instance setup details: docs/POSTGRESQL_SETUP.md in the repo.
  • •On PostgreSQL, the automatic backups are pg_dump archives, and a manual backup is pg_dump too — never a copy of trinity.db. See Backup and Restore.

On every backend boot, a versioned migration runner brings the schema up to date (the bespoke SQLite runner or Alembic for PostgreSQL). The runner is crash-safe and concurrency-safe: a cross-process lock serialises it so multiple workers and the scheduler cannot race each other, and table rebuilds run inside a transaction that rolls back cleanly on a mid-migration crash. If a migration is still pending or has failed, the backend's /health endpoint returns 503 with a migrations block (applied, expected, first_pending) naming the stuck migration — so a 503 from curl http://localhost:8000/health during an upgrade is actionable, not opaque. Before any migration runs, the backend takes a pre-migration-<timestamp>.db copy (SQLite) into backups/.

3. Build the Base Agent Image

./scripts/deploy/build-base-image.sh

This builds trinity-agent-base:latest — the image every agent container inherits. Required before you can create any agents. Takes 5–10 minutes on first build.

4. Build and Start Platform Services

docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d

This starts: backend, frontend, redis, mcp-server, scheduler, vector, and otel-collector, plus a one-shot trinity-logs-init container that prepares the log directory and exits.

The cloudflared tunnel service is not started by default — it requires an explicit --profile tunnel flag. See Public Access.

First Login

Open your domain (or http://your-server-ip, or http://your-server-ip:$FRONTEND_PORT) in a browser. Log in with:

  • Username: admin (or ADMIN_USERNAME if you changed it)
  • Password: the ADMIN_PASSWORD you set in .env (re-applied on every backend boot — change it there, not in the UI, then recreate the backend container with docker compose -f <file> up -d backend; a plain restart does not re-read .env)

There is no setup wizard on a server install: setting ADMIN_PASSWORD provisions the admin account during startup, and the unauthenticated first-run form refuses to run once a usable admin exists. The one exception is a Marketplace 1-Click Droplet created without a password, which deliberately boots with no admin and is claimed in the browser — see DigitalOcean 1-Click. What you see instead is the Dashboard with a fresh install's starter fleet and the first-run setup sequence — a Sign-in email step (so you can log in with email + password), the required Connect Claude step, and optional keys — see First-Time Setup.

After login, go to Settings → Access → Email Whitelist to allow team members to log in via email verification.

Connect from Claude Code

Create an MCP API key:

1

Log in to the web UI

2

Go to Settings → MCP Keys

3

Create a new key and copy it

Then connect from your Claude Code session:

/trinity:connect
# URL: https://trinity.your-domain.com/mcp   (the production frontend proxies /mcp to the MCP server)
#      or http://your-server:8080/mcp        (the MCP server's own port, where it is reachable)
# API Key: (your MCP API key)

The /mcp proxy on the production nginx means an install that exposes only ports 80/443 — a tunnel, a reverse proxy, the DigitalOcean 1-Click — still serves MCP at the same hostname as the web UI. Settings → MCP Keys → MCP Server URL sets the URL the UI shows to users; leave it empty to derive it from the hostname.

Restart vs. Down

Use docker compose restart, not down/up. docker compose down removes the trinity-agent-network, which orphans every running agent container — they keep running but lose their network and have to be removed and recreated. restart preserves both the agents and the network. The only times to use down are: (1) intentional full teardown, (2) recovering from a corrupted compose state.

# Correct way to restart platform services
docker compose -f docker-compose.prod.yml restart backend frontend mcp-server scheduler
# Hosted installs
docker compose -f docker-compose.hosted.yml restart backend frontend mcp-server scheduler

# Full stop (agents will need to be restarted/recreated)
docker compose -f docker-compose.prod.yml down

Since agent containers are created with Docker's unless-stopped restart policy, a down no longer leaves them merely orphaned: dockerd retries each one in a backoff loop against a network that no longer exists, and the roster shows them as stopped while it churns. Recovery: docker compose -f <file> up -d (recreates the network), then docker rm -f each stale agent container and start it again from the UI or Operations → Restart All— the workspace volume holds the agent's data and is untouched by a container removal.

./scripts/deploy/stop.sh runs stop, never down, and reads the running stack's compose label to pick docker-compose.hosted.yml when needed. It does not detect a source-built production stack — on one of those, use the explicit docker compose -f docker-compose.prod.yml stop.

Host reboots

Every platform service carries restart: unless-stopped, and so does every agent container Trinity creates, so a host reboot brings the whole fleet back. An agent you stopped on purpose (Stop, quarantine, emergency stop) stays stopped — that is the difference between unless-stopped and always. Agent containers created before this policy shipped keep Docker's default (no) until they are recreated; the one-shot sweep is in Upgrading → Agent restart policy.

Verify Service Health

After starting, verify all services are healthy:

# Backend
curl -s http://localhost:8000/health

# Scheduler
curl -s http://localhost:8001/health

# Frontend
curl -s -o /dev/null -w '%{http_code}' http://localhost

# Redis
docker exec trinity-redis redis-cli ping

# MCP Server
curl -s http://localhost:8080/health

# Vector
docker exec trinity-vector wget -q -O - http://localhost:8686/health

The scheduler's port 8001 is not published to the host by either server compose file — probe it from inside the container instead: docker exec trinity-scheduler curl -sf http://localhost:8001/health. See the Monitoring guide for the full monitoring reference.

TLS on a bare VM

Trinity serves plain HTTP and terminates TLS outside the application. There is no HTTPS listener in any compose file and no auto-certificate step, so pick one of these before putting an instance on a public address:

PathWhat it gives youWhen to use it
Tunnel (Cloudflare Tunnel — set TUNNEL_TOKEN in .env)HTTPS at a real hostname, no inbound ports open at allThe default for a public instance. Nothing to renew.
Private network (Tailscale / WireGuard / VPC)Encrypted transport, instance not on the public internetHTTP over a WireGuard tunnel is encrypted — this is a finished posture, not a compromise.
Reverse proxy you run (Caddy / nginx + Let's Encrypt)HTTPS at your own domainYou already operate a proxy, or you need a domain the tunnel can't serve.

Plain HTTP on a public IPv4 with none of the above is the one combination to avoid: credentials and JWTs cross the network in the clear. A provisioned DigitalOcean Droplet — the 1-Click or the installer script below — is the deliberate exception: it ships its own Caddy with a short-lived certificate for the Droplet's IP, and the first-run setup then prompts you to add a domain and a tunnel. Tunnel setup: Public Access.

DigitalOcean Marketplace 1-Click

The Trinity 1-Click is a Droplet image with Docker, Caddy, ufw and a pinned Trinity release already pulled — Option A baked into a snapshot, so first boot pulls nothing.

Trinity is published on the DigitalOcean Marketplace. The button below opens Droplet creation with the Trinity image already selected — choose a region and a size from the table below, then create. You can also find it by searching Trinity under Marketplace when creating a Droplet from the control panel.

Deploy to DigitalOceanTrinity is free and open source; you pay DigitalOcean for the Droplet.

Prefer to choose the admin password and hand over a Claude subscription before the Droplet exists? trinity-do-create.sh gives the same result from your own terminal — see Deploy on DigitalOcean. The sections below (first boot, sign-in, managing the Droplet) apply to both, with four differences: an installer Droplet has its admin account from first boot, takes about six minutes rather than ninety seconds (it installs and pulls everything on first boot), records do-script instead of do-marketplace as its provenance, and has no login banner.

Sizing

Use caseRAMvCPUBoot disk
Minimum — platform plus 1–2 agents4 GB250 GB
Recommended — a working fleet8 GB480 GB
Larger fleets16 GB+8+160 GB+

The baked images occupy a significant share of the disk before any agent exists; agent workspaces grow from there. Disk can be increased on a running Droplet, never decreased.

What first boot does

First boot runs once per Droplet, about ninety seconds, with no input from you:

  1. 1Admin account — none is created. Unless you supplied a password through user-data (below), ADMIN_PASSWORD stays blank and ADMIN_PASSWORD_SOURCE=browser records that this is deliberate; the first person to open the instance creates the admin in the browser. Nothing is generated, so nothing is printed.
  2. 2.env — written at /opt/trinity/.env with FRONTEND_PORT=8081 (the web UI moves off :80 so Caddy can own 80/443), TRINITY_IMAGE_TAG=<the baked release>, TRINITY_INSTALL_SOURCE=do-marketplace (the install-provenance marker) and FRONTEND_URL=https://<droplet-ip>. start.sh generates the remaining secrets as on any install.
  3. 3Firewall — ufw allows 22, 80 and 443 only. Docker publishes container ports past ufw, so a separate DOCKER-USER rule set (re-applied by a systemd unit on every boot) drops everything arriving at a container from off-box, whatever the port — there is no port list to keep in step with the compose file. It also blocks containers from reaching the cloud metadata service, so an agent cannot read the Droplet's user-data. Everything you use is served by Caddy on 80/443.
  4. 4Caddy — a Caddyfile for https://<droplet-ip> with a Let's Encrypt short-lived IP certificate (about six days, renewed automatically), reverse-proxying to the web UI on 127.0.0.1:8081; http:// redirects to https://. A second catch-all site issues a certificate on demand for the domain you later save as the Public URL (and refuses every other name). First boot verifies the IP certificate was actually issued and records the result — the login banner prints http:// if it was not.
  5. 5Trinity — ./scripts/deploy/start.sh --hosted --unattended from /opt/trinity.

Claim the admin account

Open https://<droplet-ip> as soon as the Droplet is up. You land on a Create your admin account form: enter your email (it becomes your sign-in identity), choose a password (12+ characters with uppercase, lowercase, a digit and a special character), and you are signed straight in. No terminal, no console, no password to copy.

The window between creating the Droplet and that first visit is the accepted risk of this path: anyone who finds the IP first can claim the instance. It holds nothing at that moment and can simply be destroyed and recreated. To keep the window short, open the URL right after creating the Droplet (first boot takes about ninety seconds), or restrict port 443 to your own IP with a cloud firewall until you have claimed it (leave 80 open — Let's Encrypt validates the IP certificate over it, and it serves only a redirect). If a Droplet you have never opened shows the login page instead of the form, someone else got there first: destroy it and create another.

To skip the claim window, choose the password before first boot. Paste this into Additional Options → Startup scripts when creating the Droplet:

#cloud-config
write_files:
  - path: /etc/trinity/admin-password
    permissions: '0600'
    content: "your-password-here"

It must be #cloud-config with write_files, not a shell script — a shell script runs after first boot has already started Trinity. The file is shredded once the password has been read, the admin is provisioned at boot, and the form never appears; sign in as admin with that password. (The installer script does the same from your terminal.)

The login banner (Droplet → Console) prints the URL to open, whether HTTPS came up, and which of the two paths this Droplet took — never a password.

Sign in and harden

The certificate is a real Let's Encrypt certificate for the IP address, so there is no browser warning.

Passwords. On a Droplet claimed in the browser, .env keeps ADMIN_PASSWORD blank on purpose — the password lives only in the database, and reboots and start.sh --hosted upgrades leave it alone. There is no change-password form in the UI. To change or reset the password on any Droplet, set ADMIN_PASSWORD in /opt/trinity/.env and re-run ./scripts/deploy/start.sh --hosted (or docker compose -f docker-compose.hosted.yml up -d backend); the backend adopts the value on the next boot. A plain restart does not re-read .env.

Because the install recorded do-marketplace as its provenance, the first-run setup that opens on the Dashboard begins with a Secure this instance step for admins. It is a two-stage upgrade prompt, not a breakage warning:

  1. 1Give it a real name — point a domain's A record at the Droplet, then save it as the Public URL on the step itself (the same field lives in Settings → General). Caddy on the Droplet obtains a Let's Encrypt certificate for that name the first time someone visits it — give DNS a moment to settle — so adding a domain is a Settings field and nothing else. Trinity then hands out the name instead of the IP. Saving a domain completes the step.
  2. 2Serve it without exposing it — with that domain on Cloudflare, a Cloudflare Tunnel lets the server stop listening on the public internet while inbound integrations (Telegram, WhatsApp, VoIP, public agent links, webhook triggers) keep working. Set TUNNEL_TOKEN in /opt/trinity/.env and re-run start.sh --hosted — see Public Access. This stage is guidance only and optional.

The step describes only what the instance advertises (the configured Public URL); nothing inspects a certificate or opens a socket. Skipping it is remembered per browser; re-open the sequence any time from Settings → General → Re-run setup or by adding ?onboarding=1 to the Dashboard URL. Provenance itself is recorded once at first boot and cannot be edited afterwards — the marker in .env is never re-read, and the API refuses to write or clear it — so the step never appears on an install that was not provisioned this way.

Connect a Claude credential in the Connect Claude step (the one required step; it is also under Settings → Integrations), then create your first agent — the seeded starter fleet is already running.

Managing the Droplet

Over SSH or in the Droplet Console:

cd /opt/trinity

docker compose -f docker-compose.hosted.yml ps          # status
./scripts/deploy/stop.sh                                # stop (never `down`)
./scripts/deploy/start.sh --hosted                      # start
docker compose -f docker-compose.hosted.yml restart     # restart
docker compose -f docker-compose.hosted.yml logs -f backend

Updating. Pin the release you want in .env, check out the matching tag so the compose files and mounted config move with the images, and re-run the installer:

cd /opt/trinity
echo 'TRINITY_IMAGE_TAG=v0.9.1' >> .env
sudo git fetch --tags && sudo git checkout v0.9.1
sudo ./scripts/deploy/start.sh --hosted

Backups. Nightly and pre-migration database backups land in /opt/trinity/trinity-data/backups/ (the data directory is ./trinity-data relative to the checkout). They are on the same disk as the database, so they protect against corruption and mistakes, not against losing the Droplet — take Droplet snapshots as well. See Backup and Restore.

Support. GitHub Issues on abilityai/trinity, label do-marketplace. DigitalOcean does not build or support Trinity.

DigitalOcean installer script

scripts/deploy/trinity-do-create.sh gives you the 1-Click result from your own terminal, with the admin password chosen before the Droplet exists — so there is no claim window. It needs doctl installed and signed in (doctl auth init with a write-scoped API token). The step-by-step walkthrough — from installing doctl to adding a domain, with the installer's error messages and how to remove the Droplet — is Deploy on DigitalOcean.

bash <(curl -fsSL https://raw.githubusercontent.com/abilityai/trinity/<release-tag>/scripts/deploy/trinity-do-create.sh)

It asks four questions and writes nothing to your computer:

  1. 1Admin password (twice; 12+ characters, guessable prefixes refused). Your username will be admin.
  2. 2Claude subscription token — run claude setup-token in another terminal and paste the sk-ant-oat01-… value. An API key is not accepted here; add one later under Settings → Integrations if you prefer.
  3. 3Region and Droplet name (defaults offered), then a confirmation that names the monthly cost.

It then creates an Ubuntu 24.04 Droplet (4 vCPU / 8 GB — Trinity's recommended size), attaches every SSH key already on your account, and hands the Droplet a first-boot script that clones the pinned release to /opt/trinity, runs start.sh --provision --cloud digitalocean --hosted --unattended (Docker, Caddy with the IP certificate, the firewall, then the install itself), registers your Claude subscription and assigns it to the seeded agents. Both secrets travel only in the Droplet's own user-data; the firewall blocks containers from reading it back. The script polls https://<ip>/ with certificate verification for up to fifteen minutes and prints the address when it answers. If it times out, open the Droplet's Console and read /var/log/trinity-install.log.

The install records do-script as its provenance, so the first-run Secure this instance step appears exactly as on the 1-Click. Day-two operations are identical — see Managing the Droplet. The script pins the release it was fetched from; set TRINITY_IMAGE_TAG in the environment before running it to pick another.

.env reference

Every key in .env.example, with the compose files that forward it. A key a compose file does not forward does nothing on that install. Legend: dev = docker-compose.yml, prod = docker-compose.prod.yml, hosted = docker-compose.hosted.yml. Keys marked “commented” ship commented out in .env.example and take effect only when you uncomment them.

Core, security, admin

KeyForwarded byWhat it does
SECRET_KEYdev · prod · hostedJWT signing key. Generated by start.sh if blank.
CREDENTIAL_ENCRYPTION_KEYdev · prod · hostedEncrypts credentials at rest. Generated if blank; never change once set.
CREDENTIAL_ENCRYPTION_KEY_SECONDARYdev · prod · hostedDecrypt-only fallback used only during key rotation (scripts/deploy/rotate-credential-key.py). Leave empty normally.
INTERNAL_API_SECRETdev · prod · hostedScheduler-to-backend and internal-route secret. Generated if blank.
AGENT_AUTH_SECRETdev · prod · hostedMaster for per-agent in-container auth tokens. Generated if blank; never rotate.
ADMIN_USERNAMEdev · prod · hostedAdmin account username (default admin).
ADMIN_PASSWORDdev · prod · hostedAdmin password; also the MCP server's legacy password auth. Prod refuses to render if unset or blank; hosted refuses only unset (blank is the marketplace browser-claim path).
ADMIN_PASSWORD_SOURCE (commented)hostedbrowser marks a deliberately blank ADMIN_PASSWORD on a marketplace image: no admin is provisioned and the first visitor creates one at /setup. Written by first boot; leave unset on every other install (a blank password without it is refused).
ANTHROPIC_API_KEYdev · prod · hostedPlatform-wide Claude API key for agents (or set it in Settings).
PUBLIC_ACCESS_REQUESTS_ENABLEDdev · prod · hostedtrue lets anyone reaching the backend add their own email to the login whitelist (POST /api/access/request). Default false.
DOCKER_GIDdev · prod · hostedGroup of the Docker socket inside the backend container (default 999; start.sh detects it).

Outbound intake and telemetry

KeyForwarded byWhat it does
OPERATOR_INTAKE_ENABLEDdev · prod · hostedfalse disables the once-per-install, opt-in operator contact submission.
OPERATOR_INTAKE_URLdev · prod · hostedEndpoint for that submission (override to self-host).
DO_NOT_TRACKdev · prod · hostedAny value other than 0/empty/false disables the operator intake and telemetry sharing.
TELEMETRY_SHARING_ENABLEDdev · prod · hostedHard kill switch for opt-in usage sharing (false → the Settings consent toggle refuses).
TELEMETRY_SHARING_URLdev · prod · hostedReceiver endpoint (override to self-host).
TELEMETRY_SHARING_INTERVAL_HOURSdev · prod · hostedSharing heartbeat cadence (default 24).
TELEMETRY_SHARING_BACKFILL_DEFAULT_DAYSdev · prod · hostedDefault backfill window at consent (default 30).
TEMPLATE_REGISTRY_ENABLEDdev · prod · hostedfalse stops fetching the vendor template registry (air-gap).
TEMPLATE_REGISTRY_URLdev · prod · hostedRegistry document URL (HTTPS only, no redirects).
VITE_BUG_REPORTING_ENABLEDprod build onlyBaked into the production frontend image at build time; false removes the in-app bug/feedback widget. Published hosted images carry the default; the dev Vite server reads src/frontend/.env instead.
VITE_BUG_INTAKE_URLprod build onlyBug-report endpoint baked into the production frontend (repointing also needs a CSP change in the frontend sources).

Email login

KeyForwarded byWhat it does
EMAIL_PROVIDERdev · prod · hostedconsole, smtp, sendgrid, or resend (compose default resend when absent).
RESEND_API_KEYdev · prod · hostedResend API key.
SENDGRID_API_KEYdev · prod · hostedSendGrid API key.
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASSWORDdev · prod · hostedSMTP transport.
SMTP_FROMdev · prod · hostedFrom address for verification emails.
EXTRA_CORS_ORIGINSdev · prod · hostedExtra allowed browser origins, comma-separated.

OAuth providers and GitHub

KeyForwarded byWhat it does
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETdev · prod · hostedGoogle Workspace OAuth.
SLACK_CLIENT_ID / SLACK_CLIENT_SECRETprod · hostedSlack OAuth app.
SLACK_SIGNING_SECRETdev · prod · hostedVerifies Slack webhook requests.
SLACK_SOCKET_CONNECTION_COUNTdev · prod · hostedConcurrent Slack Socket Mode connections (1–10, default 2).
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRETprod · hostedGitHub OAuth app.
GITHUB_PATdev · prod · hostedPlatform PAT for cloning private template repos.
TRINITY_GIT_BASE_URL / TRINITY_GIT_API_BASE (commented)gitea overlay onlySelf-hosted git (GitHub Enterprise, Gitea); active only under -f docker-compose.gitea.yml.
NOTION_CLIENT_ID / NOTION_CLIENT_SECRETprod · hostedNotion OAuth app.

Models, image generation, voice, telephony

KeyForwarded byWhat it does
GEMINI_API_KEYdev · prod · hostedPlatform image generation; also required for voice and VoIP.
GOOGLE_API_KEYdev · prod · hostedInjected into Gemini-runtime agents; platform fallback for GEMINI_API_KEY.
ELEVENLABS_API_KEYdev · prod · hostedOutbound voice replies on channels; empty = off.
ELEVENLABS_MODEL_IDdev · prod · hostedElevenLabs model (default eleven_multilingual_v2).
TTS_MAX_CHARSdev · prod · hostedReplies longer than this are delivered as text instead of synthesized.
GEMINI_TEXT_MODEL / GEMINI_TRANSCRIPTION_MODEL (commented)dev · prod · hostedOverride the built-in Gemini model ids. Keep commented unless overriding — an empty value shadows the default.
VOICE_ENABLEDdev · prod · hostedVoice chat platform-wide (default on).
VOICE_MODEL (commented)dev · prod · hostedGemini Live model override. Keep commented unless overriding.
WORKSPACE_VOICE_MAX_DURATIONdev · prod · hostedMax length of one Workspace voice call, seconds (default 1800).
VOIP_ENABLEDdev · prod · hostedOutbound phone calls via Twilio (default off; also needs a per-agent binding).
A2A_OUTBOUND_ENABLEDdev · prod · hostedLet agents task external A2A agents (default off; also needs registered endpoints).
VOIP_MAX_CALL_DURATION / VOIP_DEFAULT_DAILY_CALL_CAP / VOIP_CALL_RATE_LIMIT / VOIP_CALL_RATE_WINDOW / VOIP_TICKET_TTL_SECONDS / VOIP_INTENT_TTL_SECONDSdev · prod · hostedTelephony spend and abuse controls.

Rate limits and caps

KeyForwarded byWhat it does
REPORT_RATE_LIMITdev · prod · hostedStructured reports an agent may create per 60 s.
CANVAS_MAX_PER_AGENTdev · prod · hostedCanvases an agent may hold before writes are refused (default 100).
PORTAL_CHAT_BURST_LIMIT / PORTAL_CHAT_HOURLY_LIMITdev · prod · hostedWorkspace chat sends per (email, agent).
PORTAL_UPLOAD_BURST_LIMIT / PORTAL_UPLOAD_HOURLY_LIMITdev · prod · hostedWorkspace uploads per email.
PORTAL_FILE_BURST_LIMIT / PORTAL_FILE_HOURLY_LIMIT / PORTAL_FILE_DELETE_BURST_LIMITdev · prod · hostedWorkspace file downloads and deletes per email.
PORTAL_TITLE_MODEL / PORTAL_TITLE_TIMEOUT_SECONDSdev · prod · hostedModel and timeout used to name a Workspace thread.
SKILLS_RECONCILE_MAX_REMOVALS / SKILLS_FLEET_INJECT_CONCURRENCYdev · prod · hostedSkills-library reconcile safety cap and fleet re-inject parallelism.
TRINITY_DEFAULT_SKILL_SOURCE / TRINITY_DEFAULT_SKILL_SOURCE_REF (commented)dev · prod · hostedBundled community skills source seeded on fresh installs; set the URL to "" to disable the seed.
WEBHOOK_RATE_LIMIT / WEBHOOK_IP_RATE_LIMIT / WEBHOOK_MAX_BODY_BYTESdev · prod · hostedPublic webhook trigger limits.
REMINDER_MESSAGE_MAX_CHARS / REMINDER_MIN_DELAY_SECONDS / REMINDER_MAX_DELAY_SECONDS / MAX_PENDING_REMINDERS_PER_AGENT / MAX_REMINDERS_PER_AGENT_PER_DAY / REMINDER_RATE_LIMITdev · prod · hostedAgent self-reminder caps.
OPERATOR_QUEUE_MAX_PENDING_PER_AGENT / OPERATOR_QUEUE_CREATE_RATE_LIMIT / OPERATOR_QUEUE_CREATE_RATE_WINDOW / OPERATOR_QUEUE_FLEET_CREATE_RATE_LIMIT / OPERATOR_QUEUE_MAX_SCAN_PER_CYCLE / OPERATOR_QUEUE_MAX_FILE_BYTES / OPERATOR_QUEUE_TITLE_MAX / OPERATOR_QUEUE_QUESTION_MAX / OPERATOR_QUEUE_CONTEXT_MAX_BYTES / OPERATOR_QUEUE_OPTIONS_MAX_BYTES / OPERATOR_QUEUE_ID_MAX / OPERATOR_QUEUE_EXECUTION_ID_MAX / OPERATOR_QUEUE_EMAIL_MAX / OPERATOR_QUEUE_FLOOD_ALERT_COOLDOWN_SECONDS / OPERATOR_ALERT_MAX_PENDING_PER_TYPEdev · prod · hostedOperator-queue ingestion caps (bound a runaway agent).

Install identity, URLs, ports, data

KeyForwarded byWhat it does
TRINITY_INSTALL_SOURCEdev · prod · hostedInstall-provenance marker (do-marketplace, vultr-marketplace, do-script, script); written by start.sh --provision, read once at first boot and recorded permanently. Leave empty on an ordinary install.
BACKEND_URLdev · prod · hostedBackend base URL used to build OAuth callback URLs (default http://localhost:8000).
FRONTEND_PORTdev · prod · hostedHost port for the web UI (default 80).
FRONTEND_URLdev · prod · hostedPublic UI URL for email links and OAuth callbacks.
PUBLIC_CHAT_URLdev · prod · hostedExternal base URL for public chat links and webhooks.
TRINITY_IMAGE_TAGhostedWhich published image set to pull (default latest). Ignored by source builds.
TUNNEL_TOKENprod · hostedCloudflare Tunnel token; the cloudflared service is profile-gated.
SSH_HOSTdev · prod · hostedHost advertised for agent SSH access (auto-detected from FRONTEND_URL when empty).
TRINITY_DATA_PATHprod · hostedBind-mount directory for /data (default ./trinity-data). Dev uses a named volume.
HOST_TEMPLATES_PATHprod · hostedHost path of the agent-template directory when compose runs outside the repo root.
TRINITY_INSTANCE_NAMEdev · prod · hostedLabel naming this instance in outbound alerts.
TRINITY_DEFAULT_SYSTEM_MANIFESTdev · prod · hostedPath to your own first-run starter-fleet manifest, or disabled to skip seeding.
TRINITY_MANIFESTS_DIRdev · prod · hostedDirectory the “Install a system” catalog reads (bind-mount it too).

Redis

KeyForwarded byWhat it does
REDIS_PASSWORDdev · prod · hosted (also the sibling test stack)Redis admin (default) user password.
REDIS_BACKEND_PASSWORDdev · prod · hosted (also the sibling test stack)Runtime password for the backend/scheduler users; compose builds REDIS_URL from it.

Database

KeyForwarded byWhat it does
DATABASE_URL (commented)dev · prod · hostedpostgresql://… switches the backend and scheduler to PostgreSQL; unset = SQLite.
DB_POOL_SIZE / DB_MAX_OVERFLOW (commented)dev · prod · hostedPostgreSQL connection pool (defaults 10 / 20).
POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD (commented)dev only (--profile postgres)Credentials of the bundled dev PostgreSQL container.

Logs, backups, retention

KeyForwarded byWhat it does
LOG_RETENTION_DAYSdev · prod · hostedDays of raw Vector logs kept before archival/deletion (default 5).
LOG_ARCHIVE_ENABLEDdev · prod · hostedCompress logs to /data/archives instead of deleting (default true).
LOG_CLEANUP_HOURdev · prod · hostedUTC hour of the daily log cleanup (default 3).
DB_BACKUP_ENABLEDdev · prod · hostedfalse disables the nightly backup and the boot pre-migration copy.
DB_BACKUP_HOUR / DB_BACKUP_MINUTEdev · prod · hostedNightly backup time, UTC (default 03:30).
DB_BACKUP_PG_DUMP_TIMEOUT_SECONDSdev · prod · hostedWall-clock budget for pg_dump (default 1800).
CONTAINER_LOG_MAX_SIZE / CONTAINER_LOG_MAX_FILEdev · prod · hostedDocker log rotation for platform services (default 10m × 3).
AGENT_LOG_MAX_SIZE / AGENT_LOG_MAX_FILEdev · prod · hostedDocker log rotation for agent containers (applied on recreate).
AGENT_TMP_SIZEdev · prod · hostedSize of each agent's RAM-backed /tmp (default 512m; applied on recreate).
AGENT_IDLE_FINALIZE_Sdev · prod · hostedSeconds of stdout silence before a headless turn may finalize early (default 300; applied on recreate).

MCP server

KeyForwarded byWhat it does
MCP_AGENT_CHAT_PULL_ENABLEDdev · prod · hostedExperimental: route agent-to-agent chat through the async task path.
MCP_INLINE_AUTH_ENABLEDdev · prod · hostedKeyless email-code sign-in over MCP (default off; expose the MCP port over TLS only when on).
MCP_INLINE_AUTH_TIMEOUT_MSdev · prod · hostedRelay timeout for the inline-auth path.
MCP_CHAT_TIMEOUT_MS / MCP_RECOVERY_TIMEOUT_MSdev · prod · hostedSync-chat ceiling and the post-abort execution lookup budget.
MCP_A2A_TIMEOUT_MSdev · prod · hostedOutbound A2A fetch timeout.
ASK_TRINITY_ENDPOINT (commented)dev · prod · hostedEndpoint behind the ask_trinity docs-Q&A tool.

Execution, dispatch, reliability

KeyForwarded byWhat it does
PULL_MODE_PILOT_AGENTS / MAX_REDELIVERYdev · prod · hostedPull-mode pilot agents (empty = off) and the poison-task redelivery cap.
DISPATCH_ASYNCdev · prod · hostedFire-and-forget dispatch of schedule/webhook turns (default false).
BACKEND_AGENT_CALL_LIMIT / BACKEND_AGENT_CALL_QUEUE_TIMEOUT_Sdev · prod · hostedConcurrent outbound agent calls per backend worker and the queue wait ceiling.
DISPATCH_BREAKER_ENABLEDdev · prod · hostedGlobal gate for the per-agent dispatch circuit breaker (default false).
SUBSCRIPTION_SWEEP_CONCURRENCYdev · prod · hostedParallel probes in the subscription-headroom sweep.
REDELIVERY_GOVERNOR_ENABLED / REDELIVERY_FLEET_LIMIT / REDELIVERY_FLEET_WINDOW_SECONDS / REDELIVERY_AGENT_LIMIT / REDELIVERY_AGENT_WINDOW_SECONDS / CORRELATED_FAILURE_THRESHOLD / CORRELATED_FAILURE_WINDOW_SECONDS / CORRELATED_PAUSE_TTL_SECONDS / REDELIVERY_PAUSE_RETRY_AFTER_SECONDSdev · prod · hostedRe-delivery governor (default off) and its caps.

Observability

KeyForwarded byWhat it does
OTEL_ENABLEDdev · prod · hostedClaude Code metrics export from agents (default 1).
OTEL_COLLECTOR_ENDPOINTdev · prod · hostedCollector endpoint (default the bundled trinity-otel-collector).
OTEL_METRICS_EXPORTER / OTEL_LOGS_EXPORTER / OTEL_EXPORTER_OTLP_PROTOCOL / OTEL_METRIC_EXPORT_INTERVALdev · prod · hostedExporter settings.
TELEMETRY_CONTAINER_STATS_TTL / TELEMETRY_DOCKER_POOL_SIZEdev · prod · hostedContainer-stats cache freshness and Docker fetch parallelism.
CANARY_ENABLED / CANARY_SLACK_WEBHOOK_URLdev · prod · hostedContinuous invariant watcher (staging/dev) and its Slack webhook.
SYNC_HEALTH_POLL_INTERVAL_SECONDSdev · prod · hostedSeconds between git sync-health polls of each git-enabled agent (default 60). Each poll runs a git fetch inside the agent, so raise it to cut that load on a large fleet. An invalid or non-positive value falls back to 60.