From d4694875e967db9ad9a3e7d274dc5b882470ba1a Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Fri, 31 Jul 2026 23:07:31 -0400 Subject: [PATCH] fix(dev): keep container dependencies in sync with package-lock The dev stack masked node_modules with an anonymous volume, which persists across `docker compose up --build`. The container kept running whatever was installed the first time it booted, so a dependency added or bumped on the host never reached it. That surfaced as a boot crash naming a missing export from @seamless-auth/types, which reads like a bug in that package rather than a stale install, and it only cleared with `--renew-anon-volumes`. syncDevDeps.sh now compares a hash of package-lock.json against a stamp inside the volume and reinstalls when they differ. The image build writes the stamp too, so a fresh volume does not reinstall what the image already has, and an unchanged lockfile skips the install entirely. The volume is now named rather than anonymous, so it can be inspected and dropped by name. node_modules stays out of the bind mount because the host tree is built for the host's platform, not the container's. --- CONTRIBUTING.md | 13 +++++++++++++ Dockerfile.dev | 4 ++-- docker-compose.dev.yml | 12 +++++++++--- syncDevDeps.sh | 23 +++++++++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) create mode 100755 syncDevDeps.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c8c3f8..c19dd54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,19 @@ the database on first boot), and starts a watcher that reloads on change. No `.e needed: the compose file supplies development defaults. If you do create one, its values win, so you can point at a real messaging transport or OAuth provider without editing the compose file. +The container installs its own `node_modules` into a volume rather than using the one on your +machine, which is built for your platform rather than the container's. Every boot checks that +volume against `package-lock.json` and reinstalls when the two have diverged, so pulling a branch +that adds or bumps a dependency needs nothing beyond a restart. To throw the volume away and +start from a clean install: + +```bash +docker compose -f docker-compose.dev.yml down +docker volume rm seamless-auth-dev_node-modules-dev +``` + +`docker compose -f docker-compose.dev.yml down -v` removes it too, along with the database. + To run the project rather than work on it, use `docker compose up` instead. That uses the published image and also serves the admin console at `/console`, which the dev stack does not bundle. See the Docker Quickstart in [README.md](./README.md). diff --git a/Dockerfile.dev b/Dockerfile.dev index 81b894f..d7eedd1 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -4,8 +4,8 @@ WORKDIR /app RUN apk add --no-cache postgresql-client netcat-openbsd -COPY package.json package-lock.json* ./ -RUN npm ci +COPY package.json package-lock.json* syncDevDeps.sh ./ +RUN sh ./syncDevDeps.sh COPY . . RUN mkdir -p ./keys diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index aaa840d..6a5401c 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -19,13 +19,18 @@ services: - '${API_PORT:-5312}:5312' volumes: - .:/app - - /app/node_modules + # node_modules stays out of the bind mount: the host tree is built for the host's + # platform. syncDevDeps.sh reinstalls into this volume whenever package-lock.json + # has moved on, since the volume itself outlives `up --build`. + - node-modules-dev:/app/node_modules # Dockerfile.dev has no build step, so the compiled entrypoint cannot run from a - # fresh clone. Generate dev signing keys, migrate (creating the database on first - # boot), then run the watcher against the TypeScript sources. + # fresh clone. Install any dependencies added since this container last booted, + # generate dev signing keys, migrate (creating the database on first boot), then + # run the watcher against the TypeScript sources. entrypoint: ['/bin/sh', '-c'] command: - > + ./syncDevDeps.sh && npx tsx src/scripts/initKeys.ts && (npm run migrate:up || (npm run db:create && npm run migrate:up)) && npm run dev:container @@ -88,3 +93,4 @@ services: volumes: pgdata-dev: + node-modules-dev: diff --git a/syncDevDeps.sh b/syncDevDeps.sh new file mode 100755 index 0000000..7460e77 --- /dev/null +++ b/syncDevDeps.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Installs node_modules when it does not match package-lock.json. +# +# The dev stack keeps node_modules in its own volume so the host tree, built for the +# host's platform, never reaches the container. That volume outlives `docker compose +# up --build`, so without this the container keeps running whatever was installed the +# first time it booted, however far package.json has moved on since. +# +# Also run at image build time, so a fresh volume inherits a stamp that matches what +# the image already installed and the first boot does not reinstall it. +set -eu + +STAMP=node_modules/.package-lock-stamp +LOCK_HASH="$(md5sum package-lock.json | cut -d ' ' -f 1)" + +if [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$LOCK_HASH" ]; then + echo "Dependencies match package-lock.json, skipping install." + exit 0 +fi + +echo "Installing dependencies from package-lock.json..." +npm ci --no-audit --no-fund +printf '%s' "$LOCK_HASH" > "$STAMP"