From 98c2554570523c07da1dfbee37da34006d983190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sat, 11 Apr 2026 07:54:09 +0200 Subject: [PATCH] fix(docker): reconcile pnpm workspace symlinks at container start The bind mount (.:/app) provides workspace-level node_modules symlinks from the host, but those target the root node_modules/.pnpm store which inside the container is a named volume with different content-addressable hashes. Added `pnpm install --frozen-lockfile` to app-dev-start.sh so symlinks are regenerated against the container's store on every boot. Also adds restart.sh convenience script for image rebuilds. Co-Authored-By: Claude Opus 4.6 --- restart.sh | 50 +++++++++++++++++++++++++++++++++ tooling/docker/app-dev-start.sh | 10 +++++++ 2 files changed, 60 insertions(+) create mode 100755 restart.sh diff --git a/restart.sh b/restart.sh new file mode 100755 index 0000000..531efe7 --- /dev/null +++ b/restart.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# restart.sh — Rebuild the CapaKraken app container from scratch. +# +# When to use: +# - After changing pnpm-lock.yaml (new/removed dependencies) +# - After Prisma schema changes +# - After Dockerfile.dev changes +# - When you see "Cannot find module" errors in the app container +# +# A plain `docker compose restart` only restarts the existing container — it +# does NOT rebuild the image or reinstall dependencies. +# +# How it works: +# The startup script (tooling/docker/app-dev-start.sh) runs `pnpm install` +# at boot to reconcile host-side workspace symlinks with the container's +# pnpm store in the named volume. This script rebuilds the Docker image +# (which re-runs pnpm install into the image layer), then optionally purges +# stale named volumes so the startup install starts fresh. +# +# Usage: +# ./restart.sh # rebuild app image and restart +# ./restart.sh --clean # also remove node_modules + .next volumes +# ./restart.sh --full # --clean + recreate all services (postgres, redis, etc.) + +set -euo pipefail + +PROFILE="full" +SERVICES="app" +CLEAN=false + +for arg in "$@"; do + case "$arg" in + --clean) CLEAN=true ;; + --full) CLEAN=true; SERVICES="" ;; # empty = all services in the profile + esac +done + +echo "==> Stopping app container..." +docker compose --profile "$PROFILE" stop app 2>/dev/null || true + +if $CLEAN; then + echo "==> Removing stale node_modules and .next volumes..." + docker volume rm capakraken_node_modules capakraken_next 2>/dev/null || true +fi + +echo "==> Rebuilding and starting ($( [[ -z "$SERVICES" ]] && echo "all services" || echo "$SERVICES" ))..." +docker compose --profile "$PROFILE" up --build -d $SERVICES + +echo "==> Tailing logs (Ctrl-C to detach)..." +docker compose --profile "$PROFILE" logs -f app diff --git a/tooling/docker/app-dev-start.sh b/tooling/docker/app-dev-start.sh index 9f4aa72..ce96653 100644 --- a/tooling/docker/app-dev-start.sh +++ b/tooling/docker/app-dev-start.sh @@ -8,6 +8,16 @@ until pg_isready -h "${POSTGRES_HOST:-postgres}" -p "${POSTGRES_PORT:-5432}" -q; done echo "Postgres is ready." +# The bind mount (.:/app) provides workspace-level node_modules symlinks from +# the *host*, but those symlinks target the root node_modules/.pnpm store. +# Inside the container, /app/node_modules is a named volume whose pnpm +# content-addressable hashes may differ from the host's. Re-running install +# regenerates the workspace symlinks so they resolve against the container's +# pnpm store. --frozen-lockfile ensures no lock changes; --prefer-offline +# avoids network round-trips when packages are already cached in the volume. +# CI=true suppresses interactive prompts (e.g. "reinstall from scratch?") +CI=true pnpm install --frozen-lockfile + # Regenerate Prisma client (needed after bind-mount overlays the image layer) pnpm --filter @capakraken/db db:generate