Skip to content

Install the server

Build the two server images from this repository, write the server env file, and start the stack with Docker Compose.

The stack has two containers: cctraced (OTLP ingest, sync API, dashboard) and TimescaleDB. Both images are local build tags that no registry publishes, and the compose file sets pull_policy: never, so the stack does not start until you have built both.

Server deployment: published ports and services On the server host, docker compose runs cctraced and TimescaleDB. cctraced publishes OTLP gRPC 4317 and OTLP HTTP 4318 on all interfaces (0.0.0.0) and HTTP 8080 on loopback (127.0.0.1) by default; TimescaleDB publishes 5432 on loopback only. Claude Code sends OTLP gRPC to 4317 and Codex sends OTLP HTTP to 4318; the cctrace client uploads sessions and the browser loads the dashboard over HTTP 8080, which clients on other machines reach only after HTTP_BIND is changed or through the TLS proxy. The optional TLS overlay adds a Caddy service publishing 8443, 5317 and 5318, which proxies to cctraced over the compose network. CLIENT MACHINES SERVER HOST DOCKER COMPOSE proxy Claude Code agent Codex CLI agent cctrace session sync Browser dashboard OTLP gRPC :4317 · 0.0.0.0 OTLP HTTP :4318 · 0.0.0.0 HTTP :8080 127.0.0.1 set HTTP_BIND cctraced server TimescaleDB store 127.0.0.1:5432 Caddy optional TLS :8443 :5317 :5318

Requirements

  • Docker with the Compose plugin
  • A clone of this repository; run every command below from the repository root

1. Build the images

$ docker build -t cctrace/timescaledb-pgmq:latest docker/timescaledb-pgmq/
$ docker build -f deploy/Dockerfile --build-arg UPDATE_SIGNING=optional \
    -t cctrace/cctraced:latest .
  • The first image is TimescaleDB with the pgmq extension.
  • The second builds the dashboard and the cctraced binary inside Docker. UPDATE_SIGNING=optional builds without an update-signing key; the default, required, fails without one.
  • If you set IMAGE_TAG or DB_IMAGE_TAG in the server env file, build with exactly that tag (cctrace/cctraced:<IMAGE_TAG>, cctrace/timescaledb-pgmq:<DB_IMAGE_TAG>). Compose does not fall back to a registry.

2. Write the server env file

Compose reads its settings from .env in the deploy/ directory (the server env file). Copy the example file, then edit the 3 keys below; every other key keeps a working default and its explanatory comment:

$ cp deploy/.env.example deploy/.env
Key Why it is required
JWT_SECRET No default value worth keeping. Compose refuses to start without it, and cctraced exits if it is shorter than 32 bytes. Generate one with openssl rand -hex 32.
LOGS_DIR No default. Compose refuses to start without it. Host directory mounted at /data/logs in the container. Create it in advance.
DB_PASSWORD Ships with the placeholder change-me-strong-password, which you should not keep. Generate one with openssl rand -hex 20.

Edit the 3 keys in deploy/.env with your editor, or in place from the command line:

$ jwt_secret=$(openssl rand -hex 32) && sed -i.bak "s|^JWT_SECRET=.*|JWT_SECRET=${jwt_secret}|" deploy/.env
$ db_password=$(openssl rand -hex 20) && sed -i.bak "s|^DB_PASSWORD=.*|DB_PASSWORD=${db_password}|" deploy/.env
$ LOGS_DIR=/absolute/path/on/the/host/for/logs && mkdir -p "$LOGS_DIR" && sed -i.bak "s|^LOGS_DIR=.*|LOGS_DIR=${LOGS_DIR}|" deploy/.env
$ rm deploy/.env.bak

Decide DB_PASSWORD before the first start

PostgreSQL applies the password only when the data volume is initialised. Changing DB_PASSWORD afterwards leaves the database role on the old password, and cctraced restarts in a loop on password authentication failed. Recovering means removing the database volume, which deletes all collected data.

Set DB_APP_CREDENTIALS before the first start as well. With <user>:<password> there, the first start creates a non-superuser role and cctraced connects as it; left empty, cctraced connects as the database superuser. The role is created only on the first start, and its name must not be cctrace. Generate the password with openssl rand -hex 20, as for DB_PASSWORD: compose writes it into a connection URL as it is, and /, #, ? or % in it break that URL.

$ app_password=$(openssl rand -hex 20) && sed -i.bak "s|^DB_APP_CREDENTIALS=.*|DB_APP_CREDENTIALS=cctrace_app:${app_password}|" deploy/.env
$ rm deploy/.env.bak

See Configuration for the other name rules, for what to do if the first start does not create the role, and for databases already in use.

3. Check what the stack exposes

Before the first start, decide which interfaces the ports bind to.

Port Default host bind Purpose
8080 127.0.0.1 Dashboard and REST API, including client sync
4317 0.0.0.0 (all interfaces) OTLP over gRPC
4318 0.0.0.0 (all interfaces) OTLP over HTTP
5432 127.0.0.1 (fixed) TimescaleDB

OTLP listens on all interfaces without TLS

Ports 4317 and 4318 accept telemetry from other machines, and cctraced does not terminate TLS. On a network you do not control, set GRPC_BIND and OTEL_HTTP_BIND to a loopback or private address in the server env file, restrict the ports with a firewall or VPN, or use the HTTPS overlay.

The dashboard binds to loopback by default. For client machines to sync, they need to reach port 8080: set HTTP_BIND to a reachable address, or put a TLS-terminating proxy in front of it. The full port table is in Ports.

4. Start the stack

$ docker compose --env-file deploy/.env -f deploy/docker-compose.yml up -d

up -d returning cleanly means the containers were created, not that the server started. cctraced reports configuration errors in its own log.

5. Verify

$ docker compose --env-file deploy/.env -f deploy/docker-compose.yml ps
$ curl -fsS http://127.0.0.1:8080/api/health

Terminal showing docker compose ps with both containers healthy and a successful curl to /api/health

  • Both containers should show Up with (healthy). The cctraced health check probes /api/version and has a 30-second start period, so it shows health: starting at first.
  • /api/health answers with "status":"ok" when the server can reach the database, and with HTTP 503 and "status":"unhealthy" when it cannot.
  • If cctraced is Restarting, read its log:
$ docker compose --env-file deploy/.env -f deploy/docker-compose.yml logs cctraced

6. Create the first admin

On a database with no users, cctraced prints a one-time setup token in its log:

[cctraced] initial administrator setup token: <token>

Terminal showing the cctraced log line with the initial administrator setup token, redacted

To choose the token yourself, set CCTRACE_SETUP_TOKEN in the server env file before the first start. The token stops working once the first admin exists. Continue with First admin.

Optional: HTTPS with Caddy

deploy/docker-compose.tls.yml is an opt-in overlay that adds a Caddy container. Caddy terminates TLS for all three listeners and forwards to cctraced over the compose network, using deploy/caddy/Caddyfile. The base compose file is unchanged, and Caddy's image (caddy:2-alpine) is pulled from Docker Hub.

  1. In the server env file, move the plaintext ports to loopback and name the server:

    HTTP_BIND=127.0.0.1
    GRPC_BIND=127.0.0.1
    OTEL_HTTP_BIND=127.0.0.1
    CADDY_SITE_ADDRESS=cctrace.company.example
    

    CADDY_SITE_ADDRESS is required by the overlay. It can be a hostname or the server's IP address. Without the three bind settings, the plaintext ports stay reachable beside the TLS ones.

  2. Start with both compose files:

    $ docker compose --env-file deploy/.env \
        -f deploy/docker-compose.yml -f deploy/docker-compose.tls.yml up -d
    
Channel Plaintext (container) TLS (host default)
Dashboard and REST API 8080 8443
OTLP gRPC 4317 5317
OTLP HTTP 4318 5318

Clients then use https://cctrace.company.example:8443 for the dashboard and sync, and port 5317 or 5318 for OTLP.

Codex sends OTLP/HTTP, so cctrace derives its metrics endpoint from the profile's OTEL endpoint by changing port 5317 to 5318, as it changes 4317 to 4318. With an internal CA, Codex also needs that CA in its configuration; see below and Codex CLI.

Certificates

CADDY_TLS_MODE defaults to internal: Caddy issues certificates from its own CA, which needs no public DNS and works for a bare IP address. Clients must trust that CA. Export it with:

$ docker compose --env-file deploy/.env \
    -f deploy/docker-compose.yml -f deploy/docker-compose.tls.yml \
    exec caddy cat /data/caddy/pki/authorities/local/root.crt > cctrace-ca.crt

Copy the file to each client machine and give it to cctrace once. cctrace init asks for it when an endpoint is https://, or set it directly:

$ cctrace config set server.ca_cert_file ~/cctrace-ca.crt

Give it even if the CA is already in the OS keychain; whether the keychain alone is enough for Claude Code and Codex was not tested. Each client takes its CA from a different place, and cctrace writes the one file to each:

Client Where it reads the CA Who sets it
cctrace system roots plus server.ca_cert_file cctrace
Claude Code NODE_EXTRA_CA_CERTS in its settings file, with the exporter on http/protobuf to 5318 (why) cctrace
Codex tls = { ca-certificate = "..." } in its [otel] section cctrace

A running sync daemon keeps the CA it started with; restart it after setting or clearing the CA (cctrace sync --stop, then start it as usual). Codex's section is rewritten when sync next starts. See Set up the client.

The initial curl download of the client binary does not use this setting; add --cacert cctrace-ca.crt to it. See Install the client.

Set CADDY_TLS_MODE to an ACME account email address (for example ops@example.com) when the name resolves publicly and port 80 is reachable from the internet — the word acme itself makes Caddy refuse to start. Caddy then obtains a publicly trusted certificate, and cctrace and Claude Code need no CA configuration. Codex against a public certificate without server.ca_cert_file was not measured.

Keep the caddy_data volume

It holds the issued certificates and the internal CA. Removing it creates a new CA, and every client that trusted the old one stops connecting.

Next steps