Self Host
Minimal Container

Minimal Container

Lightweight Alpine image (~20-30MB) containing only the Go backend with the embedded frontend. Requires external ClickHouse and PostgreSQL instances.

Quick Start

Pull the pre-built image and run it pointing at your databases:

docker pull ghcr.io/tracewayapp/traceway:minimal
 
docker run -d --name traceway \
  -p 80:80 \
  -v traceway-storage:/app/storage \
  -e CLICKHOUSE_SERVER="clickhouse-host:9000" \
  -e CLICKHOUSE_DATABASE="traceway" \
  -e CLICKHOUSE_USERNAME="default" \
  -e CLICKHOUSE_PASSWORD="your-password" \
  -e POSTGRES_HOST="postgres-host" \
  -e POSTGRES_PORT="5432" \
  -e POSTGRES_DATABASE="traceway" \
  -e POSTGRES_USERNAME="traceway" \
  -e POSTGRES_PASSWORD="your-password" \
  -e POSTGRES_SSLMODE="disable" \
  -e JWT_SECRET="your-jwt-secret-min-32-characters-long" \
  -e APP_BASE_URL="https://traceway.example.com" \
  ghcr.io/tracewayapp/traceway:minimal

After starting, open http://localhost/register to create your first account.

The traceway-storage volume holds blob storage: uploaded source maps, session recordings, and AI traces. Without it those files are lost when the container is recreated. To use S3 instead of a volume, see Blob Storage.

Build from Source

Only needed if you want to run a custom or modified build:

docker build -f Dockerfile.minimal -t traceway:minimal .

Then use traceway:minimal instead of ghcr.io/tracewayapp/traceway:minimal in the command above.

Required Environment Variables

VariableDescriptionExample
CLICKHOUSE_SERVERClickHouse host:portclickhouse:9000
CLICKHOUSE_DATABASEClickHouse databasetraceway
CLICKHOUSE_USERNAMEClickHouse usernamedefault
CLICKHOUSE_PASSWORDClickHouse passwordpassword
CLICKHOUSE_TLSEnable TLSfalse
POSTGRES_HOSTPostgreSQL hostpostgres
POSTGRES_PORTPostgreSQL port5432
POSTGRES_DATABASEPostgreSQL databasetraceway
POSTGRES_USERNAMEPostgreSQL usernametraceway
POSTGRES_PASSWORDPostgreSQL passwordpassword
POSTGRES_SSLMODEPostgreSQL SSL modedisable
JWT_SECRETSigns all authentication tokens (dashboard sessions and CLI/MCP device logins). Use a strong, unique secret of at least 32 chars; keep it stable. See CLI Authentication.your-secret-here
APP_BASE_URLPublic URL of your instance. Used for SDK setup instructions, email links, deep links in notifications, OAuth callback URLs, and the CLI/MCP OAuth issuer plus device-login verification URL. If unset, derived per-request from Host / X-Forwarded-*.https://traceway.example.com
TRUSTED_PROXIESComma-separated CIDRs of the proxies in front of Traceway whose X-Forwarded-For / X-Real-IP is trusted for the real client IP. Every per-IP rate limit keys on it, and /api/report stores it as each session's client.ip. Unset trusts loopback only, so a proxy container on a Docker network or an in-cluster ingress has to be listed before its header counts. A value replaces the default, so list every hop between the client and this server, not just the outermost one: naming only a CDN leaves a private ingress untrusted and turns X-Forwarded-For off entirely. * trusts every peer and lets clients spoof their address. Until the proxy is listed, every visitor shares one rate-limit bucket keyed on the proxy, and the server logs a warning the first time it sees a forwarding header from a peer it does not trust.10.0.0.0/8,192.168.0.0/16
TRUSTED_PROXY_HEADERA header taken as the client IP on every request, ahead of TRUSTED_PROXIES: X-Real-IP behind ingress-nginx, CF-Connecting-IP behind Cloudflare. Only safe when every request provably passes through a proxy that overwrites the header; a directly reachable backend lets clients set it freely.X-Real-IP
REPORT_MAX_BODY_MBCap on the decompressed /api/report and /api/profiles/ingest body in megabytes. Gzip does not raise it. A body over the cap answers 413.64
INGEST_MAX_CONCURRENTTelemetry ingest requests processed at once (/api/report, /api/profiles/ingest, /api/otel/*). Defaults to 2 x CPU cores with a floor of 4. Excess requests wait up to INGEST_ADMISSION_WAIT_SECONDS (default 5) and then get 503 with Retry-After; past a bounded waiting room they are turned away immediately. Each admitted request can hold a decoded body, so on a memory-capped container size this from the memory limit.8
UPLOAD_MAX_CONCURRENTSource map and symbol uploads processed at once. Each holds a whole file in memory while it is parsed. Excess uploads wait 30 seconds and then get 503. Separate from the ingest pool so a CI burst cannot starve telemetry.4
STORAGE_TYPEBlob storage backend for source maps, session recordings, and AI traces: local (default) or s3. See Blob Storage for the S3 variables.local
STORAGE_PATHFolder for local blob storage. Defaults to ./storage, which is /app/storage in this image. Mount a volume there. Ignored when STORAGE_TYPE=s3./app/storage
SESSION_RECORDING_RETENTION_DAYSDays to keep on-disk session recordings under STORAGE_PATH/recordings/. Defaults to 30. Worker runs hourly and on startup. 0 disables; no effect when STORAGE_TYPE=s3.30

Running more than one instance

This image is the only one that supports several backend instances behind a load balancer, because everything the instances have to agree on lives in PostgreSQL and ClickHouse:

  • Project tokens. Each instance keeps the project list in memory to validate SDK tokens. Creating, updating or deleting a project, or rotating a source map token, sends a PostgreSQL NOTIFY on the project_cache_changed channel inside the write transaction, and every instance listening on that channel reloads the list. An instance that lost its database connection reloads the list as soon as the connection is back, so a change made while it was disconnected is not missed.
  • Background workers. The notification outbox, the on-call escalator, the synthetics scheduler and the one-time dashboards backfill take a PostgreSQL advisory lock per tick, so only one instance drains the queue or pages someone even though every instance runs the workers.

What the deployment needs:

  • Every instance points at the same PostgreSQL and ClickHouse and carries the same JWT_SECRET, so a session issued by one instance is valid on all of them.
  • STORAGE_TYPE=s3. With local storage, a source map uploaded through one instance exists only on that instance's disk. See Blob Storage.
  • TRUSTED_PROXIES lists the load balancer. Otherwise every instance sees the balancer's address as the client and all visitors share one rate-limit bucket.
  • Migrations run at startup. Roll instances out one at a time so a single instance applies them before the others start.

Two caches stay per instance: the source map artifact cache and the symbolicator's negative cache, which retries a failed artifact within at most 15 minutes on the instances that did not receive the upload. See Symbolicator performance.

The SQLite and DuckDB images are single-instance by design: their databases are local files, and their project cache has no way to learn about a write made by another process. The All-in-One image bundles both databases inside the container, so it is single-instance as well. Run exactly one container of those images per data volume.

SSO (optional)

Adds Continue with Google / Continue with GitHub buttons to the login and register pages. See the SSO guide for the full provider setup walkthrough.

When configuring providers, set the callback URL on the provider side to <APP_BASE_URL>/api/auth/callback/{google|github}.

VariableDescriptionExample
GOOGLE_CLIENT_IDGoogle OAuth client ID. Setting both Google variables enables the Google button....apps.googleusercontent.com
GOOGLE_CLIENT_SECRETGoogle OAuth client secret.GOCSPX-...
GITHUB_CLIENT_IDGitHub OAuth App client ID. Setting both GitHub variables enables the GitHub button.Ov23li...
GITHUB_CLIENT_SECRETGitHub OAuth App client secret.(40-char hex)
OAUTH_SESSION_SECRETCookie signing secret for the OAuth round-trip. Falls back to JWT_SECRET when unset.(32+ random bytes)

Access Points

URLDescription
http://localhost/Frontend dashboard
http://localhost/api/*Backend API
http://localhost/healthHealth check

Useful Commands

# View logs
docker logs traceway
docker logs -f traceway
 
# Enter container shell
docker exec -it traceway sh
 
# Health check
curl http://localhost/health
 
# Stop and remove
docker stop traceway && docker rm traceway