Browse resources

Run your instance / Install

Docker and Compose

Run the public image with Docker Compose or plain docker run, pin a build, and upgrade.

knotel ships as one public image, ghcr.io/kien-ngo/knotel, for linux/amd64 and linux/arm64. It holds the UI, the OTLP ingest endpoints, the browser script and the MCP server, and needs a ClickHouse server to store spans, logs and web events in. Nothing needs to be cloned or built: pull the image and run it.

There are two ways to run it. Docker Compose starts knotel and ClickHouse together from one file, and is what most instances should use. Plain Docker runs the same two containers with docker run, or the app alone next to a ClickHouse you already have.

What you need

  • Docker 24 or newer, and for the Compose route the Compose plugin 2.23 or newer (docker compose version). Older Compose versions don't read the inline configs the file below uses.
  • A machine with 2-4 vCPU and 4-8 GB of RAM handles millions of spans a day. See Usage and costs.
  • Ports 3000 (UI and OTLP/HTTP) and, if you want OTLP/gRPC, 4317.

Docker Compose

1. Write the Compose file

Make a directory for the instance, such as ~/knotel, and save this as compose.yml in it. ClickHouse's settings are inline, so this one file is the whole stack.

compose.yml
services:
  knotel:
    image: ghcr.io/kien-ngo/knotel:${KNOTEL_TAG:-latest}
    restart: unless-stopped
    env_file: .env
    environment:
      CLICKHOUSE_URL: http://clickhouse:8123
      CLICKHOUSE_USER: knotel
      CLICKHOUSE_DB: knotel
    ports:
      - "${HOST_PORT:-3000}:3000"
      # OTLP/gRPC, for SDKs and Collectors left on their default protocol.
      - "${GRPC_HOST_PORT:-4317}:4317"
    volumes:
      - knotel-data:/data
    depends_on:
      clickhouse:
        condition: service_healthy

  clickhouse:
    image: clickhouse/clickhouse-server:26.8
    restart: unless-stopped
    environment:
      CLICKHOUSE_USER: knotel
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?set CLICKHOUSE_PASSWORD in .env}
      CLICKHOUSE_DB: knotel
      CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1"
    volumes:
      - clickhouse-data:/var/lib/clickhouse
    configs:
      - source: clickhouse-server
        target: /etc/clickhouse-server/config.d/knotel.xml
      - source: clickhouse-users
        target: /etc/clickhouse-server/users.d/knotel.xml
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8123/ping"]
      interval: 5s
      timeout: 3s
      retries: 30

configs:
  # Keeps ClickHouse from logging every query and part into its own system
  # tables, which on a small server grow faster than the data does.
  clickhouse-server:
    content: |
      <clickhouse>
        <logger><level>warning</level><console>true</console></logger>
        <query_log remove="remove"/>
        <query_thread_log remove="remove"/>
        <query_views_log remove="remove"/>
        <part_log remove="remove"/>
        <trace_log remove="remove"/>
        <text_log remove="remove"/>
        <metric_log remove="remove"/>
        <asynchronous_metric_log remove="remove"/>
        <session_log remove="remove"/>
        <processors_profile_log remove="remove"/>
        <opentelemetry_span_log remove="remove"/>
        <error_log remove="remove"/>
        <latency_log remove="remove"/>
        <blob_storage_log remove="remove"/>
        <background_schedule_pool_log remove="remove"/>
      </clickhouse>
  clickhouse-users:
    content: |
      <clickhouse>
        <profiles><default>
          <log_queries>0</log_queries>
          <log_query_threads>0</log_query_threads>
        </default></profiles>
      </clickhouse>

volumes:
  knotel-data:
  clickhouse-data:

Each container keeps its data in a named volume: knotel-data holds the SQLite file with accounts, projects and keys, clickhouse-data holds the telemetry. ClickHouse's port isn't published; only the knotel container reaches it.

2. Set the secrets

Next to it, create .env. Only the first two values are required:

.env
# Required
BETTER_AUTH_SECRET=     # openssl rand -base64 32
CLICKHOUSE_PASSWORD=    # openssl rand -hex 24

# Optional
# KNOTEL_TAG=sha-c181485              # pin a build; see "Tags" below
# SETUP_TOKEN=a-code-only-you-know    # else a one-time code is logged
# PUBLIC_URL=https://knotel.example.com
# HOST_PORT=3000
# GRPC_HOST_PORT=4317
Or generate both at once
printf 'BETTER_AUTH_SECRET=%s\nCLICKHOUSE_PASSWORD=%s\n' \
  "$(openssl rand -base64 32)" "$(openssl rand -hex 24)" > .env

BETTER_AUTH_SECRET signs sign-in sessions; changing it later signs everyone out. CLICKHOUSE_PASSWORD is read by both containers. Every other setting is optional; the full list is in Configuration, and any of them can go in the same .env.

3. Start it

docker compose up -d
docker compose logs knotel    # the one-time setup code, if SETUP_TOKEN isn't set

The first start pulls both images, waits for ClickHouse to report healthy, then runs the migrations for both stores. Open http://localhost:3000/setup (or the machine's address), enter the setup code and create the owner account. From there, Getting started walks through creating a project and sending the first trace.

Day to day

CommandWhat it does
docker compose psState and health of both containers
docker compose logs -f knotelFollow the app's log
docker compose restart knotelRestart the app, e.g. after editing .env
docker compose downStop and remove the containers. The volumes, and so the data, stay
docker compose down -vAlso delete the volumes: every account, project and span
Editing .env
Containers read .env when they are created, so after changing it run docker compose up -d, which recreates whatever changed.restart alone keeps the old values.

Plain Docker

knotel and ClickHouse

The same stack without Compose: a network for the two containers to find each other by name, a volume each, and the same .env as above. ClickHouse is started without the settings file, which is fine for trying knotel out; for a long-lived instance, use Compose or mount the two XML files from the Compose file above into /etc/clickhouse-server/config.d/ and /etc/clickhouse-server/users.d/.

set -a; . ./.env; set +a      # read CLICKHOUSE_PASSWORD from .env

docker network create knotel

docker run -d --name knotel-clickhouse --network knotel --restart unless-stopped \
  --ulimit nofile=262144:262144 \
  -e CLICKHOUSE_USER=knotel -e CLICKHOUSE_PASSWORD="$CLICKHOUSE_PASSWORD" \
  -e CLICKHOUSE_DB=knotel -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \
  -v knotel-clickhouse:/var/lib/clickhouse \
  clickhouse/clickhouse-server:26.8

docker run -d --name knotel --network knotel --restart unless-stopped \
  -p 3000:3000 -p 4317:4317 \
  --env-file .env \
  -e CLICKHOUSE_URL=http://knotel-clickhouse:8123 \
  -e CLICKHOUSE_USER=knotel -e CLICKHOUSE_DB=knotel \
  -v knotel-data:/data \
  ghcr.io/kien-ngo/knotel:latest

docker logs knotel             # the one-time setup code

Until ClickHouse is up, requests to knotel fail; the first one after that runs the migrations, so give it a few seconds. With Compose, the health check does that waiting for you.

The app alone, with your own ClickHouse

Point CLICKHOUSE_URL, CLICKHOUSE_USER and CLICKHOUSE_PASSWORD at your server (in .env or as -e flags) and run only the app:

docker run -d --name knotel --restart unless-stopped \
  -p 3000:3000 -p 4317:4317 \
  --env-file .env \
  -v knotel-data:/data \
  ghcr.io/kien-ngo/knotel:latest

knotel creates CLICKHOUSE_DB (knotel by default) if it doesn't exist, then its tables, so the user needs to be allowed to create that database, and to insert, select, alter and delete in it. ClickHouse Cloud works the same way, with its https://…:8443 address as CLICKHOUSE_URL.

Keep the /data volume
It holds knotel.db: accounts, projects, ingest keys and alerts. Run the container without it and every restart is a new, unclaimed instance, with keys your senders no longer match.

Tags

TagWhat it is
latestThe current build. Moves on its own
mainThe newest commit on main. Moves on every push
sha-<commit>One build, forever. What to pin a production instance to

Pin a sha- tag for anything you depend on, so an instance only changes when you change it. In the Compose file above, that's KNOTEL_TAG=sha-<commit> in .env. The running build is at /api/version, and the changelog lists each build with the commit it reports.

Upgrading

# Compose: set a new KNOTEL_TAG in .env if you pin one, then
docker compose pull knotel && docker compose up -d

# Plain Docker
docker pull ghcr.io/kien-ngo/knotel:latest
docker rm -f knotel && docker run -d --name knotel …   # the same flags as before

Migrations run when the new container starts, once each; there is no separate step. They don't run backwards, so before rolling back across a release that carries one, read Upgrading. Back up first: Backups covers both stores.

Next steps