c080d1ad89
## 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.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #258
113 lines
10 KiB
Markdown
113 lines
10 KiB
Markdown
---
|
|
status: accepted
|
|
date: 2026-05-28
|
|
decision-makers: R&D Lead
|
|
tags: [infrastructure, process]
|
|
---
|
|
|
|
# Dockerised full-stack dev mode — `compose up` runs the Nx apps alongside infra
|
|
|
|
## Context and Problem Statement
|
|
|
|
Today the local **infrastructure** (PostgreSQL / Redis / OTel Collector) runs in Docker via [`infra/local/dev.compose.yml`](../../infra/local/dev.compose.yml), but the **Nx applications** (`portal-bff`, `portal-shell`, `portal-admin`) run **natively** — requiring Node + pnpm installed on the host (workstation or `vm-dev`). Two frictions follow:
|
|
|
|
1. **Onboarding requires the native toolchain.** nvm + corepack + the pinned pnpm is a setup step that fails in non-obvious ways — a fresh-VM install just hit exactly this (the nvm init never reached the zsh session, so `node` / `pnpm` were "command not found").
|
|
2. **A developer asked to run the whole stack with a single `docker compose up`** — no native toolchain, no IDE attach. The use case is real for frontend-focused work, quick demos, and onboarding.
|
|
|
|
How do we offer a "just run everything in Docker" dev mode **without regressing** the two flows that work today (native `nx serve`; the [ADR-0020](0020-portal-admin-app.md)-era VSCode devcontainer)?
|
|
|
|
## Decision Drivers
|
|
|
|
- **Onboarding friction.** The native toolchain (nvm / corepack / pnpm pin) is the step most likely to break for a new dev; the recent `.zshrc` nvm gap is evidence.
|
|
- **A concrete developer request.** `docker compose up` → all servers, zero native Node/pnpm.
|
|
- **Consistency with the all-in-Docker posture.** Infra is already containerised; the apps are the gap.
|
|
- **No regression.** The native `nx serve` flow and the devcontainer flow serve IDE-integrated development + debugging; both must keep working unchanged.
|
|
- **Hot reload is non-negotiable.** Angular/Vite HMR and NestJS watch mode must work inside the containers, or the mode is useless for actual development.
|
|
- **Stable, recognised tooling only** (CLAUDE.md bar) — Docker Compose + the official `node` image, no exotic dev-orchestration layer.
|
|
- **Dev-only scope.** Production images are a separate concern, already earmarked for the GitLab Container Registry after the [ADR-0028](0028-migrate-cicd-and-git-hosting-to-gitlab.md) cutover. This ADR must not entangle dev DX with deployment artefacts.
|
|
|
|
## Considered Options
|
|
|
|
- **Option A — Status quo.** Native apps + the optional devcontainer; no compose-run apps.
|
|
- **Option B — Extend `dev.compose.yml` with an `apps` profile** running the three Nx dev servers from one shared `Dockerfile.dev` (chosen).
|
|
- **Option C — Per-app Dockerfiles + a separate apps compose file**, layered over the infra compose.
|
|
- **Option D — Lean on the devcontainer alone** — tell the developer to use "Reopen in Container".
|
|
|
|
## Decision Outcome
|
|
|
|
Chosen option: **B — an `apps` Compose profile backed by a single `Dockerfile.dev`**, because it satisfies the `docker compose up` request, removes the native-toolchain dependency for that audience, and — being **profile-gated** — leaves the infra-only mode (and therefore the native and devcontainer flows) untouched.
|
|
|
|
### Shape of the implementation (specified here, built in the follow-up PR)
|
|
|
|
- **One `Dockerfile.dev`** at `infra/local/` (or repo root): `node:24-bookworm` + corepack activating the pinned pnpm. The monorepo means **one image, one install** serves all three apps.
|
|
- **Three Compose services** — `portal-bff`, `portal-shell`, `portal-admin` — all from that image, differing only by command (`pnpm exec nx serve <app> --host 0.0.0.0`) and published port (`3000` / `4200` / `4300`, matching the devcontainer's `forwardPorts`).
|
|
- **Repo bind-mounted** into each service for hot reload; **`node_modules` (and the Nx cache) held in named volumes**, NOT bind-mounted — this is the load-bearing detail: native modules (`esbuild`, `@swc/core`, Prisma engines, `lmdb`, `@parcel/watcher`) must be the ones installed _inside_ the container, never shadowed by the host's `node_modules`.
|
|
- **`depends_on` the infra services with `condition: service_healthy`** (the compose already defines healthchecks for postgres/redis).
|
|
- **BFF entrypoint**: install-if-cold → `prisma generate` → `prisma migrate deploy` → serve. (`migrate deploy`, not `migrate dev` — apply committed migrations only; never auto-author in a container.)
|
|
- **Env** points at Compose service names: `postgres:5432`, `redis:6379`, `otel-collector:4317` — same `apf-portal-dev` network the devcontainer already joins.
|
|
- **Profile-gated**: the services carry `profiles: [apps]`, so `./infra/local/dev.sh up` stays infra-only (native devs unaffected) and `./infra/local/dev.sh up apps` brings up the full stack.
|
|
|
|
### Three coexisting modes — "which when"
|
|
|
|
The follow-up PR documents this table in `docs/setup/`:
|
|
|
|
| Mode | Toolchain on host | Best for |
|
|
| -------------------------- | ------------------ | ------------------------------------------------------------------------------------------ |
|
|
| **Native** (`nx serve`) | Node + pnpm native | Day-to-day dev with the fastest iteration + simplest debugger attach. |
|
|
| **Devcontainer** | none (Docker) | IDE-integrated dev without a native toolchain; VSCode attaches, you run `nx serve` inside. |
|
|
| **Compose `apps` profile** | none (Docker) | "Run everything with one command" — onboarding, frontend-only work, demos, no IDE attach. |
|
|
|
|
### Consequences
|
|
|
|
- **Good**, because `docker compose --profile apps up` yields the full stack with zero native toolchain — and an onboarding path that structurally cannot hit the nvm/`.zshrc` class of failure.
|
|
- **Good**, because it is consistent with the already-containerised infra and is purely opt-in (profile-gated) — native and devcontainer flows are byte-for-byte unchanged.
|
|
- **Good**, because a single image + single install keeps the monorepo's dev container cheap to build and reason about.
|
|
- **Bad**, because it adds a **third** run mode to document and keep working as the apps evolve — mitigated by the explicit "which when" table and by sharing the infra network/healthchecks already in place.
|
|
- **Bad**, because iteration is marginally slower than native (Nx daemon/cache across the container lifecycle; Vite rebuilds) and **debugger attach needs extra wiring** (inspector port + source-map paths) — acceptable because the target audience for this mode is precisely the one that does not need a step-debugger.
|
|
- **Bad**, because `node_modules` in a named volume means a lockfile bump needs a re-install pass (the entrypoint handles it; first run after a bump is slower) and the image + volumes consume host disk.
|
|
- **Neutral**, because this is **dev-only**: it deliberately produces no deployment artefact. Production images land later with the [ADR-0028](0028-migrate-cicd-and-git-hosting-to-gitlab.md) Container Registry work; if a `Dockerfile.dev` stage can be reused there, that is a bonus, not a goal here.
|
|
|
|
### Confirmation
|
|
|
|
- On a host with **no native Node/pnpm**, a fresh checkout runs `./infra/local/dev.sh up apps` (or the raw `docker compose … --profile apps up`) and reaches: BFF healthy on `:3000`, `portal-shell` on `:4200`, `portal-admin` on `:4300`, all connected to postgres/redis/otel.
|
|
- Editing a source file triggers HMR / watch-reload in the relevant container within a few seconds (validated for at least one app of each type — Angular and NestJS).
|
|
- The infra-only invocation (`dev.sh up`, no `apps`) is unchanged — native devs see no difference.
|
|
- The "which mode when" table lands in `docs/setup/`.
|
|
|
|
## Pros and Cons of the Options
|
|
|
|
### Option A — Status quo (native apps + optional devcontainer)
|
|
|
|
- Good, because zero new surface to maintain.
|
|
- Good, because native iteration is the fastest and debugging is simplest.
|
|
- Bad, because it does not answer the `docker compose up` request.
|
|
- Bad, because every new dev still pays the native-toolchain setup cost (the friction that motivated this ADR).
|
|
|
|
### Option B — `apps` profile + shared `Dockerfile.dev` (chosen)
|
|
|
|
- Good, because one command brings up the full stack with no native toolchain.
|
|
- Good, because profile-gating makes it purely additive — no regression to native/devcontainer.
|
|
- Good, because one image/one install fits the monorepo and is cheap.
|
|
- Neutral, because it reuses the existing `apf-portal-dev` network + healthchecks rather than inventing new infra.
|
|
- Bad, because it is a third documented mode and adds the node_modules-volume + debugger-attach caveats above.
|
|
|
|
### Option C — Per-app Dockerfiles + separate apps compose
|
|
|
|
- Good, because per-app images map cleanly onto eventual production images.
|
|
- Bad, because three Dockerfiles + a second compose file is more surface for a monorepo where one install serves all apps — premature for a dev-only mode.
|
|
- Bad, because a separate compose file fragments the "up" experience (two files to coordinate) vs a profile in the existing one.
|
|
|
|
### Option D — Devcontainer only
|
|
|
|
- Good, because it already exists and already removes the native toolchain.
|
|
- Bad, because it is **interactive / IDE-bound** — you reopen in the container and run `nx serve` by hand. It does not deliver the "one `docker compose up` runs every server" experience the request is about.
|
|
- Bad, because it ties the no-toolchain path to VSCode specifically.
|
|
|
|
## More Information
|
|
|
|
- Complements [ADR-0020](0020-portal-admin-app.md) (the VSCode devcontainer) — this ADR adds a non-interactive, services-oriented sibling, not a replacement.
|
|
- The BFF entrypoint's `prisma generate` + `migrate deploy` follows [ADR-0006](0006-persistence-postgresql-prisma.md); migrations are applied, never authored, inside the container.
|
|
- Production images are **out of scope** and tracked against the [ADR-0028](0028-migrate-cicd-and-git-hosting-to-gitlab.md) Container Registry follow-up (post-cutover).
|
|
- Builds on the existing [`infra/local/dev.compose.yml`](../../infra/local/dev.compose.yml) profiles pattern (`dbtools`, `observability`, `serve-static`) — `apps` is one more profile in the same idiom.
|
|
- Accepted; the implementation PR carries the `Dockerfile.dev`, the `apps` Compose profile, the BFF entrypoint, the CLAUDE.md architecture roll-up entry, and the "which mode when" guidance in `docs/setup/`.
|