From 828b6d71a184e79aefbd856c738e71d87dfc0b32 Mon Sep 17 00:00:00 2001 From: Wuesteon Date: Mon, 8 Dec 2025 19:51:14 +0100 Subject: [PATCH] docs: add pnpm Docker symlinks troubleshooting guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documented the "Cannot find package" error that occurs when pnpm symlinks break in Docker containers due to directory structure changes. Includes: - Root cause explanation (symlinks point to .pnpm store) - Wrong approach (flattening directory structure) - Correct approach (preserve structure + copy .pnpm store) - Debug commands - Trade-offs (image size vs functionality) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- TROUBLESHOOTING.md | 129 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index aafb6b21d..db84b689c 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -16,6 +16,7 @@ Common issues and solutions for the manacore-monorepo. - [Client-Side Calling localhost Instead of Public IP](#problem-5-client-side-calling-localhost-instead-of-public-ip) - [CORS Blocking Cross-Origin Requests](#problem-6-cors-blocking-cross-origin-requests) - [Missing Database Schema](#problem-7-missing-database-schema) + - [pnpm Symlinks Broken in Docker Container](#problem-8-pnpm-symlinks-broken-in-docker-container) --- @@ -922,6 +923,134 @@ docker compose exec -T chat-backend wget -q -O - http://localhost:3002/api/v1/he 12. **Drizzle Kit Interactive Mode:** `drizzle-kit push` prompts for confirmation. Use `--force` flag in CI/CD to skip interactive mode. +13. **pnpm Symlinks in Docker:** pnpm uses symlinks to a central `.pnpm` store. When copying `node_modules` in Docker, you must preserve both the symlinks AND the target directory they point to. See [Problem 8](#problem-8-pnpm-symlinks-broken-in-docker-container). + +--- + +### Problem 8: pnpm Symlinks Broken in Docker Container + +**Symptoms:** + +- Container starts but crashes with: `Cannot find package 'date-fns' imported from /app/build/server/chunks/_page.svelte-xxx.js` +- Error `ERR_MODULE_NOT_FOUND` for packages that ARE in node_modules +- Works locally but fails in Docker production stage +- `ls node_modules/date-fns` shows a symlink pointing to `../../../../../node_modules/.pnpm/...` + +**Root Cause:** + +pnpm uses symlinks to a central `.pnpm` store at the monorepo root. When you copy only the app's `node_modules` in Docker, the symlinks point to paths that don't exist: + +``` +# In builder stage (pnpm workspace): +/app/apps/todo/apps/web/node_modules/date-fns → ../../../../../node_modules/.pnpm/date-fns@4.1.0/node_modules/date-fns + +# In production stage (old broken approach): +/app/node_modules/date-fns → ../../../../../node_modules/.pnpm/... +# ↑ BROKEN! This path doesn't exist because we only copied to /app/ +``` + +**❌ WRONG - Flattening Directory Structure:** + +```dockerfile +# Production stage +FROM node:20-alpine AS production + +WORKDIR /app # ❌ Different from builder structure + +# Copy node_modules (symlinks will be broken!) +COPY --from=builder /app/apps/todo/apps/web/node_modules ./node_modules # ❌ BROKEN +COPY --from=builder /app/apps/todo/apps/web/build ./build +COPY --from=builder /app/apps/todo/apps/web/package.json ./ + +CMD ["node", "build"] +``` + +The symlinks in `node_modules` point to `../../../../../node_modules/.pnpm/...` which resolves to a non-existent path from `/app/`. + +**✅ CORRECT - Preserve Directory Structure + Copy .pnpm Store:** + +```dockerfile +# Production stage +FROM node:20-alpine AS production + +# Keep same directory structure as builder so pnpm symlinks resolve correctly +WORKDIR /app/apps/todo/apps/web # ✅ Same as builder + +# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm) +COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm # ✅ Target of symlinks + +# Copy the app's node_modules (contains symlinks to the pnpm store) +COPY --from=builder /app/apps/todo/apps/web/node_modules ./node_modules # ✅ Symlinks work now + +# Copy built application +COPY --from=builder /app/apps/todo/apps/web/build ./build +COPY --from=builder /app/apps/todo/apps/web/package.json ./ + +CMD ["node", "build"] +``` + +**Why This Works:** + +1. `WORKDIR /app/apps/todo/apps/web` - Production container has same path as builder +2. Symlinks in `./node_modules/` point to `../../../../../node_modules/.pnpm/...` +3. From `/app/apps/todo/apps/web/node_modules/`, going up 5 directories reaches `/app/` +4. `/app/node_modules/.pnpm/` exists because we copied it! + +**How to Debug:** + +```bash +# Check if symlinks are broken in the container +docker exec ls -la node_modules/date-fns +# Shows: date-fns -> ../../../../../node_modules/.pnpm/date-fns@4.1.0/node_modules/date-fns + +# Check if the target exists +docker exec ls -la /app/node_modules/.pnpm/date-fns@4.1.0/ +# If "No such file or directory" → symlink is broken + +# Check the image size (should be ~1GB with .pnpm store, ~50MB without) +docker images | grep todo-web +``` + +**Trade-offs:** + +| Approach | Image Size | Symlinks | Works | +| ------------------------------------- | ---------- | -------- | ----- | +| Copy only app's node_modules | ~50MB | Broken | ❌ | +| Copy app's node_modules + .pnpm store | ~1GB | Working | ✅ | + +The larger image size is the cost of pnpm's deduplication strategy. In a monorepo, this is actually more efficient than copying all dependencies flat. + +**Alternative: Use npm Instead of pnpm in Docker:** + +If image size is critical, you could use npm in the Docker build: + +```dockerfile +# Alternative approach (not recommended for monorepos) +FROM node:20-alpine AS production +WORKDIR /app +COPY --from=builder /app/apps/todo/apps/web/build ./build +COPY --from=builder /app/apps/todo/apps/web/package.json ./ + +# Clean install with npm (flattens dependencies) +RUN npm install --omit=dev + +CMD ["node", "build"] +``` + +⚠️ **Warning:** This may fail with `workspace:*` protocol in package.json dependencies. Only works if all dependencies are published to npm. + +**Affected Files:** + +- `apps/todo/apps/web/Dockerfile` +- `apps/manacore/apps/web/Dockerfile` +- `apps/chat/apps/web/Dockerfile` +- `apps/calendar/apps/web/Dockerfile` +- `apps/clock/apps/web/Dockerfile` + +**Related Commits:** + +- `fd1c0ee6` - fix(docker): preserve pnpm symlink structure in web Dockerfiles + --- ## References