8. Local infrastructure with Docker Compose¶
Date: 2026-10-04
Status¶
Accepted
Context¶
S2 starts M3: a local stack of PostgreSQL, Kafka (KRaft), Redis, Keycloak and Traefik, with health checks and start order. Done when make up brings everything up healthy and the subdomains respond (BUILD_PLAN 10, 16.2 #18, #21, #24, #26 to #28). The development machine is an 8 GB Apple M1 with Docker Desktop limited to 3.8 GiB.
Decisions¶
- One Compose file,
deploy/compose/compose.yaml, project nameprayog, profilesinfra,app,obs. S2 fillsinfra. The Makefile is the interface:make env,up,down,ps,logs,smoke,reset. - Pinned images:
postgres:16.15-alpine,redis:7.4.11-alpine,apache/kafka:4.3.1,quay.io/keycloak/keycloak:26.8.0,traefik:v3.7.13,traefik/whoami:v1.12.0(latest patch releases on 2026-10-04). - Readiness, not just start order. Every long-running service has a health check that asks the real question (
pg_isready; a Kafka API call; Keycloak/health/readyon management port 9000 through bash/dev/tcp, since the image has no curl;redis-cli ping; Traefik--ping).depends_on: condition: service_healthyorders Keycloak after PostgreSQL and topic creation after Kafka.make upusesup --wait, so it returns only when everything is healthy. - Kafka: one KRaft node (broker + controller), fixed
CLUSTER_ID. Listeners:INTERNALadvertised askafka:9092for containers,EXTERNALadvertised aslocalhost:9094for the host,CONTROLLERon 9093. Auto topic creation is off; a one-shotkafka-initjob createsprayog.exchange.events.v1with 3 partitions. Internal topics use replication factor 1 (single node). - Keycloak in
start-devmode, stored in PostgreSQL (its ownkeycloakdatabase and user, created by an init script).KC_HOSTNAME=http://auth.prayog.localhostfixes the token issuer;KC_PROXY_HEADERS=xforwardedbecause Traefik sits in front. - Realm
prayogimported fromdeploy/compose/keycloak/prayog-realm.json: prayog-web: public client, authorization code + PKCE (S256), redirects only toapp.prayog.localhost.prayog-api: no login flows; it exists to be the token audience.prayog-bot-demo,prayog-agents: confidential clients, client credentials only; their service accounts have rolebot.- Audience mapper on the three token-issuing clients: every token carries
aud: prayog-api. - Realm roles
trader,ops,bot; userstrader1(trader) andops1(ops) with full profiles, so login needs no extra step. sslRequired: external: HTTP is allowed only from loopback and private addresses.- Secrets never committed.
make envcreates.envfrom.env.example, replacing everychange-mewithopenssl rand -hex 16. Compose refuses to start with a missing value (${VAR:?}). The realm file holds${...}placeholders that Keycloak substitutes at import. CI generates its own throwaway.env. - Traefik v3 as the single entry point on
127.0.0.1:80, routing by host name from container labels (exposedbydefault=false).app.andapi.point attraefik/whoamiplaceholders until S18 and S10.grafana.comes with theobsprofile (S21). - Every host port binds to 127.0.0.1 (80, 5432, 6379, 9094): nothing is reachable from the local network.
- Memory caps: Kafka and Keycloak heaps at 256 to 512 MB. Measured total for the infra profile: about 1.1 GB (Keycloak 570 MB, Kafka 410 MB, the rest under 50 MB each).
- Acceptance is a script,
make smoke(deploy/compose/smoke.sh), run locally and by a new CI job (make env,make up,make smoke, logs on failure,make down).
Testing¶
make smoke: 20 checks. Every subdomain through Traefik (and a 404 for an unknown host); the discovery document's issuer; client-credentials tokens for both bot clients with the right issuer, audience andbotrole; a wrong secret refused with 401;trader1andops1exist with their roles;prayog-webuses PKCE S256; PostgreSQLSELECT 1and thekeycloakdatabase's owner; RedisPING; the Kafka topic has 3 partitions; the host listener answers; auto topic creation is off.- The check can fail: with Redis stopped it reports one failure and exits 1.
- A browser login as
trader1through Traefik reaches the account page without extra prompts. - Restart:
make downthenmake uptakes 22 s; data is kept, the realm import is skipped, and smoke passes.
Trade-offs¶
- Imports happen once. Keycloak imports with
IGNORE_EXISTINGand PostgreSQL runs its init scripts only on an empty volume, so realm or secret changes needmake reset. The alternative (re-import on every start) would wipe changes made in the admin console. - The Docker socket is mounted read-only into Traefik. Read access still exposes every container's configuration (including environment secrets). Acceptable on a dev machine; a hosted setup should use a socket proxy or file-based routes.
- Plain HTTP and
start-dev. Hosting needs TLS, Keycloakstartmode and a real hostname. - Single-node Kafka has no redundancy; good enough for a local exchange, and the topic's 3 partitions keep the consumer design honest.
- Python and curl resolve
*.localhostwithout/etc/hostson macOS; Safari was not tested, so the README lists the hosts line.