Deployment

CogniGate runs as containers. The repository ships a reference docker-compose.yml that brings up three services — the gateway, the analytics engine, and PostgreSQL — with every one of them health-checked.

The reference deployment

bash
cp .env.example .env
openssl rand -hex 32   # -> ANALYTICS_TOKEN
openssl rand -hex 24   # -> GATEWAY_BOOTSTRAP_KEY

docker compose up -d --wait

--wait returns only once the whole stack is actually serving rather than merely started. The two images built here carry their own HEALTHCHECK — the gateway on /healthz, analytics on /actuator/health — and PostgreSQL is checked in the compose file, because the stock image ships without one.

Both are published to the GitHub Container Registry on every push to main, for linux/amd64 and linux/arm64, and docker-compose.yml names them alongside its build contexts. Pulling instead of building is what a host with no Go toolchain and no JDK on it wants:

bash
docker compose pull && docker compose up -d --wait

main tracks the default branch and sha-<commit> pins one commit of it. latest resolves to the same image as main for now, and will follow releases once one is tagged, so main is the tag to name if the branch is what you mean.

There is deliberately no depends_on: analytics on the gateway. The gateway builds its analytics client but does not dial it at startup, buffers what it cannot deliver, and drains the backlog when analytics answers. Waiting would mean an outage in one service becoming an outage in both.

Two variables have no defaults, and the compose file refuses to start without them:

VariableWhat it guards
GATEWAY_BOOTSTRAP_KEYThe admin plane, before any admin key has been minted
ANALYTICS_TOKENThe metering API on port 8081

A reference deployment that shipped with a known credential is a reference deployment nobody should copy, so neither has a fallback value.

Three more are optional:

VariableDefaultEffect
QUOTA_ENFORCEMENTonobserve reports over-cap without refusing
CACHE_ENABLEDfalseResponse caching (GW-12)
POSTGRES_PASSWORDdev valueChange it before production

Before production

  1. Do not publish port 8081. The analytics service holds every tenant's usage. The reference file publishes it so a first-run deployment can be inspected; a real one should leave it on the internal network. Publishing 5432 is the same mistake with a larger blast radius.
  2. Retire the bootstrap key. It exists to mint the first real admin key through POST /admin/v1/admin-keys. Once you have one, take GATEWAY_BOOTSTRAP_KEY back out of the environment — a long-lived root credential in a compose file is the thing it was meant to avoid.
  3. Terminate TLS. The recommended shape is a private network or an operator-owned reverse proxy. Where you have neither, set CG_TLS_CERT_FILE and CG_TLS_KEY_FILE; one listener serves both planes, so the pair cannot leave one of them plaintext by accident.
  4. Pin the schema. The analytics service runs Hibernate with ddl-auto: update, which is fine for a first run and wrong for a system holding billing data. Move to Flyway or Liquibase and set validate.
  5. Scrape /metrics. It is on by default and unauthenticated, which is what a Prometheus scrape on a private network expects. Set CG_METRICS_TOKEN if the path is reachable from anywhere it should not be.

Scaling

The gateway holds its configuration in memory. That is what makes it fast and what bounds how it scales, so it is worth being precise about:

  • Usage is shared. Every replica ships its records to the same analytics service, so totals and breakdowns are correct across the whole fleet no matter which replica served a request.
  • Configuration is not. Two gateways are two independent sets of tenants, keys, providers, aliases, routing rules and quotas. Creating a tenant on one replica does not create it on the others.
  • Nor are caches, rate limiters or captures. A per-key rate limit of 50 rps across three replicas is 150 rps in practice.

So docker compose up --scale gateway=3 behind a load balancer works, provided every replica is configured identically at startup and nothing calls the admin plane afterwards. Configuration that must outlive a restart or be shared between replicas has to be applied at startup, from outside.

Rolling a replica out is safe: shutting down fails /healthz immediately so traffic is steered away, then gives in-flight requests drain_timeout to finish. Open streams are not cut off mid-completion.