## 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 <julien.gautier@apf.asso.fr> Reviewed-on: #256
10 KiB
status, date, decision-makers, tags
| status | date | decision-makers | tags | ||
|---|---|---|---|---|---|
| proposed | 2026-05-28 | R&D Lead |
|
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, 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:
- 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/pnpmwere "command not found"). - 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-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
.zshrcnvm 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 serveflow 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
nodeimage, 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 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.ymlwith anappsprofile running the three Nx dev servers from one sharedDockerfile.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.devatinfra/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'sforwardPorts). - 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'snode_modules. depends_onthe infra services withcondition: service_healthy(the compose already defines healthchecks for postgres/redis).- BFF entrypoint: install-if-cold →
prisma generate→prisma migrate deploy→ serve. (migrate deploy, notmigrate dev— apply committed migrations only; never auto-author in a container.) - Env points at Compose service names:
postgres:5432,redis:6379,otel-collector:4317— sameapf-portal-devnetwork the devcontainer already joins. - Profile-gated: the services carry
profiles: [apps], so./infra/local/dev.sh upstays infra-only (native devs unaffected) and./infra/local/dev.sh up appsbrings 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 upyields the full stack with zero native toolchain — and an onboarding path that structurally cannot hit the nvm/.zshrcclass 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_modulesin 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 Container Registry work; if a
Dockerfile.devstage 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 rawdocker compose … --profile apps up) and reaches: BFF healthy on:3000,portal-shellon:4200,portal-adminon: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, noapps) 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 uprequest. - 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-devnetwork + 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 serveby hand. It does not deliver the "onedocker compose upruns every server" experience the request is about. - Bad, because it ties the no-toolchain path to VSCode specifically.
More Information
- Complements ADR-0020 (the VSCode devcontainer) — this ADR adds a non-interactive, services-oriented sibling, not a replacement.
- The BFF entrypoint's
prisma generate+migrate deployfollows ADR-0006; migrations are applied, never authored, inside the container. - Production images are out of scope and tracked against the ADR-0028 Container Registry follow-up (post-cutover).
- Builds on the existing
infra/local/dev.compose.ymlprofiles pattern (dbtools,observability,serve-static) —appsis 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 todocs/setup/.