Resolve Cannot find module errors for transitive dependencies in containers built with pnpm deploy.
Resolve Cannot find module errors when using require() or node -e inside Docker containers built with pnpm deploy --prod. The pnpm deploy command creates a production-only dependency layout where transitive dependencies are stored under .pnpm/ with hashed directory names and are not hoisted to the top-level node_modules/. Standard Node.js module resolution fails for any package that is not a direct dependency of the deployed project.
node -e "require('knex')" or similar inside a container built with pnpm deploy and getting Cannot find module 'knex'.pg, knex, mysql2) inside a pnpm-deployed container.MODULE_NOT_FOUND error that only occurs inside the Docker container but works fine in the local development environment.node -e in a Dockerfile RUN or entrypoint to execute database setup scripts.Identify whether the module is a direct or transitive dependency: Check the package.json of the deployed workspace. If the module is listed under dependencies, it should be resolvable normally. If it is only a transitive dependency (dependency of a dependency), it will not be hoisted.
Find the full pnpm path: Inside the container or in the deploy output directory, locate the module:
find /app/node_modules/.pnpm -path '*/MODULE_NAME/lib/index.js' -o -path '*/MODULE_NAME/index.js' | head -1
This returns a path like /app/node_modules/.pnpm/[email protected][email protected]/node_modules/knex/lib/index.js.
Use the full path in require(): Strip the trailing entry point file from the found path and use the directory:
const knex = require('/app/node_modules/.pnpm/[email protected][email protected]/node_modules/knex');
Alternative -- use a direct dependency instead: If possible, use a package that IS listed in the workspace's own dependencies. For example, use pg directly instead of going through knex for simple SQL queries:
const { Client } = require('pg');
Alternative -- dynamic path resolution in entrypoint: If the exact version may change, resolve the path dynamically in the entrypoint script:
PG_PATH=$(find /app/node_modules/.pnpm -path '*/pg/lib/index.js' | head -1)
PG_DIR=$(dirname "$PG_PATH")/..
node -e "const { Client } = require('${PG_DIR}'); /* ... */"
Error: Cannot find module 'knex' (or any transitive dependency name) at runtime inside pnpm-deployed containers.require() for database drivers or utilities.--shamefully-hoist in production pnpm configurations solely to work around this issue; it defeats the purpose of pnpm's strict dependency isolation and can introduce phantom dependency bugs.find at runtime to resolve it dynamically so the script survives dependency version bumps.node -e "require('MODULE_NAME')" succeeds inside the running container (using the full path if needed).node_modules/MODULE_NAME layout for transitive dependencies.find is used, it includes error handling for the case where the path is not found (e.g., dependency was removed).Scenario: A d9 Docker image is built with pnpm deploy --prod /app. The entrypoint script runs a database check using knex. At container startup:
Error: Cannot find module 'knex'
Require stack:
- /app/entrypoint-check.js
The same script works in local development because pnpm's workspace hoisting makes knex available at the top level.
Diagnosis: Inside the container, ls /app/node_modules/knex shows "No such file or directory". Running find /app/node_modules/.pnpm -name 'knex' -type d reveals the module exists at /app/node_modules/.pnpm/[email protected][email protected]/node_modules/knex.
Fix applied:
Replaced the direct require with a dynamic path resolution in the entrypoint:
KNEX_PATH=$(find /app/node_modules/.pnpm -path '*/knex/lib/index.js' | head -1)
if [ -z "$KNEX_PATH" ]; then
echo "ERROR: knex module not found in pnpm store"
exit 1
fi
KNEX_DIR=$(dirname "$KNEX_PATH")/..
node -e "const knex = require('${KNEX_DIR}'); /* migration logic */"
Container starts and the migration script executes successfully.
Scenario: pg is a direct dependency and resolves fine. However, a script also needs pg-connection-string (a dependency of pg) to parse a connection URI manually:
const parse = require('pg-connection-string').parse; // MODULE_NOT_FOUND
Diagnosis: pg-connection-string is a transitive dependency of pg and lives inside .pnpm/[email protected]/node_modules/pg-connection-string.
Fix applied:
Instead of requiring pg-connection-string directly, the code was refactored to use pg's built-in connection string support, which handles parsing internally:
const { Client } = require('pg');
const client = new Client({ connectionString: process.env.DB_CONNECTION_STRING });
This avoids the transitive dependency entirely. When refactoring is not possible, the find-based dynamic resolution from Example 1 applies.
Scenario: After upgrading pnpm from 8.x to 9.x, the container build succeeds but the entrypoint fails. The hardcoded path /app/node_modules/.pnpm/[email protected]/node_modules/pg no longer exists because pnpm 9 changed its content-addressable store layout and the hash in the directory name changed.
Diagnosis: find /app/node_modules/.pnpm -path '*/pg/lib/index.js' returns a path with a different hash structure than the one hardcoded in the entrypoint.
Fix applied:
find approach:
PG_PATH=$(find /app/node_modules/.pnpm -path '*/pg/lib/index.js' | head -1)
if [ -z "$PG_PATH" ]; then
echo "FATAL: pg module not found. Check pnpm deploy output."
exit 1
fi