🌀

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.jsonexclude 設定が不適切
  • 不要なディレクトリがビルド対象に含まれ、出力構造が期待と異なる

解決方針と実装

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 --from=builder --chown=honojs:nodejs /app/backend/dist ./dist
COPY --from=builder --chown=honojs:nodejs /app/backend/package.json ./package.json

# Copy shared artifacts to parent directory (for monorepo dependencies)
COPY --from=builder --chown=honojs:nodejs /app/shared/dist ../shared/dist 2>/dev/null || echo "No shared dist to copy"

# Copy production dependencies
COPY --from=deps --chown=honojs:nodejs /app/backend/node_modules ./node_modules

USER honojs
EXPOSE 8787

CMD ["npm", "start"]

2. TypeScript 設定の改善

tsconfig.jsonexclude 設定を修正:

{
  "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 の設定に依存することで、より堅牢な実行環境を構築

参考にした類似事例

まとめ

Railway で Node.js のプロジェクトをデプロイする際に「Cannot find module」エラーが発生する場合、主に以下の問題が原因となります:

🔍 主な原因と対処法

1. ファイルの場所がずれている問題

  • 何が起きているか: Docker内でファイルを探す場所と、実際にファイルがある場所が違う
  • 解決方法: WORKDIR(作業ディレクトリ)と実行コマンドのパスを合わせる
  • 具体例: WORKDIR /app/backend + CMD ["npm", "start"]

2. プロジェクト間の依存関係の問題

  • 何が起きているか: backendsharedフォルダのコードを使えない
  • 解決方法: Dockerfileで両方のフォルダを正しくコピーし、適切な順序でビルドする
  • ポイント: sharedを先にビルド → backendをビルド

3. TypeScript の設定問題

  • 何が起きているか: ビルド時に不要なファイルまで処理してしまい、出力先がおかしくなる
  • 解決方法: tsconfig.jsonexclude"dist""drizzle"を追加
  • 理由: 自動生成されるファイルはビルド対象から除外すべき

🛠️ 実践的な解決ステップ

  1. まずは確認: ls -la backend/dist/でファイルが実際に存在するかチェック
  2. Dockerfileを修正: WORKDIR /app/backendに変更
  3. 実行方法を変更: CMD ["npm", "start"]を使用(より安全)
  4. package.json確認: "start": "node dist/index.js"があることを確認

🚀 Railway での特別な注意点

  • ビルドログを確認: Railway のダッシュボードでビルドが成功しているか見る
  • 環境変数の確認: NODE_ENV=productionが設定されているか
  • Dockerfileの場所: プロジェクトルートに Dockerfile があることを確認

💡 避けるべき落とし穴

  • パスの指定で\(Windows形式)と/(Unix形式)を混在させない
  • node_modulesdistフォルダを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