Skip to main content
Trinity

Local Development

Run Trinity on your own machine for development and experimentation. All services run in Docker; nothing is installed on the host except Docker and Git.

Prerequisites

  • •Docker Desktop (includes Docker Compose v2) — version 24 or later recommended. On Linux, Docker Engine plus the docker compose plugin (docker-compose v1 is not supported).
  • •Git
  • •openssl on your PATH (comes with macOS and most Linux distributions)
  • •8 GB RAM available to Docker

1. Clone the Repository

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

2. Configure .env

cp .env.example .env

The only edit a local install needs is ADMIN_PASSWORD. Everything else below is either generated by start.sh or optional.

Required for a working dev boot

VariableWhere usedHow to set
ADMIN_PASSWORDBackend — admin login; MCP server — legacy password authChoose a strong password (12+ characters). start.sh refuses to start while it is blank; under --unattended it generates one and prints it once (an ADMIN_PASSWORD exported in your shell is written into .env when the file has none). The backend re-applies this value on every boot, so change the admin password here and recreate the backend container (docker compose up -d backend) — a plain restart does not re-read .env.
SECRET_KEYBackend — JWT signingGenerated by start.sh if blank
INTERNAL_API_SECRETScheduler-to-backend callsGenerated by start.sh if blank (falls back to SECRET_KEY only if you bring the stack up without the script)
CREDENTIAL_ENCRYPTION_KEYBackend — encrypts OAuth tokens, channel bot tokens, subscription credentialsGenerated by start.sh if blank; do not change once set
AGENT_AUTH_SECRETBackend — derives each agent's in-container auth tokenGenerated by start.sh if blank; do not change once set — every running agent's token stops working until the agent is recreated
REDIS_PASSWORDRedis default ACL userGenerated by start.sh on a fresh install; see note below
REDIS_BACKEND_PASSWORDRedis backend and scheduler ACL users; embedded in REDIS_URLGenerated by start.sh on a fresh install; see note below
DOCKER_GIDBackend — group of /var/run/docker.sock inside the containerDetected by start.sh by probing the socket from a throwaway container (Docker Desktop, Colima and rootless report 0; a native Linux daemon reports the host docker group). Set it yourself only to override.

Redis passwords and start.sh: On a fresh install (no redis-data volume), start.sh fills in missing REDIS_PASSWORD / REDIS_BACKEND_PASSWORD values. If the volume already exists and the passwords are missing, the script exits with an error and points you at the migration guide — re-keying a populated Redis would lock the platform out of its own data.

CREDENTIAL_ENCRYPTION_KEY: once generated, do not change or delete it — all encrypted credentials (channel bot tokens, OAuth secrets, subscription credentials) become unrecoverable if the key changes.

Optional but recommended

VariablePurpose
ANTHROPIC_API_KEYLets agents use Claude. Can be left blank and configured in Settings after login; start.sh warns in its summary when no model key (ANTHROPIC_API_KEY, GOOGLE_API_KEY or CLAUDE_CODE_OAUTH_TOKEN) is set.
EMAIL_PROVIDERHow verification codes are sent. .env.example sets console (codes print to the backend log); the compose default when the line is absent is resend.
RESEND_API_KEYRequired if EMAIL_PROVIDER=resend.
GITHUB_PATForwarded to the backend; needed to clone private template repos.
GEMINI_API_KEYPlatform image generation, voice.
SLACK_SIGNING_SECRETRequired only if you configure the Slack channel adapter.
PUBLIC_CHAT_URLPublic-facing URL for chat links and webhooks. Leave empty for local dev.
FRONTEND_URLUsed for OAuth redirect callbacks. Leave empty for local dev.
TRINITY_DEFAULT_SYSTEM_MANIFESTSet to disabled to skip the starter fleet a fresh install seeds (see First-Time Setup).

Keys the dev compose does not forward

Every key in .env.example is forwarded by docker-compose.yml except these, which only the production and hosted compose files read: SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, NOTION_CLIENT_ID, NOTION_CLIENT_SECRET, TUNNEL_TOKEN, TRINITY_DATA_PATH, HOST_TEMPLATES_PATH. TRINITY_IMAGE_TAG and ADMIN_PASSWORD_SOURCE are read only by the hosted compose, and the VITE_* keys are build arguments of the production frontend image (for the dev Vite server, set them in src/frontend/.env instead). Setting any of these in a dev .env does nothing. The complete key-by-key reference lives in Single-Server Deployment → .env reference.

Port conflict

The frontend binds port 80 by default. If another process already holds :80, add FRONTEND_PORT=8090 (or any free port) to .env. start.shprobes the web UI port, 8000, 8080 and 6379 before starting and prints a warning if any is busy (a re-run over a running Trinity is expected to hit this — those are Trinity's own containers).

3. Build the Base Agent Image

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

This builds trinity-agent-base:latest — the Docker image every agent container inherits. It includes Python 3.13, Node.js 20, Go 1.23, and Claude Code. The platform can start without this, but you cannot create any agents until the image exists.

The image is tagged both trinity-agent-base:latest and trinity-agent-base:<VERSION> (read from the VERSION file). First build takes 5–10 minutes.

start.sh detects a missing base image and calls build-base-image.sh before starting services. You can skip this step if you prefer. (start.sh --hosted pulls the published image from GHCR instead of building — see Single-Server Deployment.)

4. Start Services

./scripts/deploy/start.sh

./quickstart.sh is an alias for the same script (--defaults is translated to --unattended; every other flag passes through).

The script does the following, in order:

  1. 1Pre-flight. Exits with one consolidated message if the Docker daemon is unreachable or Compose v2 is missing. Warns (does not stop) if the web UI port, 8000, 8080 or 6379 is already in use.
  2. 2If .env does not exist, copies .env.example to .env.
  3. 3Generates CREDENTIAL_ENCRYPTION_KEY, SECRET_KEY, INTERNAL_API_SECRET and AGENT_AUTH_SECRET if blank.
  4. 4Checks ADMIN_PASSWORD: if .env has none but your shell exports one, writes it in; otherwise blank → exits with an error (interactive) or generates a 24-character password (--unattended). A quoted-empty value (ADMIN_PASSWORD="") counts as blank. (The one exception, ADMIN_PASSWORD_SOURCE=browser, is set only by a marketplace image's first boot and never applies locally.)
  5. 5Notes whether a model API key is present (a warning in the final summary, never a stop).
  6. 6Generates REDIS_PASSWORD and REDIS_BACKEND_PASSWORD if both are blank and no redis-data volume exists yet; if the volume exists and passwords are missing, exits with an error.
  7. 7Creates the data directory used by the prod/hosted bind mount (./trinity-data by default) and, on Linux, makes it owned by UID 1000.
  8. 8Detects DOCKER_GID (see the table above) and writes it to .env.
  9. 9Data-switch guard. Refuses to start if the checkout holds a database this file set cannot see — a trinity-data bind-mount directory written by the prod/hosted stack while you are starting the dev stack, or the reverse under --hosted — and prints the copy command. If both stores exist, it warns which one the stack will use.
  10. 10Checks for trinity-agent-base:latest; builds it if missing (pulls and retags it under --hosted).
  11. 11On Docker Desktop, creates docker-compose.override.yml from docker-compose.override.example.yml so Vector tails log files instead of busy-looping on the Docker socket (opt out with TRINITY_LOCAL_LOG_SOURCE=docker).
  12. 12Runs docker compose up -d.
  13. 13Polls http://localhost:8000/health for up to 180 seconds — the backend answers only after migrations and startup complete — then prints the access URLs and a next-steps card. A timeout is a warning, not a failure: check docker compose logs -f backend and re-run the script once it settles.

One-shot / unattended install: pass --unattended (or set TRINITY_UNATTENDED=1) — ./scripts/deploy/start.sh --unattended — and the happy path never blocks on a prompt. It generates ADMIN_PASSWORD (and any missing secrets) and prints the admin password in the final summary. Save it — it is stored in .env and shown only once.

Let an AI agent install it for you: an AI coding agent (Claude Code) can run the whole local install by following the deterministic runbook at docs/AGENT_INSTALL_GUIDE.md. Point your agent at it and it drives the steps above end to end.

Once complete, the platform services are running:

ServiceContainerPort
Backend (FastAPI)trinity-backend8000
Frontend (Vite dev server)trinity-frontend80 (or $FRONTEND_PORT)
MCP Servertrinity-mcp-server8080
Schedulertrinity-scheduler8001 inside the container (health only; not published to the host)
Redistrinity-redis127.0.0.1:6379 (loopback only)
Vector (logs)trinity-vector8686
OTel collectortrinity-otel-collector4317, 4318, 8889, 13133

Two one-shot containers (trinity-logs-init, trinity-archives-init) prepare the log directories and exit; they are expected to show as exited.

5. Open the Web UI

Navigate to http://localhost and log in with username admin and the ADMIN_PASSWORD from .env. There is no setup screen: setting ADMIN_PASSWORD provisions the admin account at boot. The Dashboard then opens the first-run setup sequence — Connect Claude (a subscription token or API key) is its one required step; the rest can be skipped and picked up later from Settings → General → Re-run setup. See First-Time Setup.

URLPurpose
http://localhostWeb UI
http://localhost:8000/docsBackend API (Swagger)
http://localhost:8080/mcpMCP Server endpoint (the dev Vite server does not proxy /mcp; that route exists on the production nginx only)
http://localhost:8686/healthVector log aggregator health

Hot Reload

The dev compose mounts source trees into the containers:

  • •Backend: ./src/backend is bind-mounted inside trinity-backend. Uvicorn runs with --reload, so any change to a .py file restarts the backend worker automatically.
  • •Frontend: ./src/frontend is bind-mounted into trinity-frontend. Vite watches for .vue and .js changes and hot-reloads automatically.

If you add or remove a Python dependency or change package.json, rebuild the affected image:

docker compose build backend   # after requirements.txt change
docker compose build frontend  # after package.json change
docker compose up -d backend   # restart after rebuild

Viewing Logs

# All services
docker compose logs -f

# One service
docker compose logs -f backend
docker compose logs -f frontend

# Structured platform logs via Vector
docker exec trinity-vector sh -c "tail -50 /data/logs/platform.json" | jq .

# Structured agent logs via Vector
docker exec trinity-vector sh -c "tail -50 /data/logs/agents.json" | jq .

Stopping Services

./scripts/deploy/stop.sh

stop.sh runs docker compose stop — containers and the agent network stay in place, and start.sh brings the same containers back. It reads the running stack's compose label and adds -f docker-compose.hosted.yml when the stack was started with --hosted.

Never use docker compose down on a Trinity checkout you want to keep. down removes the trinity-agent-network; agent containers are created with Docker's unless-stopped restart policy, so after a down they loop in Restarting against a network that no longer exists. Recovery is docker compose up -d (recreates the network), then docker rm -f each stale agent container and start it again from the UI — the workspace volume is untouched.

Data Persistence

All platform state is stored in Docker named volumes. These survive a docker compose stop / start cycle, and even docker compose down (unless you pass -v).

VolumeContents
trinity_trinity-datatrinity.db — agents, schedules, chat history, credentials metadata — plus backups/ (automatic database backups)
trinity_redis-dataRedis AOF journal
trinity_agent-configsAgent configuration files
trinity_trinity-logsVector log aggregation output
trinity_trinity-archivesCompressed log archives (when LOG_ARCHIVE_ENABLED is on)

The volume name prefix trinity_ comes from the Compose project name (the directory name, lowercased). start.sh derives the same name compose does, so a checkout called trinity-dev or project_trinity gets trinity-dev_trinity-data / project_trinity_trinity-data.

Optional: PostgreSQL Backend

SQLite is the zero-config default for local development, but the dev compose ships a bundled PostgreSQL container behind a profile. Note: SQLite support ends 2026-09-01 — production instances should run PostgreSQL (see the Single-Server guide).

# In .env:
#   POSTGRES_PASSWORD=your-postgres-password
#   DATABASE_URL=postgresql://trinity:your-postgres-password@postgres:5432/trinity
docker compose --profile postgres up -d

The host in the URL is the compose service name postgres (Docker DNS). POSTGRES_DB, POSTGRES_USER and POSTGRES_PASSWORD are read by the dev compose only (the bundled container does not exist in the prod or hosted files). The switch is non-destructive — comment DATABASE_URL out and the next restart is back on SQLite (each backend keeps its own data).

Troubleshooting

ModuleNotFoundError on backend startup

A Python dependency was added since your last build. Rebuild:

docker compose build backend && docker compose up -d backend

Port 80 already in use

Add FRONTEND_PORT=8090 to .env, then restart.

Agents can't be created

The base image is missing. Run:

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

Refusing to start: this checkout has a bind-mounted database that the dev stack cannot see

The checkout was last run with the production or hosted file set, whose database lives in the trinity-data directory, not the dev volume. Bring it up the way it was installed (docker compose -f docker-compose.prod.yml up -d or ./scripts/deploy/start.sh --hosted), or copy the data into the dev volume with the command the message prints. To start fresh on purpose, move the directory aside.

Redis password errors on startup

Passwords changed after the redis-data volume was created. See docs/migrations/REDIS_AUTH.md for the upgrade path.

Vector pegging the CPU on Docker Desktop

The on-disk log source override was not applied. Run start.sh again (it creates docker-compose.override.yml), or copy docker-compose.override.example.yml to docker-compose.override.yml yourself.