From 8a2a99c351d458c27bce2b5975ebf7c7ab798784 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Fri, 29 May 2026 19:06:07 +0200 Subject: [PATCH] docs(decisions): add ADR-0030 dockerised full-stack dev mode (proposed) (#256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Propose **ADR-0030 — Dockerised full-stack dev mode**: an `apps` Compose profile that runs the three Nx apps (`portal-bff`, `portal-shell`, `portal-admin`) from a shared `Dockerfile.dev`, so a developer can `docker compose up` the whole stack with **no native Node/pnpm** on the host. `proposed` status — this PR is the **decision record only**. The `Dockerfile.dev` + the `apps` profile + the BFF entrypoint land in a follow-up PR once the ADR is accepted. ## Why Two frictions motivate it: 1. Infra is already in Docker (`infra/local/dev.compose.yml`), but the Nx apps run natively — every dev pays the nvm/corepack/pnpm setup cost. The recent fresh-VM `.zshrc` nvm gap is a concrete example of that path breaking. 2. A developer asked to run all servers with a single `docker compose up`, no toolchain, no IDE attach. ## Decision (chosen option) An **`apps` Compose profile** backed by **one shared `Dockerfile.dev`** (node:24 + pinned pnpm), because it is purely additive: - `./infra/local/dev.sh up` stays infra-only (native + devcontainer flows unchanged). - `./infra/local/dev.sh up apps` brings up infra + the three dev servers with hot reload. Key design points captured in the ADR (built in the follow-up): one image / one install for the monorepo; repo bind-mounted but `node_modules` + Nx cache in **named volumes** (native-module arch correctness); `depends_on … service_healthy`; BFF entrypoint does `prisma generate` + `migrate deploy`; services point at the existing `apf-portal-dev` Compose network. The ADR documents a "which mode when" table so the **three** dev modes (native / devcontainer / compose-`apps`) coexist without confusion. ## Scope guardrails - **Dev-only.** No production images here — those are tracked against the ADR-0028 Container Registry follow-up (post-cutover). The ADR is explicit that it produces no deployment artefact. - Complements ADR-0020's devcontainer (interactive/IDE) with a non-interactive services sibling; does not replace it. ## Numbering Takes **0030**, not 0029. `0029` is reserved for the cascade/Pléiades/Acteurs+ syncs ADR (referenced by ADR-0026/0027/0028); numbers are never reused, so the dev-mode ADR takes the next free slot. The index gap at 0029 is intentional until that ADR is written. ## What lands | File | Change | | --- | --- | | `docs/decisions/0030-dockerised-dev-mode.md` | New ADR, `proposed`. | | `docs/decisions/README.md` | Index row for 0030. | ## Test plan - [x] MADR 4.0.0 frontmatter; tags drawn from the README vocabulary (`infrastructure`, `process`). - [x] Index updated in the same commit (per the repo's index-maintenance rule). - [ ] R&D Lead review → accept / revise. On acceptance: update the CLAUDE.md architecture roll-up + add the "which mode when" guidance to `docs/setup/`, then open the implementation PR. ## Related - [ADR-0020](docs/decisions/0020-portal-admin-app.md) — VSCode devcontainer (the interactive no-toolchain path this complements). - [ADR-0028](docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) — Container Registry / production images (the deferred prod-image work). - [ADR-0006](docs/decisions/0006-persistence-postgresql-prisma.md) — Prisma; the entrypoint applies (never authors) migrations. --------- Co-authored-by: Julien Gautier Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/256 --- docs/decisions/0030-dockerised-dev-mode.md | 112 +++++++++++++++++++++ docs/decisions/README.md | 1 + 2 files changed, 113 insertions(+) create mode 100644 docs/decisions/0030-dockerised-dev-mode.md diff --git a/docs/decisions/0030-dockerised-dev-mode.md b/docs/decisions/0030-dockerised-dev-mode.md new file mode 100644 index 0000000..49e7675 --- /dev/null +++ b/docs/decisions/0030-dockerised-dev-mode.md @@ -0,0 +1,112 @@ +--- +status: proposed +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 --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. +- Status is `proposed`; on acceptance, update the CLAUDE.md architecture roll-up and add the "which mode when" guidance to `docs/setup/`. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 4bb541f..01851a3 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -72,3 +72,4 @@ ADRs are listed in numerical order. To slice by topic, filter on the `Tags` colu | [0026](0026-person-user-portal-data-model.md) | `Person` golden record + `User` portal-account — portal-side identity model | accepted | `data`, `backend`, `security` | 2026-05-24 | | [0027](0027-portal-side-organisational-hierarchy.md) | Portal-side organisational hierarchy — `Structure` with kind discriminator and nullable FINESS / SIRET | accepted | `data`, `backend` | 2026-05-24 | | [0028](0028-migrate-cicd-and-git-hosting-to-gitlab.md) | Migrate CI/CD + git hosting from Gitea to GitLab self-hosted | accepted | `infrastructure`, `process` | 2026-05-26 | +| [0030](0030-dockerised-dev-mode.md) | Dockerised full-stack dev mode — `compose up` runs the Nx apps alongside infra | proposed | `infrastructure`, `process` | 2026-05-28 |