Files
apf_portal/docs/decisions/0030-dockerised-dev-mode.md
T
Julien Gautier 7ab43125a1
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
feat(infra): dockerised full-stack dev mode — apps compose profile (ADR-0030)
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).
2026-05-29 19:46:13 +02:00

10 KiB

status, date, decision-makers, tags
status date decision-makers tags
accepted 2026-05-28 R&D Lead
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, 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-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 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 servicesportal-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 generateprisma 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 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 (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; 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.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/.