๐ŸŒ Detecting your locationโ€ฆ

How to Fix MODULE_NOT_FOUND Error After Deploying Node.js to Production

โฑ๏ธ6 min read  ยท  1,152 words

Error: Cannot find module './Utils/logger' that works perfectly on your machine and fails the moment it deploys. This class of bug has a small number of causes, and nearly all of them come down to a difference between your development environment and the server.

Cause 1: Case Sensitivity (The Most Common)

macOS and Windows use case-insensitive filesystems by default. Linux does not. So require('./Utils/logger') resolves a file named utils/logger.js on your laptop and fails on the server.

// File on disk: src/utils/logger.js

const logger = require('./Utils/logger');   // works on macOS, fails on Linux
const logger = require('./utils/logger');   // โœ… correct everywhere

Find these before deploying by checking what Git actually has recorded, which is authoritative regardless of your local filesystem.

# List tracked paths and eyeball the casing
git ls-files | grep -i utils

# Catch a rename that Git ignored because only the case changed
git config core.ignorecase false
git status

If Git recorded the wrong case, force the rename through an intermediate name.

git mv src/Utils src/utils-tmp
git mv src/utils-tmp src/utils
git commit -m "fix: correct directory casing for case-sensitive filesystems"

The reliable prevention is a CI job that runs on Linux. It catches this on every pull request instead of at deployment.

Cause 2: The Package Is in devDependencies

Production installs skip development dependencies, so anything imported by runtime code must be a regular dependency.

npm ci --omit=dev        # devDependencies are not installed
{
  "dependencies": {
    "express": "^5.0.0"
  },
  "devDependencies": {
    "dotenv": "^17.0.0"     // โŒ but required at runtime in server.js
  }
}
# Move it
npm uninstall dotenv
npm install dotenv

To find every case at once, install production dependencies into a clean directory and start the application.

rm -rf node_modules
npm ci --omit=dev
node dist/server.js

Cause 3: node_modules Copied Into Docker

Copying a locally built node_modules into an image breaks native modules, because binaries compiled for macOS or for your architecture will not load on the container’s platform.

# .dockerignore โ€” essential
node_modules
npm-debug.log
.git
dist
.env
# Dockerfile โ€” install inside the image
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

Copying package*.json before the rest of the source is a deliberate layer-caching choice: dependencies are only reinstalled when the manifest changes, not on every source edit.

Cause 4: Native Modules Built for the Wrong Platform

Packages with compiled components โ€” bcrypt, sharp, canvas, database drivers โ€” produce platform-specific binaries.

Error: Cannot find module '.../node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node'

Rebuild them on the target platform, or install inside the container as above.

npm rebuild bcrypt --build-from-source

# Alpine images need build tools for native compilation
RUN apk add --no-cache python3 make g++

Where a pure-JavaScript alternative exists โ€” bcryptjs instead of bcrypt, for example โ€” using it removes this entire category of deployment problem.

Cause 5: Missing File Extensions in ESM

ES modules require the extension in relative imports. CommonJS does not. Migrating between them surfaces this immediately.

// package.json has "type": "module"
import { logger } from './utils/logger';        // โŒ ERR_MODULE_NOT_FOUND
import { logger } from './utils/logger.js';     // โœ…

In TypeScript compiling to ESM, the import specifier must reference the output file, so you write .js even though the source is .ts.

import { logger } from './utils/logger.js';   // correct โ€” refers to compiled output

Cause 6: TypeScript Path Aliases Not Resolved at Runtime

Aliases in tsconfig.json are a compile-time convenience. The compiler does not rewrite them, so the emitted JavaScript still contains @/utils/logger, which Node cannot resolve.

{
  "compilerOptions": {
    "paths": { "@/*": ["./src/*"] }
  }
}
// Compiles fine, fails at runtime:
// Error: Cannot find module '@/utils/logger'
import { logger } from '@/utils/logger';

Either rewrite the paths after compiling, or register a runtime resolver.

npm install -D tsc-alias

# package.json
"build": "tsc && tsc-alias"

Bundling with esbuild, tsup, or similar also resolves aliases during the build, which is why bundled deployments rarely hit this.

Cause 7: The Build Output Was Not Deployed

A .gitignore or .dockerignore entry for dist is correct โ€” but then the build must run on the server or in CI. Confirm what actually shipped.

# Inspect the running container
docker exec -it <container> ls -la /app/dist
docker exec -it <container> ls -la /app/node_modules | head

Diagnosing Systematically

# 1. Which exact path is Node looking for?
node dist/server.js
# Read the full error โ€” it prints the resolved path it tried.

# 2. Does that path exist on the server?
ls -la /app/dist/utils/

# 3. Is the package installed?
ls /app/node_modules | grep package-name
npm ls package-name

# 4. Trace resolution in detail
NODE_DEBUG=module node dist/server.js 2>&1 | head -50

# 5. Confirm the Node version matches your local one
node --version

NODE_DEBUG=module prints every directory Node checks, which usually makes the problem obvious within a few lines.

Prevention

  • Run CI on Linux so case-sensitivity issues fail before merge
  • Test with npm ci --omit=dev in CI to catch misplaced dependencies
  • Always .dockerignore your node_modules
  • Commit the lockfile and use npm ci, never npm install, in builds
  • Pin the Node major version in the Dockerfile and in engines
  • Prefer pure-JavaScript packages where a native module is not necessary

Frequently Asked Questions

Q: Why does it work locally but not in Docker?
A: Different filesystem case sensitivity, different platform for native binaries, and a different dependency set if you install with dev dependencies locally. All three are eliminated by installing inside the image.

Q: Should I commit node_modules?
A: No. Commit the lockfile and install during the build. Committed dependencies break on platform changes and bloat the repository badly.

Q: npm ci or npm install in production?
A: npm ci. It installs exactly what the lockfile specifies and fails if the manifest and lockfile disagree, which is what you want in a build.

Q: How do I find case mismatches across a large codebase?
A: Build on Linux in CI. That is the only reliable method โ€” local tooling on a case-insensitive filesystem cannot see the problem.

Q: The module is in node_modules but still not found. Why?
A: Usually a nested dependency conflict, a broken symlink from a workspace setup, or a package whose exports field does not expose the subpath you are importing. Check the package’s exports map in its package.json.

Conclusion

Production-only MODULE_NOT_FOUND errors come from environment differences. Check them in order: case sensitivity against Linux, runtime imports sitting in devDependencies, a locally built node_modules copied into the image, native modules compiled for the wrong platform, missing .js extensions under ESM, and unresolved TypeScript path aliases. The structural fix for nearly all of them is the same โ€” build and install inside the target environment, run CI on Linux, and use npm ci with a committed lockfile.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work โ€” which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

โœ๏ธ Leave a Comment

Your email address will not be published. Required fields are marked *