ソースを参照

Slim the Docker image: keep migration and seed tools out of the server's modules (#453)

Bernt Christian Egeland 2 週間 前
親
コミット
e467191970
4 ファイル変更、54 行追加、53 行削除
  1. 10 0
      .dockerignore
  2. 3 0
      .github/workflows/deploy-demo.yml
  3. 34 53
      Dockerfile
  4. 7 0
      init-db.sh

+ 10 - 0
.dockerignore

@@ -9,3 +9,13 @@ plans
 public/uploads
 backups
 data/uploads
+.github
+.claude
+.vscode
+e2e
+test-results
+playwright-report
+playwright.config.ts
+vitest.config.ts
+tsconfig.tsbuildinfo
+src/__tests__

+ 3 - 0
.github/workflows/deploy-demo.yml

@@ -168,7 +168,10 @@ jobs:
       - name: Seed demo data
         if: ${{ github.event_name == 'push' || inputs.reseed }}
         run: |
+          # The seed runs from /app/tools, beside the full Prisma client and
+          # tsx the image keeps there for it (see Dockerfile).
           docker exec \
+            -w /app/tools \
             -e DEMO_USER_EMAIL="demo@torqvoice.com" \
             -e DEMO_USER_PASSWORD="demo" \
             -e DATA_ROOT=/app/data \

+ 34 - 53
Dockerfile

@@ -27,6 +27,17 @@ ENV NEXT_TELEMETRY_DISABLED=1
 
 RUN npm run build
 
+# What the container runs beside the server: the Prisma CLI for the migrations
+# at start, and tsx with a full Prisma client for the demo seed. Installed in
+# a folder of its own, with no package.json beside it. Run inside /app, where
+# the standalone build keeps the project's package.json, the same install
+# pulled in every production dependency the project has, replaced the patched
+# next the build had traced, and made the image well over a gigabyte larger.
+FROM base AS tools
+WORKDIR /tools
+RUN npm install --no-save --no-audit --no-fund \
+      prisma@7.6.0 @prisma/client@7.6.0 @prisma/adapter-pg@7.6.0 pg dotenv tsx
+
 # Production image, copy all the files and run next
 FROM base AS runner
 WORKDIR /app
@@ -49,65 +60,35 @@ RUN mkdir .next && chown nextjs:nodejs .next
 COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
 COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
 
-# Copy prisma schema and config for migrations
-COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
-COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./prisma.config.ts
-
 # Copy generated Prisma client
 COPY --from=builder --chown=nextjs:nodejs /app/src/generated ./src/generated
 
-# Give the install below the lockfile the app was built with. Without it npm
-# re-resolves the whole tree against the live registry on every build, so the
-# runtime image drifts from what was tested, and a registry change can break
-# the build with nothing changed here (npm 10 crashed with "Cannot read
-# properties of null (reading 'edgesOut')" resolving vitest's peers this way).
-COPY --from=builder --chown=nextjs:nodejs /app/package-lock.json ./package-lock.json
-
-# Remove Next.js standalone's traced stubs for pg/prisma (tracer copies package.json
-# but not all files), then install the full runtime packages fresh.
-RUN rm -rf \
-      /app/node_modules/pg \
-      /app/node_modules/pg-types \
-      /app/node_modules/pg-pool \
-      /app/node_modules/pg-connection-string \
-      /app/node_modules/pg-protocol \
-      /app/node_modules/pg-int8 \
-      /app/node_modules/pg-cloudflare \
-      /app/node_modules/pgpass \
-      /app/node_modules/postgres-array \
-      /app/node_modules/postgres-bytea \
-      /app/node_modules/postgres-date \
-      /app/node_modules/postgres-interval \
-      /app/node_modules/split2 \
-      /app/node_modules/prisma \
-      /app/node_modules/@prisma \
-      /app/node_modules/.prisma \
-    && npm install --omit=dev prisma@7.6.0 @prisma/client@7.6.0 @prisma/adapter-pg@7.6.0 pg dotenv tsx sharp
-
-# The npm install above re-resolves the dependency tree and replaces the
-# standalone build's PATCHED next package with a fresh unpatched copy from the
-# registry, which silently breaks all WebSocket routes (live updates, work
-# board sync). Re-apply the next-ws patch so the runtime server can accept
-# WebSocket upgrades. next-ws itself is already in the standalone bundle.
+# The server runs on the node_modules Next traced for it. That is exactly what
+# the build ran with: next carrying the next-ws patch and the first-upgrade fix
+# the prepare script applied in the deps stage, and sharp with the binary for
+# this image's platform. Nothing is installed over it, so none of that has to
+# be patched or resolved a second time here.
 #
-# sharp is named in that install for the same reason. It is the only native
-# module in the tree, its binary lives in a platform-specific optional
-# dependency (@img/sharp-linuxmusl-x64 on this image), and re-resolving the
-# tree can leave the traced copy without one. Installing it by name makes npm
-# resolve the binary against the platform the image will actually run on.
-RUN npx next-ws patch --yes
-
-# next-ws reads a route module's exports the moment an upgrade arrives, and
-# Next 16.3 loads route modules lazily, so the first WebSocket connection after
-# every boot died with "The lazy module is still loading" until the fix below
-# is applied to the fresh copy the install above pulled in.
+# The traced node_modules is not a full install, though: several packages are
+# a package.json with their entry point pruned away, and a plain Node script
+# run in /app dies on a missing module. So what runs beside the server lives
+# in /app/tools with its own complete node_modules: the migrations (see
+# init-db.sh) and the demo seed (see .github/workflows/deploy-demo.yml), each
+# next to the files it reads.
+COPY --from=tools --chown=nextjs:nodejs /tools/node_modules ./tools/node_modules
+COPY --from=builder --chown=nextjs:nodejs /app/prisma ./tools/prisma
+COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./tools/prisma.config.ts
+COPY --from=builder --chown=nextjs:nodejs /app/src/generated ./tools/src/generated
+
+# Fail the build here rather than at the first live update or certificate
+# download. WebSocket routes only work on a next that carries the next-ws
+# patch, with next-ws waiting for the lazily loaded route module; a native
+# module that cannot be loaded throws while its route is being evaluated,
+# which reaches the browser as an empty HTTP 500 with nothing to explain it.
 COPY --from=builder --chown=nextjs:nodejs /app/scripts/patch-next-ws-first-upgrade.mjs ./scripts/patch-next-ws-first-upgrade.mjs
+RUN grep -qs "next-ws" node_modules/next/dist/server/next-server.js \
+      || { echo "the traced next is missing the next-ws patch"; exit 1; }
 RUN node scripts/patch-next-ws-first-upgrade.mjs
-
-# Fail the build here rather than at the first certificate download: a native
-# module that cannot be loaded throws while the route module is being
-# evaluated, which reaches the browser as an empty HTTP 500 with nothing in it
-# to explain itself.
 RUN node -e "require('sharp'); console.log('sharp loads')"
 
 # Copy init script

+ 7 - 0
init-db.sh

@@ -17,6 +17,11 @@ until pg_isready -d "$DATABASE_URL" -q; do
 done
 echo "PostgreSQL is ready!"
 
+# The Prisma CLI, the schema and the migrations live in their own folder so
+# the CLI's dependencies stay out of the server's node_modules (see Dockerfile).
+app_dir="$PWD"
+cd "$app_dir/tools"
+
 echo "Applying database migrations..."
 if ! npx prisma migrate deploy 2>/dev/null; then
   # P3005: existing DB without migration history (from db push).
@@ -28,6 +33,8 @@ if ! npx prisma migrate deploy 2>/dev/null; then
 fi
 echo "Migrations applied successfully!"
 
+cd "$app_dir"
+
 # Photo embedding is the one thing here that depends on a native module, and
 # sharp's prebuilt binaries need an x86-64-v2 CPU. Some virtualised hosts do not
 # advertise one — Proxmox's default kvm64 processor model, for instance, reports