feat(infra): dockerised full-stack dev mode — apps compose profile (ADR-0030) #258

Merged
julien merged 1 commits from feat/dockerised-dev-mode into main 2026-06-01 11:11:45 +02:00
Owner

Summary

Implements ADR-0030 (now accepted): a Docker Compose apps profile that runs the three Nx dev servers (portal-bff, portal-shell, portal-admin) from a shared Dockerfile.dev, so a developer can boot the whole stack with no native Node/pnpm:

./infra/local/dev.sh up apps   # infra + portal-bff:3000 + portal-shell:4200 + portal-admin:4300

Purely additive and profile-gated — the native nx serve flow and the devcontainer are untouched. Dev-only; no production images (those stay with the ADR-0028 Container Registry work).

What lands

File Change
docs/decisions/0030-dockerised-dev-mode.md Status proposedaccepted.
docs/decisions/README.md Index status → accepted.
infra/local/Dockerfile.dev New. node:24-bookworm + corepack (pnpm resolved from packageManager at runtime — no pinned version to drift). No COPY/install at build time. NX_DAEMON=false, NODE_OPTIONS=--max-old-space-size=4096.
infra/local/dev-entrypoint.sh New. Shared entrypoint: BFF (APF_ROLE=bff) runs prisma generate + prisma migrate deploy then serves; SPA services go straight to nx serve.
infra/local/dev.compose.yml New apps profile. A one-shot apps-deps service installs into a shared node_modules volume once (the 3 servers gate on its service_completed_successfully, avoiding a 3-way install race); portal-bff / portal-shell / portal-admin services from the shared image via a x-app-base anchor. Repo bind-mounted; node_modules + .nx in named volumes.
infra/local/dev.sh apps added to ALL_PROFILES (so teardown / status / logs catch it) + usage / examples.
infra/README.md New "Dockerised app dev mode" section + cheat-sheet / file-table rows.
docs/setup/01-dev-debian-vm-setup.md "Three dev modes — which when" table at the top of Step 5.
CLAUDE.md Architecture roll-up bullet + ADR-count line + environment-conventions note.

Key design decisions

  • One image, one install. The monorepo means a single Dockerfile.dev + a single pnpm install serves all three apps.
  • node_modules + .nx in named volumes, not bind-mounted. The container's install (native modules — esbuild, @swc/core, Prisma engines, lmdb, @parcel/watcher — built for this image) must never be shadowed by the host's node_modules. The repo source is bind-mounted for hot reload; these two directories are overlaid with named volumes.
  • apps-deps one-shot avoids the install race. Three services sharing one node_modules volume can't all run pnpm install concurrently. A dedicated install service runs first; the three app services depends_on its completion.
  • NX_DAEMON=false in the containers — three containers sharing one workspace would otherwise contend on the Nx daemon.
  • Env wiring. The BFF reuses its own apps/portal-bff/.env (Entra / session / jwks secrets) via env_file: { required: false }; the host-specific URLs (DATABASE_URL / REDIS_URL / OTel endpoint) are overridden in environment: — rebuilt from infra/local/.env creds → Compose service names. Compose environment wins over env_file, so the localhost values in the BFF .env don't leak into the container.
  • BFF still needs its secrets. "No native toolchain" ≠ "no config". apps/portal-bff/.env must exist (same as native dev); required: false lets SPA-only devs up without it (the BFF then fails its own boot validators with a clear message).

Validation on the VM

  • docker compose -f dev.compose.yml --profile apps config validates (YAML, anchors / merge, env interpolation).
  • bash -n clean on dev-entrypoint.sh and dev.sh.
  • Full boot on vm-dev./infra/local/dev.sh up apps brings up postgres / redis / otel + apps-deps (one-shot, exit 0) + portal-bff / portal-shell / portal-admin, all containers report healthy or running.
  • apps-deps populates the shared node_modules volume; the three servers reach their nx serve step without re-installing.
  • Ports published as expected: BFF :3000, portal-shell :4200, portal-admin :4300.
  • ./infra/local/dev.sh up (no apps) unchanged for native devs.

Follow-ups identified during VM validation

  • SPA → BFF reachability from a remote browser. Opening http://<vm-ip>:4200/ from the workstation surfaces a "Backend unreachable" message: the SPA's hardcoded bffApiBaseUrl: 'http://localhost:3000/api' (ADR-0018 build-time env) plus the BFF's CORS_ALLOWED_ORIGINS=http://localhost:4200,… both assume "browser on the same machine as the BFF", which doesn't hold here. Fixed in the stacked follow-up PR feat/spa-dev-proxy (proxy /api in the Angular dev-server + relative bffApiBaseUrl), which lands right after this PR.
  • The OTel HTTP exporter URL (environment.otlpEndpoint) and the cross-SPA links (adminAppUrl, shellAppUrl) remain absolute and hit the same remote-browser limit; not blocking for v1, can be revisited if needed.

Related

  • ADR-0030 — the decision (accepted in this PR's chain).
  • ADR-0020 — the devcontainer this complements.
  • ADR-0028 — production images / Container Registry (deferred).
  • Follow-up branch feat/spa-dev-proxy — the SPA-side proxy fix that makes the dockerised mode usable from a remote browser.
## Summary Implements [ADR-0030](../docs/decisions/0030-dockerised-dev-mode.md) (now `accepted`): a Docker Compose `apps` profile that runs the three Nx dev servers (`portal-bff`, `portal-shell`, `portal-admin`) from a shared `Dockerfile.dev`, so a developer can boot the whole stack with **no native Node/pnpm**: ```bash ./infra/local/dev.sh up apps # infra + portal-bff:3000 + portal-shell:4200 + portal-admin:4300 ``` Purely additive and profile-gated — the native `nx serve` flow and the devcontainer are untouched. Dev-only; no production images (those stay with the ADR-0028 Container Registry work). ## What lands | File | Change | | --- | --- | | `docs/decisions/0030-dockerised-dev-mode.md` | Status `proposed` → `accepted`. | | `docs/decisions/README.md` | Index status → `accepted`. | | `infra/local/Dockerfile.dev` | **New.** `node:24-bookworm` + corepack (pnpm resolved from `packageManager` at runtime — no pinned version to drift). No COPY/install at build time. `NX_DAEMON=false`, `NODE_OPTIONS=--max-old-space-size=4096`. | | `infra/local/dev-entrypoint.sh` | **New.** Shared entrypoint: BFF (`APF_ROLE=bff`) runs `prisma generate` + `prisma migrate deploy` then serves; SPA services go straight to `nx serve`. | | `infra/local/dev.compose.yml` | **New `apps` profile.** A one-shot `apps-deps` service installs into a shared `node_modules` volume once (the 3 servers gate on its `service_completed_successfully`, avoiding a 3-way install race); `portal-bff` / `portal-shell` / `portal-admin` services from the shared image via a `x-app-base` anchor. Repo bind-mounted; `node_modules` + `.nx` in named volumes. | | `infra/local/dev.sh` | `apps` added to `ALL_PROFILES` (so teardown / status / logs catch it) + usage / examples. | | `infra/README.md` | New "Dockerised app dev mode" section + cheat-sheet / file-table rows. | | `docs/setup/01-dev-debian-vm-setup.md` | "Three dev modes — which when" table at the top of Step 5. | | `CLAUDE.md` | Architecture roll-up bullet + ADR-count line + environment-conventions note. | ## Key design decisions - **One image, one install.** The monorepo means a single `Dockerfile.dev` + a single `pnpm install` serves all three apps. - **`node_modules` + `.nx` in named volumes, not bind-mounted.** The container's install (native modules — `esbuild`, `@swc/core`, Prisma engines, `lmdb`, `@parcel/watcher` — built for this image) must never be shadowed by the host's `node_modules`. The repo source is bind-mounted for hot reload; these two directories are overlaid with named volumes. - **`apps-deps` one-shot avoids the install race.** Three services sharing one `node_modules` volume can't all run `pnpm install` concurrently. A dedicated install service runs first; the three app services `depends_on` its completion. - **`NX_DAEMON=false`** in the containers — three containers sharing one workspace would otherwise contend on the Nx daemon. - **Env wiring.** The BFF reuses its own `apps/portal-bff/.env` (Entra / session / jwks secrets) via `env_file: { required: false }`; the host-specific URLs (`DATABASE_URL` / `REDIS_URL` / OTel endpoint) are overridden in `environment:` — rebuilt from `infra/local/.env` creds → Compose service names. Compose `environment` wins over `env_file`, so the localhost values in the BFF `.env` don't leak into the container. - **BFF still needs its secrets.** "No native toolchain" ≠ "no config". `apps/portal-bff/.env` must exist (same as native dev); `required: false` lets SPA-only devs `up` without it (the BFF then fails its own boot validators with a clear message). ## Validation on the VM - [x] `docker compose -f dev.compose.yml --profile apps config` validates (YAML, anchors / merge, env interpolation). - [x] `bash -n` clean on `dev-entrypoint.sh` and `dev.sh`. - [x] **Full boot on vm-dev** — `./infra/local/dev.sh up apps` brings up postgres / redis / otel + `apps-deps` (one-shot, exit 0) + portal-bff / portal-shell / portal-admin, all containers report healthy or running. - [x] `apps-deps` populates the shared `node_modules` volume; the three servers reach their `nx serve` step without re-installing. - [x] Ports published as expected: BFF :3000, portal-shell :4200, portal-admin :4300. - [x] `./infra/local/dev.sh up` (no `apps`) unchanged for native devs. ## Follow-ups identified during VM validation - **SPA → BFF reachability from a remote browser.** Opening `http://<vm-ip>:4200/` from the workstation surfaces a "Backend unreachable" message: the SPA's hardcoded `bffApiBaseUrl: 'http://localhost:3000/api'` (ADR-0018 build-time env) plus the BFF's `CORS_ALLOWED_ORIGINS=http://localhost:4200,…` both assume "browser on the same machine as the BFF", which doesn't hold here. Fixed in the **stacked follow-up PR `feat/spa-dev-proxy`** (proxy `/api` in the Angular dev-server + relative `bffApiBaseUrl`), which lands right after this PR. - The OTel HTTP exporter URL (`environment.otlpEndpoint`) and the cross-SPA links (`adminAppUrl`, `shellAppUrl`) remain absolute and hit the same remote-browser limit; not blocking for v1, can be revisited if needed. ## Related - [ADR-0030](docs/decisions/0030-dockerised-dev-mode.md) — the decision (accepted in this PR's chain). - [ADR-0020](docs/decisions/0020-portal-admin-app.md) — the devcontainer this complements. - [ADR-0028](docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) — production images / Container Registry (deferred). - Follow-up branch `feat/spa-dev-proxy` — the SPA-side proxy fix that makes the dockerised mode usable from a remote browser.
julien added 1 commit 2026-06-01 11:11:25 +02:00
feat(infra): dockerised full-stack dev mode — apps compose profile (ADR-0030)
CI / scan (pull_request) Successful in 3m46s
CI / commits (pull_request) Successful in 3m46s
CI / check (pull_request) Successful in 4m4s
CI / a11y (pull_request) Successful in 4m25s
Docs site / build (pull_request) Successful in 4m37s
CI / perf (pull_request) Successful in 8m43s
7ab43125a1
Add an 'apps' Compose profile that runs the three Nx dev servers
(portal-bff, portal-shell, portal-admin) from a shared Dockerfile.dev,
so the whole stack boots with 'dev.sh up apps' and no native Node/pnpm.

- Dockerfile.dev: node:24-bookworm + corepack (pnpm from packageManager
  at runtime), NX_DAEMON=false. No build-time COPY/install.
- dev-entrypoint.sh: BFF runs prisma generate + migrate deploy then
  serves; SPA services go straight to nx serve.
- dev.compose.yml: one-shot apps-deps installs into a shared
  node_modules volume once (apps gate on its completion, no install
  race); repo bind-mounted, node_modules + .nx in named volumes; BFF
  reuses its own .env (required:false) with host URLs overridden to
  Compose service names.
- dev.sh: apps added to ALL_PROFILES + usage.
- Docs: which-mode-when table (setup doc), apps section (infra README),
  CLAUDE.md roll-up. ADR-0030 flipped to accepted.

Dev-only; production images stay with the ADR-0028 Container Registry
work. Additive + profile-gated — native and devcontainer flows
unchanged. Full-boot validation pending on a Docker host (see PR body).
julien merged commit c080d1ad89 into main 2026-06-01 11:11:45 +02:00
julien deleted branch feat/dockerised-dev-mode 2026-06-01 11:11:48 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#258