Make docker-compose.yml enforce the api/enterprise-api binary swap

Closes the local/dev half of a gap named repeatedly across this
phase's docs: Helm already made api/enterprise-api mutually exclusive
(same enterprise.enabled flag that turns on RBAC/audit/SSO, both
rendering to the same Service name/port); docker-compose.yml let both
run side by side, with nothing actually pointing at enterprise-api by
default.

Mechanism: both services now carry a `profiles` entry (single-tenant /
enterprise), selected via COMPOSE_PROFILES -- a new checked-in .env
sets single-tenant as the zero-config default (unchanged behavior for
anyone who doesn't touch it), and `COMPOSE_PROFILES=enterprise docker
compose up` swaps to enterprise-api instead. Docker Compose profiles
are purely additive (no "profile X excludes service Y" primitive), so
true exclusivity comes from both being profile-gated with no shared
default profile, not from one excluding the other directly.

Mirrors Helm's same-Service-name trick so alerting's API_QUERY_URL and
web's VITE_API_BASE_URL need zero conditional logic either way:
enterprise-api now maps host port 8080 (was 8083, its own binary
default -- overridden via HTTP_LISTEN_ADDR) and carries a
`networks.default.aliases: [api]` entry, so whichever binary is
actually running answers on the same compose-network hostname and host
port. alerting's and web's depends_on for api/enterprise-api are now
`required: false` (Compose's supported "optional dependency" shape) --
without it, compose errors on the inactive one rather than just
skipping it, since depends_on doesn't otherwise know about profiles.

Verified for real in this environment via `docker compose config`
(renders and validates the merged YAML without needing a daemon):
confirmed api/enterprise-api never both appear in --services output
for either profile selection, confirmed enterprise-api's rendered
block has port 8080/alias "api"/HTTP_LISTEN_ADDR ":8080" when the
enterprise profile is active, and confirmed `docker compose run
enterprise-api ...`/`docker compose build enterprise-api` (used by
enterprise/README.md's and phase-4-runbook.md's provisioning steps)
still work regardless of the active profile -- explicit service
references bypass profile filtering, confirmed by the commands
reaching a daemon-connection permission error rather than a
profile-resolution error. Not verified: an actual `docker compose up`
against a real daemon, still unavailable in this environment.

Docs updated in lockstep -- CLAUDE.md, threat-model.md (including its
summary table), phase-4-runbook.md (new §10a, §8's provisioning
commands updated for the new port/profile), enterprise/README.md.
This commit is contained in:
2026-08-14 08:17:41 -07:00
parent 2e698f5623
commit 8d7326fc6a
6 changed files with 178 additions and 56 deletions
+20 -14
View File
@@ -44,20 +44,26 @@ searchclient`'s tests run a real in-process gRPC server and confirm the
wire-level `SearchRequest` carries the right `tenant_id`. All pass, for
real, no disclaimer needed for this specific claim.
**The Helm chart now closes this for K8s deployments; `docker-compose.yml`
still doesn't.** `deploy/helm/sentry/templates/api.yaml` and
`enterprise-api.yaml` are mutually exclusive, gated on opposite sides of
the same `enterprise.enabled` flag, rendering to the same Service
name/port — so a Helm-deployed cluster runs exactly one of the two
binaries, chosen by the same flag that turns on RBAC/audit/SSO, not a
second independently-forgettable decision. Verified by parsing (not
**Both Helm and docker-compose now close this.**
`deploy/helm/sentry/templates/api.yaml` and `enterprise-api.yaml` are
mutually exclusive, gated on opposite sides of the same
`enterprise.enabled` flag, rendering to the same Service name/port — so
a Helm-deployed cluster runs exactly one of the two binaries, chosen by
the same flag that turns on RBAC/audit/SSO, not a second
independently-forgettable decision. Verified by parsing (not
eyeballing) the rendered YAML under both values: exactly one `sentry-api`
Deployment either way, with the right image. **`docker-compose.yml`
still runs plain `api` unconditionally** and includes `enterprise-api`
as an extra, separately-started service — local/dev parity with the Helm
chart's enforcement is real remaining work. And this only constrains
*deployment*, not *operation*: nothing stops an operator from manually
running plain `api`'s image against a cluster that has tenants
Deployment either way, with the right image. `docker-compose.yml`'s
`api`/`enterprise-api` services are now the analogous mutually-exclusive
choice, gated behind `COMPOSE_PROFILES` (`.env` checks in
`single-tenant`, i.e. plain `api`, as the zero-config default) and
sharing the same host port/network-alias trick to stay transparent to
`alerting`/`web` either way — verified via `docker compose config`
(renders and validates the merged YAML without a daemon; confirms
`api`/`enterprise-api` never both appear in `--services` output for the
same profile selection) — see `/docs/phase-4-runbook.md` §10a. This
still only constrains *deployment*, not *operation*: nothing stops an
operator from manually running plain `api`'s image against a cluster
(or compose project) that has tenants
provisioned, pointing at the same ClickHouse/Postgres. The Helm chart
makes the *default*, chart-managed path correct; it isn't a runtime
guard against misconfiguration.
@@ -404,7 +410,7 @@ terms:
| Tantivy tenant_id resolution (`enterprise/internal/searchclient`) | **Enforced, verified live** — real gRPC wire-level test |
| Ingest tenant-awareness (ClickHouse and Tantivy both) | **Not implemented, undesigned** — every ingested record lands in the single shared database/index regardless of tenant |
| Deployment actually routing traffic to `enterprise-api` (Helm) | **Enforced**`api`/`enterprise-api` are mutually exclusive, same flag as RBAC/audit/SSO |
| Deployment actually routing traffic to `enterprise-api` (docker-compose) | **Not implemented** `docker-compose.yml` runs plain `api` unconditionally |
| Deployment actually routing traffic to `enterprise-api` (docker-compose) | **Enforced**`api`/`enterprise-api` are mutually exclusive via `COMPOSE_PROFILES`, same flag choice as Helm's `enterprise.enabled`; verified via `docker compose config`, not an actual `docker compose up` in this environment |
| Human SSO login — OIDC | **Built, verified with a real fake IdP** (not yet tried against a real external IdP) |
| Human SSO login — SAML | **Built, verified with a real fake IdP** (not yet tried against a real external IdP) |
| Multi-tenant-membership login (tenant picker) | **Not implemented** — refused with a clear error, not guessed |