🌀
Railway で Node.js Monorepo Docker デプロイ時に "Cannot find module" エラーが発生する
発生したエラー
Railway にデプロイした Node.js アプリケーションで、以下のエラーが発生しました。
node:internal/modules/cjs/loader:1252
throw err;
^
Error: Cannot find module '/app/backend/dist/index.js'
at Function._resolveFilename (node:internal/modules/cjs/loader:1249:15)
at Function._load (node:internal/modules/cjs/loader:1075:27)
at TracingChannel.traceSync (node:diagnostics_channel:315:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:218:24)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
at node:internal/main/run_main_module:36:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
環境・構成
- デプロイ先: Railway
- Node.js: 20-alpine
- 構成: npm workspaces を使用した monorepo
-
プロジェクト構造:
webapp/ ├── backend/ │ ├── src/ │ ├── dist/ # TypeScript ビルド出力 │ ├── package.json │ └── tsconfig.json └── shared/ ├── src/ ├── dist/ └── package.json
問題の分析
1. ビルドプロセスの確認
ビルド自体は正常に完了していました:
> npm run build --workspace=@hogehoge/shared
> npm run build --workspace=@hogehoge/backend
しかし、実行時にモジュールが見つからないエラーが発生。
2. 根本原因の特定
複数の要因が複合的に作用していました:
原因1: Docker内でのパス解決問題
-
WORKDIR:
/app
-
実行コマンド:
CMD ["node", "dist/index.js"]
-
実際のファイルパス:
/app/backend/dist/index.js
パスの不整合により、Node.js が正しいファイルを見つけられない状態でした。
原因2: Monorepo 依存関係の不完全なコピー
-
@hogehoge/shared
パッケージへの依存関係が Docker 内で正しく解決されない - workspace のシンボリックリンクが Docker 内で機能しない
- shared パッケージのビルド成果物が適切にコピーされていない
原因3: TypeScript ビルド設定の問題
-
tsconfig.json
のexclude
設定が不適切 - 不要なディレクトリがビルド対象に含まれ、出力構造が期待と異なる
解決方針と実装
1. Dockerfile の修正
monorepo 構造に対応し、依存関係を適切に処理するよう修正:
# Backend Build Image
FROM node:20-alpine AS base
# Prepare dependencies
FROM base AS deps
WORKDIR /app
COPY backend/package*.json ./backend/
COPY shared/package*.json ./shared/ 2>/dev/null || echo "No shared package.json found"
RUN cd backend && npm ci --only=production
# Build Stage
FROM base AS builder
WORKDIR /app
COPY . .
RUN cd backend && npm ci
# Build shared first if it exists
RUN if [ -d "shared" ] && [ -f "shared/package.json" ]; then \
cd shared && npm ci && npm run build; \
fi
# Build backend
RUN cd backend && npm run build
# Verify build output
RUN ls -la backend/dist/ && test -f backend/dist/index.js || (echo "❌ backend/dist/index.js not found!" && exit 1)
# Runtime
FROM base AS runner
WORKDIR /app/backend
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 honojs
# Copy backend files to current directory
COPY /app/backend/dist ./dist
COPY /app/backend/package.json ./package.json
# Copy shared artifacts to parent directory (for monorepo dependencies)
COPY /app/shared/dist ../shared/dist 2>/dev/null || echo "No shared dist to copy"
# Copy production dependencies
COPY /app/backend/node_modules ./node_modules
USER honojs
EXPOSE 8787
CMD ["npm", "start"]
2. TypeScript 設定の改善
tsconfig.json
の exclude
設定を修正:
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"drizzle",
"test",
"**/*.test.ts",
"**/*.spec.ts"
]
}
3. package.json の確認
backend/package.json
に適切な scripts を設定:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"main": "dist/index.js"
}
解決のポイント
1. WORKDIR の適切な設定
-
WORKDIR /app/backend
に変更することで、npm start
が正しく動作 - monorepo の依存関係を相対パスで解決可能
2. ビルド順序の明確化
- shared → backend の順序でビルド
- 依存関係を考慮した適切な順序
3. ファイル存在確認の追加
- ビルド後および実行前にファイル存在を確認
- 早期のエラー検出とデバッグ情報の提供
4. npm start の活用
-
CMD ["node", "dist/index.js"]
の代わりにCMD ["npm", "start"]
を使用 - package.json の設定に依存することで、より堅牢な実行環境を構築
参考にした類似事例
- Backstage Issue #24808: 同様の monorepo での Docker 実行エラー
- Zenn記事: Nest.js でのモジュール見つからないエラー: TypeScript ビルド設定の重要性
まとめ
Railway で Node.js のプロジェクトをデプロイする際に「Cannot find module」エラーが発生する場合、主に以下の問題が原因となります:
🔍 主な原因と対処法
1. ファイルの場所がずれている問題
- 何が起きているか: Docker内でファイルを探す場所と、実際にファイルがある場所が違う
-
解決方法:
WORKDIR
(作業ディレクトリ)と実行コマンドのパスを合わせる -
具体例:
WORKDIR /app/backend
+CMD ["npm", "start"]
2. プロジェクト間の依存関係の問題
-
何が起きているか:
backend
がshared
フォルダのコードを使えない - 解決方法: Dockerfileで両方のフォルダを正しくコピーし、適切な順序でビルドする
-
ポイント:
shared
を先にビルド →backend
をビルド
3. TypeScript の設定問題
- 何が起きているか: ビルド時に不要なファイルまで処理してしまい、出力先がおかしくなる
-
解決方法:
tsconfig.json
のexclude
に"dist"
や"drizzle"
を追加 - 理由: 自動生成されるファイルはビルド対象から除外すべき
🛠️ 実践的な解決ステップ
-
まずは確認:
ls -la backend/dist/
でファイルが実際に存在するかチェック -
Dockerfileを修正:
WORKDIR /app/backend
に変更 -
実行方法を変更:
CMD ["npm", "start"]
を使用(より安全) -
package.json確認:
"start": "node dist/index.js"
があることを確認
🚀 Railway での特別な注意点
- ビルドログを確認: Railway のダッシュボードでビルドが成功しているか見る
-
環境変数の確認:
NODE_ENV=production
が設定されているか - Dockerfileの場所: プロジェクトルートに Dockerfile があることを確認
💡 避けるべき落とし穴
- パスの指定で
\
(Windows形式)と/
(Unix形式)を混在させない -
node_modules
やdist
フォルダをGitにコミットしない - Dockerfileで
COPY . .
する前に、不要なファイルが含まれていないか確認
この問題は複数の原因が重なって発生することが多いですが、上記のステップを順番に確認していけば解決できます。特に monorepo(複数のパッケージを1つのリポジトリで管理する構成)では、パッケージ間の依存関係を正しく処理することが最も重要です。
🌐 他のデプロイ環境での発生可能性
この「Cannot find module」エラーは Railway だけでなく、他のプラットフォームでも発生する可能性があります:
発生しやすい環境:
- AWS (ECS/Fargate): Docker ベースのため同様の問題が発生
- Google Cloud Run: コンテナ内でのパス解決問題が共通
- Cloudflare Workers: ES modules の仕組みの違いで類似エラーが発生
- Heroku: monorepo の workspace 処理で問題が起きることがある
比較的発生しにくい環境:
- Vercel: Next.js/Node.js に特化した最適化と monorepo サポートが充実
根本的な原因(monorepo の依存関係処理、TypeScript ビルド設定、モジュール解決パス)は環境に関係なく共通するため、今回の解決方法は他のプラットフォームでも有効です。
Discussion