Self-hosting

Docker Compose Quickstart

Run Telmoni on a single host with Docker Compose.

Docker Compose runs a Telmoni instance on a single host, to evaluate it or to serve a small company; it runs every module as one database user, without the per-module roles a production deployment gives them. That user is the database's superuser, which Row-Level Security does not bind, so the database itself does not keep organizations' rows apart here. ClickHouse, the store for your agents' spans, likewise has one user here, which its row policy does not hold. The stack bundles PostgreSQL 17 with pgvector, ClickHouse, Redis 7 Alpine, the automated database migrator, the Rust core backend server, and the Next.js web console.

Prerequisites

Before starting, ensure your host has the following tools installed:


Deployment steps

Obtain the Docker Compose configuration

Clone the Telmoni repository. The compose file works only inside the checkout: it bind-mounts crates/migrator/sql/dev_extensions.sql into Postgres's init directory, which creates the vector extension the agent's tables need when the database volume is first made, and crates/telemetry/clickhouse/low-memory.xml into ClickHouse's configuration, which keeps it small enough to share the host.

git clone https://github.com/telmoni/telmoni.git
cd telmoni

The compose file is deploy/compose/docker-compose.yml.

Generate secrets and configure environment variables

Compose reads .env from the compose file's directory. With docker compose -f deploy/compose/docker-compose.yml, the environment file is deploy/compose/.env. Create it from scratch with the keys shown below, not from the repository's root .env.example: that file is the development stack's. It points mail at a local Mailpit, which this stack does not run, and names hosted model endpoints.

touch deploy/compose/.env

Five values have no default, and the stack does not start without them:

  • SERVICE_SECRET: A shared secret the web console sends to the backend server as the x-service-secret header on every internal call, compared in constant time.
  • AUTH_SECRET: A secret of 32 bytes or more used to seal and decrypt session cookies.
  • POSTGRES_PASSWORD: The password of the stack's one database user. A default in the compose file would be the same on every install.
  • CLICKHOUSE_PASSWORD: The password of ClickHouse's one user. Like POSTGRES_PASSWORD, it goes into a connection URL, so keep to the hex openssl prints.
  • TELEMETRY_KEK: local: followed by 64 hex characters: the key that seals each project's hashing key, which the user ids its spans carry are hashed under before they are stored. A secret to back up beside your database dumps: a dump restored without it leaves every stored user id matching nothing a new key makes.

A sixth, CONNECTOR_KEK, seals connector secrets at rest. The server starts without it unless Slack credentials are configured, but no connector (webhooks included) is offered without it. It has no default: a key shipped in the compose file would be the same on every install, and a database dump would then open every connector's grant.

Generate random values using openssl:

# Generate 32-byte hex strings
openssl rand -hex 32

Update deploy/compose/.env with the generated values and set your initial administrator credentials:

# Secrets: the first five have no default, and the stack does not start without them
SERVICE_SECRET=f3b9c7e2a1d4...      # Output from openssl rand -hex 32
AUTH_SECRET=8e6a1c5d0f2b...         # Output from openssl rand -hex 32
POSTGRES_PASSWORD=4c7d2e9f1a6b...   # Output from openssl rand -hex 32
CLICKHOUSE_PASSWORD=b52e0c9d7f13... # Output from openssl rand -hex 32
TELEMETRY_KEK=local:7c3e...         # local: and the output of openssl rand -hex 32; a secret to back up
CONNECTOR_KEK=local:9a1f...         # local: and the output of openssl rand -hex 32; a secret to back up

# Public application URL: APP_URL and AUTH_URL change with your domain.
# An AUTH_URL that is not an http:// loopback address (localhost,
# 127.0.0.1, [::1]) must be https://, behind your TLS proxy
APP_URL=http://localhost:3000
AUTH_URL=http://localhost:3000

# Proxies in front of the console that append the client's address to
# X-Forwarded-For, which its per-address rate limits read. The compose
# file's default is 1, for the TLS-terminating reverse proxy production
# needs (Production → Reverse proxy configuration); 0 while the console
# is reached directly, which leaves those limits to the caller's own header
TRUSTED_PROXY_HOPS=1

# Database settings: each defaults to telmoni when left out
POSTGRES_USER=telmoni
POSTGRES_DB=telmoni
CLICKHOUSE_USER=telmoni

# Bootstrap initial administrator account (quickstart / setup only)
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your_super_secret_admin_password
AUTH_DISABLE_SIGNUP=true
VERIFY_EMAIL=false

The compose file also passes the server these sign-in settings from this file, each with its default when left out: AUTH_DISABLE_SIGNUP (true, so that only an invited address can sign up; false opens sign-ups), AUTH_DISABLE_USERNAME_PASSWORD (false), AUTH_DOMAINS_WITH_SSO_ENFORCEMENT (empty), AUTH_REDIRECT_URI (http://localhost:3000/auth/callback), and the identity provider's AUTH_CUSTOM_* settings, with the defaults the configuration reference gives, and the default organization every new person joins, TELMONI_DEFAULT_ORG_ID and its project's TELMONI_DEFAULT_PROJECT_ID, with their roles, all empty unless set.

ADMIN_EMAIL and ADMIN_PASSWORD are intended only for initial bootstrapping, local evaluation, and offline setups. For production environments, we strongly recommend configuring an OpenID Connect (OIDC) identity provider and, once you have removed ADMIN_EMAIL and ADMIN_PASSWORD, disabling username/password logins via AUTH_DISABLE_USERNAME_PASSWORD=true. See Production Hardening & OIDC.

Start the Telmoni stack

Run the compose file in detached mode:

docker compose -f deploy/compose/docker-compose.yml up -d

Compose starts the services in dependency order:

  1. Starts postgres, clickhouse and redis, each published on the host's loopback interface only (127.0.0.1:5432, 127.0.0.1:8123 and 127.0.0.1:6379), and waits for their healthchecks.
  2. Once Postgres and ClickHouse are healthy, runs the one-shot migrate container, which applies every schema's DDL, then ClickHouse's database and tables, and exits 0. This quickstart runs every module as the one database user, which owns every table, so the object grants for separate roles are skipped with a note in its log.
  3. Starts the server container once migrate has exited 0. It listens on port 8082 inside the compose network, published on the host's loopback interface alone (127.0.0.1:8082) for the machine lanes — ingest at /otlp/v1/traces, where the SDKs send; the read API under /v1; the CLI's sign-in under /cli/v1; Slack's signed events at /webhooks/v1/slack — which your reverse proxy forwards there by path or on a host of its own (Production → Reverse proxy configuration); an SDK or the CLI on the host itself may use http://localhost:8082 directly. The console's lanes share the port, behind the service secret, and are the console's to call.
  4. Starts the web container once the server container has started (Compose does not wait for the server to answer) and Redis is healthy, publishing port 3000 on every interface of the host, unlike the server, Postgres, ClickHouse and Redis; keep it behind your firewall and TLS proxy.

Verify container status

Check running containers:

docker compose -f deploy/compose/docker-compose.yml ps

Postgres, ClickHouse and Redis run as telmoni-postgres, telmoni-clickhouse and telmoni-redis (pgvector/pgvector:pg17, clickhouse/clickhouse-server:26.8.19.9 and redis:7-alpine), and their healthchecks show them (healthy). ClickHouse is capped at 1.5 GiB of memory, which it takes as its ceiling; raise mem_limit in the compose file on a bigger host. The server and web containers (and ollama when running with --profile agent) take the compose project's name, telmoni-selfhost-<service>-1, and run ghcr.io/telmoni/server:latest and ghcr.io/telmoni/web:latest (built for linux/amd64 only). Each image is also tagged with the full SHA of the commit it was built from; TELMONI_IMAGE_SERVER and TELMONI_IMAGE_WEB in deploy/compose/.env pin one. They have no healthcheck, so they show Up alone. The server image's entrypoint is /app/telmoni, so its command reads /app/telmoni serve.

The one-shot migrate container has exited by then, as telmoni-selfhost-migrate-1 with Exited (0) and the command /app/telmoni migrate. It appears only when listing all containers:

docker compose -f deploy/compose/docker-compose.yml ps -a

Log in to the console

Open your browser and navigate to http://localhost:3000. Log in with the ADMIN_EMAIL and ADMIN_PASSWORD you configured in deploy/compose/.env. At your first login, Telmoni provisions your organization. The seeded account has no name, so the organization is called "My organization", and the console opens on its Overview at http://localhost:3000/my-organization; create the first project from its Projects page or the switcher in the console header. Rename the organization, or change its URL, on its Settings page.


Enabling local AI agent search (optional)

Telmoni includes an AI context agent with vector similarity search. You can run embeddings and chat inference on your own host with Ollama. Its models are downloaded from the internet once, by the pulls below. With the model and embeddings URLs below on the ollama service, every endpoint the agent calls is on the Compose network: the documentation it indexes is this stack's own console's (http://web:3000/llms-full.txt), and the compose file passes no RERANK_URL.

  1. Start the stack with the agent profile:

    docker compose -f deploy/compose/docker-compose.yml --profile agent up -d
  2. Pull the embedding model and the chat model into the persistent ollama_models volume:

    docker compose -f deploy/compose/docker-compose.yml exec ollama ollama pull nomic-embed-text
    docker compose -f deploy/compose/docker-compose.yml exec ollama ollama pull llama3.2
  3. Set the agent environment variables in deploy/compose/.env:

    AGENT_MODEL_PROVIDER=openai
    AGENT_MODEL_URL=http://ollama:11434/v1
    AGENT_MODEL=llama3.2
    EMBEDDINGS_URL=http://ollama:11434/v1
    EMBEDDINGS_MODEL=nomic-embed-text

    To record the agent's own questions as runs of a project of yours, as any agent's are, add TELMONI_API_KEY, an ingest key of that project: the runs go to this stack's own server (Configuration). Unset, they are recorded nowhere.

  4. Recreate the server container to apply the changes (restart keeps the container's old environment):

    docker compose -f deploy/compose/docker-compose.yml --profile agent up -d server

Maintenance commands

Viewing live service logs

# Follow logs for the core backend server
docker compose -f deploy/compose/docker-compose.yml logs -f server

# Follow logs for the web console
docker compose -f deploy/compose/docker-compose.yml logs -f web

Rotating partitions

The audit log uses monthly PostgreSQL partitions, and ClickHouse keeps your spans a day to a partition. Run rotate once a day, from the host's cron: it keeps the audit log's 4-month forward runway, and drops each day of spans once every span in it arrived more than 360 days ago. Without it, no span is ever dropped, and from the fourth month after the first migration every change the audit log records fails, for want of a partition.

docker compose -f deploy/compose/docker-compose.yml run --rm migrate rotate

Upgrading

The Compose file runs the images tagged latest, unless TELMONI_IMAGE_SERVER and TELMONI_IMAGE_WEB name others, and up -d pulls an image only when the host has none, so pull first (with --profile agent on both if you run the agent):

docker compose -f deploy/compose/docker-compose.yml pull
docker compose -f deploy/compose/docker-compose.yml up -d

The migrate service runs the new release's migrations before the server starts.

Stopping the stack

# Stop containers (keeps the Postgres, ClickHouse and Ollama model volumes;
# Redis keeps nothing on disk, so rate-limit windows, signed-out sessions and
# the announcement banner key are lost)
docker compose -f deploy/compose/docker-compose.yml down

# Stop containers and remove all persistent data (destructive)
docker compose -f deploy/compose/docker-compose.yml down -v