Run the local stack
AI-generated content
This document was generated by an AI assistant. Verify accuracy before relying on the details.
infra/docker/compose.yaml runs the supporting infrastructure (MongoDB replica set, Keycloak, LocalStack, Mailpit) AND the 9 Spring Boot services. Everything is built from monorepo source — only Docker is required, no AWS credentials, no host-side Java/Maven, no external Maven repositories beyond Maven Central + Spring Milestones.
First-time setup (zero → working SPA)
The shortest path that gets you to a logged-in dev/dev on http://localhost:4200/ showing the home dashboard, in French, against real seeded data.
Prerequisites
- Docker Desktop (or any Docker engine + compose v2). Allocate ≥ 8 GB RAM to the engine — the full stack peaks around 6 GB.
- An Atlas snapshot tarball of the dev cluster — ask the team for a recent
restore-<id>.tar.gz(the file the "Download Snapshot" button produces in the Atlas UI). Drop it ininfra/seeds/— that's wheremake seedlooks first. The directory itself is git-tracked (via.gitkeep) but every*.tar.gzinside is gitignored, so a straygit addwon't commit it. See Seeding the local Mongo from an Atlas dump for the full lookup order. - Read access on the i18n spreadsheet (https://docs.google.com/spreadsheets/d/1pSlwsb6XEZ_uzz-KtNgffFrzEgLR6WcToLvO-_IeVsc). It's currently public, no Google credentials needed.
One-time setup
cd infra
# 1. Create your local .env file from the template and add the i18n vars.
cp .env.example .env.local
chmod 600 .env.local
Open .env.local and uncomment / fill in just the translation block at the bottom (the rest is needed only if you want to point at real AWS dev infra — LocalStack covers everything for local dev):
SPREADSHEET_URL=https://docs.google.com/spreadsheets/d/1pSlwsb6XEZ_uzz-KtNgffFrzEgLR6WcToLvO-_IeVsc/export?format=tsv
CATALOG_GID=2016792665
COMMON_GID=1740544717
DOCUMENTS_GID=1844992713
PERSONS_GID=1558004497
NOTES_GID=1409729206
LISTS_GID=378239050
ACCOUNTING_GID=1607045627
CONTRACTS_GID=1161123401
PARTS_GID=59981804
SEARCH_GID=1022530540
NACE_GID=14446475
TICKET_GID=452972274
JEDI_GID=536707157
ADMIN_GID=1828090766
(Without these, make build-base falls back to empty translation stubs and the SPA shows raw keys instead of French/English text — the rest of the stack still works.)
Boot, seed, login
cd infra
# 2. Build all JARs (10–15 min first time, ~30s on rebuild).
# Fetches translations from the Google Sheet using the env vars above.
make build-base
# 3. Boot infrastructure + every Spring service. Builds the per-service
# images on first run (~1 min), caches afterwards.
make up-services
# 4. Seed the Mongo databases from the Atlas snapshot. Picks up the
# most recent infra/seeds/restore-*.tar.gz automatically (with
# ~/Downloads/ as legacy fallback); pass an explicit path with
# `DUMP=/some/where/restore-XXX.tar.gz` if needed.
make seed
# 5. Open the SPA. Login: dev / dev.
open http://localhost:4200/
You should land on the home dashboard with menu items in French ("Propriétaires", "Locataires", "Mandats", "Sinistres", …). Switch to English from the language flag in the topbar — localStorage.language just flips between "fr" and "en" and reloads.
Day-to-day workflow
| You changed | What to run |
|---|---|
| Backend Java code | make build-base && docker compose -f docker/compose.yaml --profile services up -d --build --no-deps adb-<svc> |
adb-ui Angular code | nothing — wrangler dev watches dist/ and auto-reloads |
compose.yaml env or wiring | docker compose -f docker/compose.yaml --profile services up -d --no-deps adb-<svc> |
| Mongo content | make seed-clear && make seed DUMP=... |
| Keycloak realm-export | docker volume rm adb-local_keycloak-data && make up-services (Keycloak only re-imports on a clean H2 DB) |
make down-all # stop everything (containers stay around, volumes preserved)
make logs-all SERVICE=adb-views # follow logs for one service
make ps # container health
Starting over from scratch
make reset # destructive: prompts for "yes"
# nukes containers + volumes + locally-built
# images, but leaves .env.local and
# infra/seeds/*.tar.gz alone
make reset YES=1 # skip the prompt (CI / scripted use)
make fresh # one-shot: reset + build-base + up-services + seed
make fresh YES=1 # skip the prompt
Use reset when something has gone weird (corrupted mongo replica set, stuck volumes, mismatched JARs) and you want a guaranteed-clean rebuild. Use fresh to chain that with the full setup recipe.
Two-tier model
| Target | What runs | First-run time |
|---|---|---|
make up | mongo, keycloak, localstack, mailpit | ~30 s |
make build-base | Maven build of every JAR (shared adb-local-build image) | ~10–15 min (first time), then cached |
make up-services | runs build-base then all Spring services | first time = build-base + ~1 min, subsequent = ~30 s |
Make-target reference
cd infra
# Just the supporting infra (mongo, keycloak, localstack, mailpit):
make up
# Or full stack (infra + 9 Spring services):
make up-services
make ps # see container health
make logs # follow infra logs
make logs-all # follow infra + service logs (pass SERVICE=adb-x for one)
make down-all # stop everything
# Foreground variants — everything streams to your terminal, Ctrl+C tears it down:
make up-fg
make up-services-fg
# Mongo seeding (see "Seeding the local Mongo from an Atlas dump" below):
make seed # auto-find infra/seeds/restore-*.tar.gz
make seed DUMP=/path/to/restore-XXX.tar.gz
make seed-clear # drop every adb-* db (keeps the volume)
# Nuclear option (see "Starting over from scratch" above):
make reset # tear down + remove volumes + remove images
make fresh # reset + build-base + up-services + seed
How services reach LocalStack
Every Spring service builds its SqsAsyncClient / SnsClient / S3Client via MessagingConfig / AwsS3Config. These read AwsEndpointOverride.fromEnv() — which checks AWS_ENDPOINT_URL and applies it as .endpointOverride(...) on the builder. The compose file sets AWS_ENDPOINT_URL=http://localstack:4566, so locally everything routes there. In production the env var is unset and the SDK targets real AWS as usual. (This is the equivalent of what global AWS_ENDPOINT_URL support gives you in AWS SDK 2.21+; the services still pin SDK 2.17, hence the helper.)
What's running
| Container | Host port | Notes |
|---|---|---|
mongo | 27018 | rs0 single-node replica set; container internal port stays 27017 |
keycloak | 8080 | Realm ADB pre-imported, dev user dev/dev (email aligned with the seeded Pierre Sacco — see Seeding the local Mongo) |
localstack | 4566 | SQS/SNS/S3/Secrets Manager mock |
mailpit | 8025 (UI), 1025 (SMTP) | Catches outbound mail |
adb-persons | 8081 | /persons/actuator/health |
adb-utilities | 8082 | |
adb-files | 8088 | host port 8088 → container 8083 (avoids local port clash) |
adb-contracts | 8084 | |
adb-parts | 8085 | |
adb-aggregates | 8086 | |
adb-accounting | 8089 | does not start locally without a valid Google Cloud service-account JSON in GOOGLE_SHEETS_CLIENT_CREDENTIALS. Skip in local dev unless you need accounting. |
adb-reports | 8090 | actuator endpoint requires auth (returns 401, but server is up) |
adb-views | 8092 | actuator endpoint requires auth (returns 401, but server is up) |
adb-ui | 4200 | served by wrangler dev inside a node:20-bookworm container. First start runs npm install (~3–5 min) then ng build --watch; wrangler picks up changes on rebuild. Worker config in adb-ui/wrangler.jsonc |
Verifying it works
# every Spring service (except adb-accounting + adb-ui — see caveats above)
for spec in 8081/persons 8082/utilities 8088/files 8084/contracts \
8085/parts 8086/aggregates 8090/reports 8092/views; do
port="${spec%/*}"; name="${spec#*/}"
curl -fsS "http://localhost:$port/$name/actuator/health" || echo " $port: not yet"
done
# Keycloak realm reachable
curl -fsS http://localhost:8080/realms/adb >/dev/null && echo "keycloak OK"
# LocalStack queues exist (24 queues + 24 DLQs = 48)
aws --endpoint-url http://localhost:4566 sqs list-queues \
--queue-name-prefix local- | jq '.QueueUrls | length'
How the build works
The Java build happens in two layers:
make build-baserunsmvn -DskipTests installover the entire monorepo via the repo-rootpom.xmlaggregator. The output is a Docker image taggedadb-local-build:latestcontaining every spring-boot JAR.- Each service's Dockerfile is a 3-line file:
FROM eclipse-temurin:21-jre-alpine,COPY --from=adb-local-build:latest /workspace/<svc>/.../target/*.jar /app/app.jar, ENTRYPOINT.
This means:
- The Maven dependency download happens once, not per service.
- A code change anywhere triggers a rebuild of
adb-local-build(Maven layer cache helps but isn't perfect). - For very fast iteration on a single service you can
mvn package -pl :<svc> -amon the host (if you have Java + Maven) and rebuild only that container withdocker compose build <svc>.
The aws-maven extension in each service's .mvn/extensions.xml is preserved (CI still uses it to publish to the prod S3 Maven repo) but .mvn/settings.xml no longer references the S3 URLs, so it's a no-op for local builds.
Common issues
- Mongo binds to host port 27018, not 27017. macOS Docker Desktop's
vmnetddriver sometimes holds 27017 even whenlsofshows nothing. - adb-files binds to host port 8088, not 8083. Same reason.
- First run of
make build-basetakes 10–15 minutes while Maven resolves all transitive deps (Spring Boot, Keycloak, AWS SDK, …). Subsequent runs are fast thanks to the BuildKit cache mount on/root/.m2/repository. - adb-accounting fails to start with "Error reading credentials from stream, 'type' field not specified" — the service requires a valid Google Cloud service-account JSON to connect to Google Sheets at startup. Either provide one via
GOOGLE_SHEETS_CLIENT_CREDENTIALSor skip the service for local dev. - adb-ui first start is slow — runs
npm install(~3–5 min) inside the container beforeng build --watchkicks off, thenwrangler devboots. Subsequent restarts skip the install thanks to the namedadb-ui-node-modulesandadb-ui-angular-cachevolumes. The legacy nginx-based Dockerfile (adb-ui/Dockerfile) is not used by the local stack any more. - adb-ui post-login lands on
/denied— the Angular app'sAuthGuardcallsGET /persons/meimmediately after login. If the local Mongo is empty, the call returns 204 and the SPA routes to/denied. Seed the local Mongo from a real Atlas dump first — see below. /persons/mereturns 403 "No JWT token found in request headers" —wrangler dev(workerd) silently strips theAuthorizationheader from outbound subrequests when the originating browser request was credentialed (Cookie + Sec-Fetch-*). The worker forcescredentials: "omit"on the proxied fetch inadb-ui/worker/index.jsto keep the bearer untouched. If you re-introduce browser-style request semantics in that proxy, every backend call will start failing with this error.
Seeding the local Mongo from an Atlas dump
The Spring services start with empty databases. The SPA's home page needs at least the dev user's Person and matching Organization to render — without them GET /persons/me returns 204 and AuthGuard routes to /denied. Rather than fabricate fixtures, the local stack imports a real MongoDB Atlas snapshot.
Where to drop the tarball
Recommended: infra/seeds/ — git-tracked directory (via .gitkeep) with all *.tar.gz files gitignored. That's where make seed looks first, so the common workflow is just:
cd infra
cp /wherever/you/got/it/restore-<snapshot-id>.tar.gz seeds/
make seed # auto-picks the most recent
make seed lookup order (the first hit wins):
- The
DUMP=argument — e.g.make seed DUMP=/explicit/path.tar.gz. - The
$ATLAS_DUMP_PATHenv var. - The newest
restore-*.tar.gzininfra/seeds/(gitignored). - The newest
restore-*.tar.gzin~/Downloads/(legacy fallback, convenient for engineers used to the old workflow).
Why infra/seeds/? It keeps the tarball next to the rest of the local-stack artefacts (so it survives make reset, doesn't get lost in your Downloads folder), and the directory's gitignore protects you from ever committing a dump file by accident — only .gitkeep is tracked, every *.tar.gz is ignored.
# Verify the tarball is in place and gitignored:
ls -lh infra/seeds/ # should show your restore-*.tar.gz
git check-ignore -v infra/seeds/restore-*.tar.gz # confirms it's ignored
What the script (infra/scripts/seed-from-atlas-dump.sh) does:
- Extracts the WiredTiger files from the tarball.
- Boots a temporary
mongo:7against them with the embedded replica-set name. - Force-reconfigures it as a single-node primary (Atlas snapshots ship with a stale replica-set config that won't elect a primary on its own).
mongodumps everyadb-*database, thenmongorestore --drops into the stack'smongocontainer.- Tears the temp container down.
The seeded dev identity lands on Pierre Sacco — pierre.sacco@life-connect.fr, organization 5f7325ec5d7c2205ca4688df, with agent / agent-admin / internal / application-admin roles. The Keycloak realm-export pins the dev user's email to that address so personRepository.findOneByUserEmail(...) resolves.
To swap to a different seeded user, change email in the users block of infra/docker/keycloak/realm-export.json, recreate the keycloak container (docker volume rm adb-local_keycloak-data then docker compose up -d keycloak), and clear the SPA's session storage.
Wipe + re-seed if you need a clean slate:
make seed-clear # drops every adb-* database
make seed DUMP=/path/to/another.tar.gz
Why the worker injects an organization-id header
The backend's AdbJwtGrantedAuthoritiesConverter filters the JWT's accesses claim by the organization-id request header — without it the user has zero scopes and every call 403s. The SPA only sets that header once it has loaded the user's organizations, but the very first GET /persons/me runs before that. The worker provides a fallback via the DEFAULT_ORGANIZATION_ID var in adb-ui/wrangler.jsonc (matching the seeded Pierre's org). Production deploys never hit the proxy code path, so production behaviour is unchanged.
Why every Spring service has extra_hosts: ["localhost:host-gateway"]
The browser logs in via http://localhost:8080/realms/ADB/..., which Keycloak embeds in the JWT's iss claim. The backend validates that JWT against SECURITY_ISSUER_URI, which has to resolve to the same string from inside the container — hence the host-gateway entry. If you change SECURITY_ISSUER_URI to e.g. http://keycloak:8080/..., every API call fails with JwtException: The iss claim is not valid.
Translation bundles (i18n)
adb-utilities exposes /utilities/i18n/<NAME>?language=<lang> for the SPA's MultiTranslateHttpLoader (defined in adb-ui/src/app/content/layout/layout.module.ts). The handler calls ResourceBundle.getBundle(<NAME>, locale, classloader), so each bundle (COMMON, CONTRACTS, PERSONS, ACCOUNTING, PARTS, NOTES, LISTS, DOCUMENTS, SEARCH, TICKET, CATALOG, NACE, JEDI, ADMIN) needs <NAME>_en.properties and <NAME>_fr.properties on the JAR's classpath. The repo only ships JEDI_*.properties — the others come from a private Google Sheet.
Build pipeline
make build-base invokes adb-utilities/pipeline_scripts/prepare-translations.sh inside the build image, before mvn install. The wrapper either:
- Fetches real translations from the prod sheet — when
SPREADSHEET_URLand the 14*_GIDenv vars are set in.env.local, the wrapper callscreate_translations.sh, which downloads each tab as TSV and emits<NAME>_{en,fr}.propertiesintotarget/i18n/. The pom registers that directory as a resource, so the bundles end up on the classpath. - Stubs — when those env vars are missing (the default for any engineer who doesn't have spreadsheet read access), it writes empty
<NAME>_{en,fr}.propertiesfiles./utilities/i18n/<NAME>then returns{}(200) instead of 500. The SPA shows raw translation keys (COMMON.SAVE,CONTRACTS.MANDATE.CREATE, …), which is the current local-dev experience — but no error storm in the console.
Enabling real translations
Get read access to the sheet (https://docs.google.com/spreadsheets/d/12gBJ99fHXDJjY84lx1dprXbj_bAzC5tK), copy the gid for each tab into .env.local (see the env var names in infra/.env.example under "adb-utilities translation bundles"), then re-run make build-base.
The Makefile sources .env.local and forwards the variables as Docker build-args; the Dockerfile re-injects them as env vars for the wrapper RUN step.
Per-environment secrets and env files
The compose stack runs against LocalStack by default — no real credentials needed. To run against the real dev / staging / prod AWS instead (or to seed AWS Secrets Manager from the same values), use the infra/.env.* files:
| File | Purpose | Tracked in git? |
|---|---|---|
infra/.env.example | Schema reference (every key, no values) | ✅ yes |
infra/.env.local | Your local-dev values (against real dev infra if you want) | ❌ gitignored |
infra/.env.staging | Staging values | ❌ gitignored |
infra/.env.prod | Production values | ❌ gitignored |
# Use a specific env file with docker compose
cd infra
docker compose --env-file .env.local -f docker/compose.yaml --profile services up
# Seed AWS Secrets Manager for an env (uses the file you point at)
./scripts/seed-secrets.sh staging .env.staging
The env files contain real credentials. They are chmod 600 and gitignored, but rotate values immediately if any file ever lands in chat / a PR / a screenshot.
Adding a new queue / event
The single source of truth is infra/src/catalog/services.js. After updating the catalog, refresh the queue list in infra/docker/localstack-init/bootstrap.sh and the matching env vars in infra/docker/compose.yaml (the cloud Spring profile reads each queue URL by name).
See also
- Pulumi project overview — what the local stack mirrors.
- EventBridge fanout — how the queues are wired in production.