ClickHouse Product Analytics
Deployment

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 -d

The 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 down

Build 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-orphans

docker-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_KEYS when backend or no-origin requests are used,
  • explicit ALLOWED_ORIGINS,
  • LOG_LEVEL=warn or stricter for high-volume traffic,
  • migrations run manually before deploy,
  • MIGRATE_ON_START=false or 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:latest

Each 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_analytics

Then 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=false

Run 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 migrate

The 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

VariableRequiredDefaultPurpose
HOSTno0.0.0.0HTTP listen host.
PORTno8080HTTP listen port.
LOG_LEVELnowarnService log level. Accepted values are silent, fatal, error, warn, info, debug, and trace.
PUBLIC_API_KEYSfor no-origin backend ingestemptyComma-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_ORIGINSrecommendedemptyComma-separated browser origins accepted by CORS and source validation.
MAX_BATCH_BYTESno20971520Maximum request body size after decompression.
MAX_EVENTS_PER_BATCHno10000Maximum number of events in a batch request.
CLICKHOUSE_URLnohttp://localhost:8123ClickHouse HTTP endpoint. Use https://<service-host>:8443 for ClickHouse Cloud.
CLICKHOUSE_USERnodefaultClickHouse username.
CLICKHOUSE_PASSWORDnoemptyClickHouse password.
CLICKHOUSE_DATABASEnoproduct_analyticsDatabase used for migrations and queries.
MIGRATE_ON_STARTnofalseApply 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:

  • events
  • persons
  • person_distinct_ids
  • sessions

Run migrations manually in production:

CLICKHOUSE_URL=https://<service-host>:8443 \
CLICKHOUSE_USER=<user> \
CLICKHOUSE_PASSWORD=<password> \
CLICKHOUSE_DATABASE=product_analytics \
npm run migrate

Inside 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.js

The 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.com

Backend 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.

On this page