RUN_MODE selects which surfaces a process serves, so the same image runs as the API, as the report worker, or as both. Four dependencies sit behind them.
Reporter reads your databases through an extraction engine that runs inside the worker process. There is no separate extraction service to deploy.
What you deploy
Run modes
RUN_MODE=api serves every REST operation, plus /health, /readyz, and /version, on the address in SERVER_ADDRESS. RUN_MODE=worker consumes the report command queue and serves /health and /readyz on HEALTH_PORT. RUN_MODE=all runs both surfaces in one process.
Use all for local development. In production, deploy the two surfaces separately, so report generation scales on its own.
MongoDB
Both surfaces use the same MongoDB deployment. The API surface writes templates, reports, and deadlines; the worker updates a report as it completes. In single-tenant mode,
MONGO_HOST and MONGO_NAME are required at startup. In multi-tenant mode, each tenant gets its own database, resolved from the JWT on the request. That path fails closed: a request that carries a tenant with no tenant database returns an error instead of touching a shared database.
RabbitMQ
Two separate concerns share one broker.
The report command queue
This queue carries work from the API surface to the worker.RABBITMQ_EXCHANGE, RABBITMQ_GENERATE_REPORT_QUEUE, and RABBITMQ_GENERATE_REPORT_KEY name the exchange, the queue, and the routing key. The API surface publishes, so it needs all three. The worker only consumes, so it needs RABBITMQ_GENERATE_REPORT_QUEUE alone. Both surfaces need the broker connection itself, and the objects must exist before the services start.
The channel is internal to Reporter. To learn that a report finished, subscribe to the business events below, or poll the report.
A worker that fails a message retries it up to five times with backoff, and then rejects it without requeue. Bind a dead-letter exchange to the command queue, so a rejected report lands somewhere you can inspect.
The events exchange
Business events (template.*, report.*, deadline.*) go to an exchange you name in RABBITMQ_REPORT_EVENTS_EXCHANGE. The value is operator-configured; reporter.events is the reference value.
Set that exchange, STREAMING_BROKERS, and STREAMING_CLOUDEVENTS_SOURCE whenever STREAMING_ENABLED=true. With streaming off, both surfaces start normally and publish nothing.
Object storage
Reporter uses one S3-compatible bucket, named in
OBJECT_STORAGE_BUCKET. It holds two kinds of object, each under its own prefix:
- the template source, as
templates/<templateId>.tpl - the rendered report, as
reports/<templateId>/<reportId>.<format>
<tenantId>/templates/... and <tenantId>/reports/....
AWS S3, MinIO, and SeaweedFS all work. Reach SeaweedFS through its S3 gateway. OBJECT_STORAGE_USE_PATH_STYLE=true is what MinIO and SeaweedFS expect, and OBJECT_STORAGE_DISABLE_SSL stays false outside local development.
Report retention
Reporter keeps every report it renders, so the bucket grows with your report volume. Set expiry on the bucket, with the object store’s own lifecycle policy, over the window your retention rules require.Redis or Valkey
REDIS_HOST is required on the API surface. On the worker it is required only when MULTI_TENANT_ENABLED=true, where it caches tenant discovery. Redis backs the idempotency lock on report creation, the datasource schema cache, and tenant lifecycle messages in multi-tenant mode.
Idempotency state lives in Redis, not in process memory, so API replicas share it. The same report request sent twice, to two replicas, creates one report.
Data sources
Reporter reads reporting data from PostgreSQL and MongoDB data sources declared in the environment, one
DATASOURCE_{NAME}_* block per source. See Environment variables for the block.
There is no API that registers a data source, so a new source is a configuration change and a restart. Reporter also starts with none configured, and serves templates, deadlines, and metrics.
Sizing the worker
RABBITMQ_NUMBERS_OF_WORKERS sets how many report jobs one worker process runs in parallel. For horizontal scale, add worker replicas and drive them from the depth of the command queue.
PDF output renders through a headless-browser pool: PDF_POOL_WORKERS concurrent renders, which defaults to 2, each bounded by PDF_TIMEOUT_SECONDS, which defaults to 90. Size worker memory against that pool, not only against the rows a report reads. The other output formats do not use it.
Every report also carries fixed extraction limits. They are 10 data sources, 50 tables per data source, and 200 fields per table, with 4 data sources read at a time, a 300-second deadline, and a 100 MiB cap on extracted data. The ENGINE_* variables change them.
Deployment mode and TLS
DEPLOYMENT_MODE declares the flavor of the deployment: local, byoc, or saas. It tags the /readyz response, and in saas it enforces TLS.
Leave ALLOW_INSECURE_TLS unset in production. It bypasses those checks, and the local development stack is the only place for it.
Startup checks
Reporter validates its configuration before it serves anything. Each check below stops the process, and the error names every variable at fault.
A dependency that is down at boot does not produce a silently broken pod.
/health answers 503 until the startup self-probe succeeds. The kubelet then restarts the pod instead of sending it traffic.Rolling updates
On
SIGTERM, both surfaces enter a drain. /readyz answers 503 from the moment the signal arrives, before the servers begin shutdown, and in-flight requests and in-flight messages finish. Kubernetes removes the pod from Service endpoints while it still works.
Set the termination grace period above your longest report render. See the health and readiness reference for what the probes report during a drain.
Next steps
Environment variables
Every Reporter variable, by category.
BYOC configuration
The configuration blocks every Lerian product shares.
Health and readiness
The probe contract and what each response means.
Connect Reporter to Midaz
Point Reporter at a Midaz database and render your first report.

