Skip to content

✨ feat(dev): native local dev — infra in Docker, apps via pnpm dev#361

Open
cteyton wants to merge 4 commits into
mainfrom
nix
Open

✨ feat(dev): native local dev — infra in Docker, apps via pnpm dev#361
cteyton wants to merge 4 commits into
mainfrom
nix

Conversation

@cteyton

@cteyton cteyton commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Why

The legacy docker-compose.yml dev stack runs the watched Node apps (api, frontend, mcp-server) inside containers over a bind-mounted source tree. File-system events don't cross the container/VM boundary, so the setup forces polling everywhere (NX_WATCHER: polling, CHOKIDAR_USEPOLLING, …) — laggy HMR, CPU burn — plus a fragile shared Nx-daemon socket. A year of friction.

Insight: Docker is great for stateful infra, bad for hot-reloading Node. This PR splits them.

What

Hybrid setup — infra in Docker, apps native, toolchain pinned by mise:

mise install && pnpm install   # one-time
pnpm dev                        # infra up → migrations → serve api/frontend/mcp native
  • docker-compose.dev.yml — Postgres + Redis only, isolated Compose project packmind-dev
  • mise.toml — pins Node 24.15.0 + pnpm 11.5.0 (corepack)
  • scripts/dev-serve.sh — bakes deterministic localhost wiring; .env overrides win
  • scripts/prepare-local-dev.mjs — tsconfig select + JS-playground seed (idempotent)
  • package.jsondev, dev:infra, dev:infra:down, dev:reset, dev:setup, migrate
  • docs: CLAUDE.md + CONTRIBUTING.md

Result: real fs events, fast HMR, no daemon socket, no polling.

Validated locally

Native stack on Node 24: frontend=200 · api=200 · mcp up · frontend→api proxy=200 · 0 DB-connection failures · MCP "started successfully".

Scope / transition

  • Legacy docker-compose.yml left intact as a fallback. Don't run both at once (ports 5432/6379).
  • Removing the legacy app/daemon services (~250 lines) is a follow-up, gated on team validation of this flow (see LOCAL_DEV_REVAMP_PLAN.md, phase 4).

🤖 Generated with Claude Code

cteyton and others added 3 commits June 17, 2026 17:03
- Add docker-compose.dev.yml running only Postgres + Redis under an
  isolated Compose project "packmind-dev", so app processes no longer
  run in containers over a bind mount
- Add mise.toml pinning Node 24.15.0 and pnpm 11.5.0 via corepack
- Add .env.example documenting optional local overrides
- Add LOCAL_DEV_REVAMP_PLAN.md describing the migration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add pnpm dev / dev:infra / dev:infra:down / dev:reset / dev:setup /
  migrate scripts to package.json
- Add scripts/dev-serve.sh: bake deterministic localhost wiring (an
  optional .env overrides it) and serve api, mcp-server and frontend
  natively via nx run-many
- Add scripts/prepare-local-dev.mjs: select the effective tsconfig and
  seed the local JS playground directory (idempotent)
- Native processes restore real fs events: no Nx daemon socket, no
  file-watch polling

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Rewrite CLAUDE.md and CONTRIBUTING.md "Starting the stack" sections for
  the hybrid setup: infra in Docker, apps native via pnpm dev
- Recommend mise for toolchain pinning; keep nvm + corepack as alternative
- Note the legacy in-container stack as a transitional fallback and the
  port conflict between the two

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the all-in-container dev stack with a hybrid model: only Postgres and Redis run in Docker (docker-compose.dev.yml), while the three Node apps (api, frontend, mcp-server) run natively via pnpm dev. This eliminates the bind-mount fs-event problem that forced polling everywhere and caused slow HMR.

  • docker-compose.dev.yml provides an isolated infra-only Compose project with healthchecks and pinned image versions; mise.toml pins the Node/pnpm toolchain; scripts/dev-serve.sh bakes deterministic localhost wiring with .env-override support.
  • apps/api/project.json conditionally gates legacy-watch polling behind ${WATCH_POLLING:+...}, activated by the new WATCH_POLLING: '1' env var added to the legacy docker-compose.yml backend service, so both stacks work correctly.
  • New root package.json scripts (dev, dev:infra, dev:reset, migrate, dev:setup) compose the full workflow; LOCAL_DEV_REVAMP_PLAN.md documents the phased rollout and rollback strategy.

Confidence Score: 5/5

Safe to merge. All changes are additive — new scripts and compose file — with the legacy docker-compose.yml left intact as a fallback.

The hybrid dev setup is well-structured: infra healthchecks gate migrations, WATCH_POLLING correctly activates legacy polling only in the old container stack, and the exec-replace pattern in dev-serve.sh cleanly hands off to nx. The only gap is that DATABASE_URL overrides in .env are not forwarded to the migration step, but this only affects the edge case documented in .env.example as optional.

package.json — the migrate script does not source .env, so DATABASE_URL overrides are not propagated to migrations.

Important Files Changed

Filename Overview
scripts/dev-serve.sh Correctly sources .env overrides before baking in deterministic localhost defaults, then exec-replaces itself with nx run-many. set -a/set +a pattern properly auto-exports all vars to child processes.
package.json New dev scripts are clean. The migrate script runs against a hardcoded datasource.ts (localhost:5432), so a DATABASE_URL override in .env won't be honored at migration time — only at runtime.
docker-compose.dev.yml Infra-only compose with proper healthchecks, pinned image versions for postgres/redis, and isolated project name. pgadmin (tools profile) uses unpinned latest tag — already flagged in a prior review.
apps/api/project.json Correctly gates legacy-watch polling behind ${WATCH_POLLING:+...} in the sh -lc script; POSIX-compliant expansion works because sh evaluates it in the inner subshell. defaultConfiguration: "development" ensures nx run-many picks the right config.
mise.toml Pins Node 24.15.0 and pnpm 11.5.0 via corepack postinstall; sets PACKMIND_EDITION=oss as a mise env default. Clean and minimal.
scripts/prepare-local-dev.mjs Idempotent setup: runs tsconfig selection and seeds js-playground-local if absent. Uses Node 16.7+ fs.cpSync safely with Node 24.
apps/api/nodemon.json Removes legacyWatch: true since polling is now controlled exclusively via the --legacy-watch CLI flag conditionally passed by project.json.
docker-compose.yml Adds WATCH_POLLING: '1' to the backend service so the legacy in-container stack still activates polling for nodemon, preserving backward compatibility.
.env.example Documents the DATABASE_URL/REDIS_URI override pattern. Since datasource.ts is hardcoded to localhost:5432, a DATABASE_URL override only affects app runtime, not migrations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Dev as Developer
    participant PJ as package.json (pnpm dev)
    participant DC as docker-compose.dev.yml
    participant DS as dev-serve.sh
    participant NX as nx run-many

    Dev->>PJ: pnpm dev
    PJ->>DC: docker compose up -d --wait (dev:infra)
    DC-->>PJ: postgres + redis healthy ✓
    PJ->>PJ: node scripts/prepare-local-dev.mjs (dev:setup)
    Note over PJ: tsconfig select + js-playground seed
    PJ->>PJ: pnpm typeorm migration:run (migrate)
    Note over PJ: datasource.ts hardcoded → localhost:5432
    PJ->>DS: bash scripts/dev-serve.sh
    DS->>DS: source .env (overrides)
    DS->>DS: "apply localhost defaults (:= syntax)"
    DS->>NX: exec nx run-many -t serve -p api mcp-server frontend
    NX-->>Dev: api :3000 · mcp-server :3001 · frontend :4200
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Dev as Developer
    participant PJ as package.json (pnpm dev)
    participant DC as docker-compose.dev.yml
    participant DS as dev-serve.sh
    participant NX as nx run-many

    Dev->>PJ: pnpm dev
    PJ->>DC: docker compose up -d --wait (dev:infra)
    DC-->>PJ: postgres + redis healthy ✓
    PJ->>PJ: node scripts/prepare-local-dev.mjs (dev:setup)
    Note over PJ: tsconfig select + js-playground seed
    PJ->>PJ: pnpm typeorm migration:run (migrate)
    Note over PJ: datasource.ts hardcoded → localhost:5432
    PJ->>DS: bash scripts/dev-serve.sh
    DS->>DS: source .env (overrides)
    DS->>DS: "apply localhost defaults (:= syntax)"
    DS->>NX: exec nx run-many -t serve -p api mcp-server frontend
    NX-->>Dev: api :3000 · mcp-server :3001 · frontend :4200
Loading

Reviews (2): Last reviewed commit: "⚡️ perf(api): use native file watching f..." | Re-trigger Greptile

Comment thread docker-compose.dev.yml
pgadmin:
profiles:
- tools
image: dpage/pgadmin4:latest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The pgadmin service uses the unpinned latest tag while postgres and redis are both version-pinned. On a fresh pull, different developers could get different pgAdmin versions, breaking the consistency goal this PR sets out to achieve. Pinning to a specific version avoids unexpected breakage when pgAdmin ships a breaking UI or config-schema change.

Suggested change
image: dpage/pgadmin4:latest
image: dpage/pgadmin4:9.4

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread CLAUDE.md
- Gate nodemon's --legacy-watch/--polling-interval behind WATCH_POLLING,
  expanded only when the var is set, so native `pnpm dev` uses real fs
  events (~3s reload) instead of 500ms polling
- Remove "legacyWatch": true from apps/api/nodemon.json (default native)
- Set WATCH_POLLING=1 on the legacy docker-compose.yml backend service,
  which still needs polling over the bind mount

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant