Docker Compose
Run the ingest service and ClickHouse with Docker Compose.
The runtime stack is intentionally simple: one stateless ingest service and one ClickHouse database. The browser SDK and React package are built into your application bundle; backend services call the ingest API directly.
For production platform examples, see Helm deployment and Railway deployment.
Local Stack
cp .env.example .env
docker compose up -dThe Compose stack starts:
- ClickHouse on
http://127.0.0.1:8123 - ingest service on
http://127.0.0.1:8080
docker-compose.yml pins ClickHouse to clickhouse/clickhouse-server:26.3.9.8-alpine and runs the prebuilt ghcr.io/marcklingen/clickhouse-product-analytics/ingest-service:latest image. It does not build from the local checkout. Keep concrete image tags instead of floating tags when you need reproducible deployment manifests, and pin by digest for registry-level reproducibility.
Stop the local stack when finished:
docker compose downBuild From Source
Use docker-compose-build.yml for CI, contribution checks, and local validation of ingest service changes from this checkout:
npm install
npm run build:packages
docker compose -f docker-compose-build.yml up -d --build
npm run verify:e2e
docker compose -f docker-compose-build.yml down -v --remove-orphansdocker-compose-build.yml keeps the same ClickHouse, environment, ports, health checks, and migration-on-start behavior as the default stack, but replaces the GHCR ingest image with build.context: . and packages/ingest-service/Dockerfile.
Production Shape
Run the ingest service as a container close to ClickHouse. Put it behind a TLS-terminating reverse proxy or load balancer. Browser applications talk to the public HTTPS ingest URL. Backend services can use the same public URL or a private network URL.
Recommended production model:
- one or more ingest service containers,
- shared ClickHouse database on a supported stable or LTS ClickHouse release,
- stable
PUBLIC_API_KEYSwhen backend or no-origin requests are used, - explicit
ALLOWED_ORIGINS, LOG_LEVEL=warnor stricter for high-volume traffic,- migrations run manually before deploy,
MIGRATE_ON_START=falseor omitted in production.
The ingest service does not require Redis, Postgres, Kafka, object storage, or a background worker.
Container Image
The published ingest service image is:
ghcr.io/marcklingen/clickhouse-product-analytics/ingest-service:sha-<commit>
ghcr.io/marcklingen/clickhouse-product-analytics/ingest-service:main
ghcr.io/marcklingen/clickhouse-product-analytics/ingest-service:latestEach published tag is a multi-platform image for linux/amd64 and linux/arm64.
Use the sha-<commit> tag for production rollouts, or the image digest reported by GHCR after the publish job completes. The main tag follows the latest successful main publish. The mutable latest tag is useful for quick trials but should not be the rollout target for a stable environment. For the strictest rollout, deploy ghcr.io/marcklingen/clickhouse-product-analytics/ingest-service@sha256:<digest>.
ClickHouse Cloud
ClickHouse Cloud works with the same ingest service configuration. The service uses the official @clickhouse/client package and passes CLICKHOUSE_URL, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD, and CLICKHOUSE_DATABASE directly to the client. No code change is required for a Cloud service as long as the URL is the HTTPS endpoint for the ClickHouse HTTP interface.
Use the connection details from the ClickHouse Cloud service connection menu. For the Node.js or HTTPS connection form, the URL usually has this shape:
CLICKHOUSE_URL=https://<service-host>:8443
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=<clickhouse-cloud-password>
CLICKHOUSE_DATABASE=product_analyticsThen configure the ingest service the same way you would for a self-hosted production deployment:
PUBLIC_API_KEYS=<backend-ingest-key-old>,<backend-ingest-key-new>
ALLOWED_ORIGINS=https://app.example.com
MIGRATE_ON_START=falseRun migrations from a trusted environment that can reach the ClickHouse Cloud service:
CLICKHOUSE_URL=https://<service-host>:8443 \
CLICKHOUSE_USER=default \
CLICKHOUSE_PASSWORD=<clickhouse-cloud-password> \
CLICKHOUSE_DATABASE=product_analytics \
npm run migrateThe browser SDK and React package do not connect to ClickHouse Cloud directly. They should continue to send events to the ingest service apiHost; the ingest service is the only component that needs ClickHouse Cloud credentials.
Environment Variables
| Variable | Required | Default | Purpose |
|---|---|---|---|
HOST | no | 0.0.0.0 | HTTP listen host. |
PORT | no | 8080 | HTTP listen port. |
LOG_LEVEL | no | warn | Service log level. Accepted values are silent, fatal, error, warn, info, debug, and trace. |
PUBLIC_API_KEYS | for no-origin backend ingest | empty | Comma-separated API keys. No-origin backend requests require one of these keys; leave empty to disable no-origin backend ingest. Browser requests from allowed origins can omit api_key; provided keys are still validated. Include old and new keys during rotation. |
ALLOWED_ORIGINS | recommended | empty | Comma-separated browser origins accepted by CORS and source validation. |
MAX_BATCH_BYTES | no | 20971520 | Maximum request body size after decompression. |
MAX_EVENTS_PER_BATCH | no | 10000 | Maximum number of events in a batch request. |
CLICKHOUSE_URL | no | http://localhost:8123 | ClickHouse HTTP endpoint. Use https://<service-host>:8443 for ClickHouse Cloud. |
CLICKHOUSE_USER | no | default | ClickHouse username. |
CLICKHOUSE_PASSWORD | no | empty | ClickHouse password. |
CLICKHOUSE_DATABASE | no | product_analytics | Database used for migrations and queries. |
MIGRATE_ON_START | no | false | Apply migrations when the ingest service starts. Use for local development, not production. |
Migrations
The migration runner applies SQL files from packages/ingest-service/migrations and records applied filenames in schema_migrations. The first migration creates:
eventspersonsperson_distinct_idssessions
Run migrations manually in production:
CLICKHOUSE_URL=https://<service-host>:8443 \
CLICKHOUSE_USER=<user> \
CLICKHOUSE_PASSWORD=<password> \
CLICKHOUSE_DATABASE=product_analytics \
npm run migrateInside the published container image, run the compiled migration entrypoint from the ingest service workdir:
CLICKHOUSE_URL=https://<service-host>:8443 \
CLICKHOUSE_USER=<user> \
CLICKHOUSE_PASSWORD=<password> \
CLICKHOUSE_DATABASE=product_analytics \
node dist/migrate.jsThe runner substitutes {{DATABASE}} with CLICKHOUSE_DATABASE. The database name must be a valid ClickHouse identifier.
CORS and Source Validation
Browser traffic must come from an allowed origin. Configure ALLOWED_ORIGINS with the exact scheme, host, and port used by your app, for example:
ALLOWED_ORIGINS=https://app.example.com,https://www.example.comBackend requests often omit Origin. They are accepted when they send one of the configured PUBLIC_API_KEYS. To disable no-origin backend ingest, leave PUBLIC_API_KEYS empty.
Scaling
The ingest service has no local runtime state and can be scaled horizontally for event capture. All instances must share:
- the same ClickHouse database,
- the same accepted API keys when backend ingest uses them,
- the same origin policy,
- the same request limits.
ClickHouse write capacity is the primary scaling boundary. Keep batch sizes moderate and prefer SDK batching over one request per event for high-volume browser apps.
Identity side effects ($identify, $set, $set_once, and $create_alias) are stored in ClickHouse versioned rows. That model works well for normal SDK traffic, but strict first-write-wins $set_once semantics can race if many replicas process identity updates for the same distinct ID at exactly the same time. For identity-heavy production workloads, either keep identity traffic on one ingest replica or route identity requests consistently until the deployment has been load-tested with your concurrency profile.
ClickHouse Version Upgrades
For local development, update the Compose image only after checking the current stable ClickHouse server tag and running the full verifier. For production, upgrade ClickHouse separately from the ingest service, run migrations against a staging database first, and verify the starter queries in clickhouse-schema.md before promoting the version.