feat(infra): compose base with LDAP, Keycloak and OSRM profiles #49

Merged
patrick merged 1 commit from feature/infra-compose-base into main 2026-09-11 10:24:09 +00:00
Owner

PH0.1 — Compose base: db, LDAP, Keycloak realm, nginx, OSRM

docker compose -f infra/compose/docker-compose.yml up (no profiles) brings db (Postgres 17 + PostGIS), openldap, keycloak and nginx to healthy. api/worker/web stay behind --profile app until PH0.2/PH0.5 land their Dockerfiles; osrm behind --profile osrm; pgadmin/phpldapadmin behind --profile tools.

Everything below was verified live in this session (fresh docker compose up, down -v, and re-run from scratch), not just config-validated.

What's in this PR

  • infra/compose/docker-compose.yml + docker-compose.dev.yml — services, profiles, healthchecks.
  • infra/db/init/01-databases.sql — creates the keycloak database, enables postgis/postgis_topology on polaris.
  • infra/ldap/bootstrap.ldif — base dc=polaris,dc=local, ou=people/ou=groups, five groupOfNames (admin/planner/technician/viewer/ingest) each with one matching inetOrgPerson. Applied by a one-shot ldap-init service running ldapadd against the live directory, not osixia's own custom-ldif bootstrap mechanism — that mechanism deletes each imported ldif file after success, which is fatal ("Device or resource busy") against a bind-mounted file, or silently deletes the repo file if the mount targets a bind-mounted directory instead. Documented inline in the compose file.
  • infra/keycloak/realm-polaris.json — realm roles matching the LDAP groups; clients polaris-web (PKCE, plus an audience mapper injecting polaris-api and a locale protocol mapper), polaris-api (bearer-only), fleetpulse (client-credentials, role ingest via its service-account user — the only "user" entry in the file, not a human). LDAP User Federation with role-ldap-mapper (group cn → realm role, LDAP_ONLY) and username/first-name/last-name/email attribute mappers. No realm-local human users.
  • infra/nginx/default.conf + a standalone nginx gateway service — single browser origin, /apiapi, /web, upstream hostnames resolved lazily via Docker's embedded DNS so the gateway stays healthy while api/web aren't running yet.
  • infra/osrm/prepare.sh — Geofabrik NL extract → osrm-extract/-partition/-customize (MLD) into infra/osrm/data (git-ignored).
  • infra/compose/.env.example, root README.md — startup sequence, profiles table, default dev logins.

Verified end-to-end (fresh docker compose up, no manual steps)

  • db, openldap, keycloak, nginx all reach healthy automatically (~30s from a cold docker compose up).
  • LDAP full sync imports all 5 users with 0 failures; each of admin/planner/technician/viewer/ingest logs in and receives exactly their own realm role in the access token (decoded JWT realm_access.roles), with aud correctly including polaris-api.
  • polaris and keycloak databases exist; postgis 3.5.2 and postgis_topology enabled on polaris.
  • Clients polaris-web (public, PKCE), polaris-api (bearer-only), fleetpulse (confidential, serviceAccountsEnabled) all present in the realm.
  • osrm, pgadmin, phpldapadmin, api, worker, web all stay off by default and only appear with their profile flag (checked via docker compose config/ps with each profile combination).
  • docker compose -f infra/compose/docker-compose.yml config (and with --profile app --profile osrm --profile tools, and with docker-compose.dev.yml layered) all validate cleanly.

Notable deviations from the ticket's sketch (see issue comment)

docs/07-architecture.md sketches LDAP bind/user credentials as ${env.VAR} placeholders resolved by envsubst. LDAP has no env-substitution of its own and Keycloak's realm importer has none either, so this PR instead bakes fixed, clearly-documented dev-only credentials into bootstrap.ldif/realm-polaris.json (same password for all five bootstrap users, fixed LDAP bind password, fixed fleetpulse client secret) and documents them in infra/compose/.env.example and root README.md. Rotating these for anything beyond a local compose stack is a follow-up.

Also adds infra/osrm/data/ to the root .gitignore (the only touch outside infra/**/README.md/.env.example) since prepare.sh's output is multi-GB and no infra/**-scoped ignore file exists yet.

Closes #3

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX

## PH0.1 — Compose base: db, LDAP, Keycloak realm, nginx, OSRM `docker compose -f infra/compose/docker-compose.yml up` (no profiles) brings `db` (Postgres 17 + PostGIS), `openldap`, `keycloak` and `nginx` to healthy. `api`/`worker`/`web` stay behind `--profile app` until PH0.2/PH0.5 land their Dockerfiles; `osrm` behind `--profile osrm`; `pgadmin`/`phpldapadmin` behind `--profile tools`. Everything below was verified live in this session (fresh `docker compose up`, `down -v`, and re-run from scratch), not just `config`-validated. ### What's in this PR - `infra/compose/docker-compose.yml` + `docker-compose.dev.yml` — services, profiles, healthchecks. - `infra/db/init/01-databases.sql` — creates the `keycloak` database, enables `postgis`/`postgis_topology` on `polaris`. - `infra/ldap/bootstrap.ldif` — base `dc=polaris,dc=local`, `ou=people`/`ou=groups`, five `groupOfNames` (admin/planner/technician/viewer/ingest) each with one matching `inetOrgPerson`. Applied by a one-shot `ldap-init` service running `ldapadd` against the live directory, **not** osixia's own custom-ldif bootstrap mechanism — that mechanism deletes each imported ldif file after success, which is fatal ("Device or resource busy") against a bind-mounted file, or silently deletes the repo file if the mount targets a bind-mounted directory instead. Documented inline in the compose file. - `infra/keycloak/realm-polaris.json` — realm roles matching the LDAP groups; clients `polaris-web` (PKCE, plus an audience mapper injecting `polaris-api` and a `locale` protocol mapper), `polaris-api` (bearer-only), `fleetpulse` (client-credentials, role `ingest` via its service-account user — the only "user" entry in the file, not a human). LDAP User Federation with `role-ldap-mapper` (group `cn` → realm role, `LDAP_ONLY`) and username/first-name/last-name/email attribute mappers. No realm-local human users. - `infra/nginx/default.conf` + a standalone `nginx` gateway service — single browser origin, `/api` → `api`, `/` → `web`, upstream hostnames resolved lazily via Docker's embedded DNS so the gateway stays healthy while `api`/`web` aren't running yet. - `infra/osrm/prepare.sh` — Geofabrik NL extract → `osrm-extract`/`-partition`/`-customize` (MLD) into `infra/osrm/data` (git-ignored). - `infra/compose/.env.example`, root `README.md` — startup sequence, profiles table, default dev logins. ### Verified end-to-end (fresh `docker compose up`, no manual steps) - [x] `db`, `openldap`, `keycloak`, `nginx` all reach `healthy` automatically (~30s from a cold `docker compose up`). - [x] LDAP full sync imports all 5 users with 0 failures; each of `admin`/`planner`/`technician`/`viewer`/`ingest` logs in and receives **exactly** their own realm role in the access token (decoded JWT `realm_access.roles`), with `aud` correctly including `polaris-api`. - [x] `polaris` and `keycloak` databases exist; `postgis` 3.5.2 and `postgis_topology` enabled on `polaris`. - [x] Clients `polaris-web` (public, PKCE), `polaris-api` (bearer-only), `fleetpulse` (confidential, `serviceAccountsEnabled`) all present in the realm. - [x] `osrm`, `pgadmin`, `phpldapadmin`, `api`, `worker`, `web` all stay off by default and only appear with their profile flag (checked via `docker compose config`/`ps` with each profile combination). - [x] `docker compose -f infra/compose/docker-compose.yml config` (and with `--profile app --profile osrm --profile tools`, and with `docker-compose.dev.yml` layered) all validate cleanly. ### Notable deviations from the ticket's sketch (see issue comment) `docs/07-architecture.md` sketches LDAP bind/user credentials as `${env.VAR}` placeholders resolved by envsubst. LDAP has no env-substitution of its own and Keycloak's realm importer has none either, so this PR instead bakes fixed, clearly-documented **dev-only** credentials into `bootstrap.ldif`/`realm-polaris.json` (same password for all five bootstrap users, fixed LDAP bind password, fixed `fleetpulse` client secret) and documents them in `infra/compose/.env.example` and root `README.md`. Rotating these for anything beyond a local compose stack is a follow-up. Also adds `infra/osrm/data/` to the root `.gitignore` (the only touch outside `infra/**`/`README.md`/`.env.example`) since `prepare.sh`'s output is multi-GB and no `infra/**`-scoped ignore file exists yet. Closes #3 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX
Wave 0a row PH0.1: docker compose up (no profiles) brings db (Postgres
17 + PostGIS), openldap, keycloak and nginx to healthy, verified live
end-to-end in this session:

- infra/db/init/01-databases.sql: creates the keycloak database and
  enables postgis/postgis_topology on polaris.
- infra/ldap/bootstrap.ldif + a one-shot ldap-init service: base
  dc=polaris,dc=local, ou=people/ou=groups, five groupOfNames
  (admin/planner/technician/viewer/ingest) each with one matching
  inetOrgPerson. Bootstrap runs via ldapadd against the running
  directory rather than osixia's own custom-ldif mechanism, which
  deletes each imported ldif file on success — fatal against a
  bind-mounted file (or silently deletes the repo file if the mount
  targets a directory instead).
- infra/keycloak/realm-polaris.json: realm roles matching the LDAP
  groups; clients polaris-web (PKCE + audience/locale protocol
  mappers), polaris-api (bearer-only), fleetpulse (client-credentials,
  role ingest via its service-account user); LDAP user federation with
  role-ldap-mapper (group cn -> realm role, LDAP_ONLY) and
  username/first/last/email attribute mappers. No realm-local human
  users. Verified: all five LDAP users log in and receive exactly
  their own realm role in the access token, with the polaris-api
  audience present.
- infra/nginx/default.conf + standalone nginx service: single browser
  origin proxying /api -> api and / -> web via lazily-resolved
  upstreams (Docker embedded DNS, not resolved at nginx startup), so
  the gateway stays healthy while api/web are still gated behind
  --profile app (their Dockerfiles land in PH0.2/PH0.5).
- infra/osrm/prepare.sh: Geofabrik NL extract -> osrm-extract/
  -partition/-customize (MLD) into infra/osrm/data (git-ignored), used
  by the osrm service behind --profile osrm.
- pgadmin/phpldapadmin behind --profile tools.
- infra/compose/.env.example documents the dev-only credentials baked
  into bootstrap.ldif/realm-polaris.json (LDAP has no env-substitution
  of its own); root README.md documents the full startup sequence,
  profiles and default dev logins.

Dev-only fixed credentials (LDAP bind password, per-user password,
fleetpulse client secret) are used instead of the env-templated
secrets docs/07-architecture.md sketches, since Keycloak's realm
importer and LDIF have no env-substitution of their own; noted as a
follow-up on the ticket.

Also adds infra/osrm/data/ to the root .gitignore (prepare.sh's
multi-GB output) since no infra/**-scoped gitignore exists yet.

Refs: #3

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX
patrick deleted branch feature/infra-compose-base 2026-09-11 10:24:10 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
patrick/Polaris!49
No description provided.