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:minimalAfter 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
| Variable | Description | Example |
|---|---|---|
CLICKHOUSE_SERVER | ClickHouse host:port | clickhouse:9000 |
CLICKHOUSE_DATABASE | ClickHouse database | traceway |
CLICKHOUSE_USERNAME | ClickHouse username | default |
CLICKHOUSE_PASSWORD | ClickHouse password | password |
CLICKHOUSE_TLS | Enable TLS | false |
POSTGRES_HOST | PostgreSQL host | postgres |
POSTGRES_PORT | PostgreSQL port | 5432 |
POSTGRES_DATABASE | PostgreSQL database | traceway |
POSTGRES_USERNAME | PostgreSQL username | traceway |
POSTGRES_PASSWORD | PostgreSQL password | password |
POSTGRES_SSLMODE | PostgreSQL SSL mode | disable |
JWT_SECRET | Signs 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_URL | Public 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_PROXIES | Comma-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_HEADER | A 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_MB | Cap 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_CONCURRENT | Telemetry 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_CONCURRENT | Source 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_TYPE | Blob storage backend for source maps, session recordings, and AI traces: local (default) or s3. See Blob Storage for the S3 variables. | local |
STORAGE_PATH | Folder 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_DAYS | Days 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
NOTIFYon theproject_cache_changedchannel 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_PROXIESlists 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}.
| Variable | Description | Example |
|---|---|---|
GOOGLE_CLIENT_ID | Google OAuth client ID. Setting both Google variables enables the Google button. | ...apps.googleusercontent.com |
GOOGLE_CLIENT_SECRET | Google OAuth client secret. | GOCSPX-... |
GITHUB_CLIENT_ID | GitHub OAuth App client ID. Setting both GitHub variables enables the GitHub button. | Ov23li... |
GITHUB_CLIENT_SECRET | GitHub OAuth App client secret. | (40-char hex) |
OAUTH_SESSION_SECRET | Cookie signing secret for the OAuth round-trip. Falls back to JWT_SECRET when unset. | (32+ random bytes) |
Access Points
| URL | Description |
|---|---|
http://localhost/ | Frontend dashboard |
http://localhost/api/* | Backend API |
http://localhost/health | Health 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