さよならNext.js 13:DockerとBunがもたらした5つの関門の突破記録

2025 06 23
18

Next.js 14 アップグレード記録:5つの関門を乗り越えたDocker/Bunデバッグ日誌

プロローグ:「すぐに終わるだろう?」

2025年6月、私はNext.js 13を使用しているサービスに新たな活力を吹き込むことを決意しました。目標はささやかなものでした。「Next.js 14にアップグレードして、Turbopackの高速な開発速度を享受しよう!」

2024年3月、私がこの現場に初めて参加したとき、サービスはNext.js 13をベースにしていました。その後、時折マイナーバージョンのアップデートを行ってきましたが、時は流れNext.js 15がリリースされ、13バージョンは2024年10月をもって公式のセキュリティサポートが終了しました。
Next.js Support Policy

これ以上アップグレードを先延ばしにはできない状況。ちょうど他の主要な業務が一段落した6月に、まずは14バージョンへの引き上げ作業に着手しました。

package.jsonのバージョン番号を一つ変えるだけのこの作業が、すぐに終わるだろうと甘く考えていました。しかし、私が直面したのは、ローカルとは異なるDocker環境、bunnpmの微妙な違い、そしてmacOSとLinuxの根本的な差異が生み出す、巨大な混沌の渦でした。

この記事は、私が5つの関門を一つずつ突破していった物語の記録です。


第1の関門:最も身近な裏切り sh: next: not found

🚨 問題発生

まず最初にpackage.jsonのnextのバージョンを14.2.5に上げ、docker compose up --buildを実行しました。しかし、私を迎えたのは冷たいエラーメッセージでした。
sh: next: not found

🤔 原因分析:「なぜ13バージョンでは問題なかったのか?」

この点が最大の疑問でした。既存の設定のままNext.js 13環境ではcommand: npm run devが何の問題もなく動作していたからです。本当の原因は、Next.js 14の依存関係ツリー構造の変化にありました。
bun installでインストールされたnode_modulesの内部フォルダ構造が以前と微妙に変わり、これによりnpmや基本的なシェル(shell)がnextの実行ファイルを安定して見つけられなくなったのです。つまり、「既存の設定が完璧だったのではなく、偶然動作していた脆弱な構造であり、アップグレードによってその脆弱性が露呈した」ということでした。

試行錯誤と解決!

commandをbun run devに統一するだけでは不十分でした。
私はDockerfileに**ENV PATH="/usr/src/app/node_modules/.bin:$PATH"** を追加し、シェルがどのような状況でもnextコマンドの場所を明示的に認識できるように修正することで、この最初の関門を突破することができました。

WORKDIR /usr/src/app
 
# new!!
ENV PATH="/usr/src/app/node_modules/.bin:$PATH"

第2の関門:Storybook v8の逆襲、終わらない型エラー

🚨 問題発生

Next.js 14との互換性のためにStorybookをv7からv8に上げました。npx storybook@8 upgradeを実行すると、ビルドログが数多くのTypeScriptの型エラーと実行時エラーで埋め尽くされました。

🤔 原因分析

原因は、Storybook 8へのバージョンアップに伴い、APIと型定義がより厳格になり、デフォルト設定が変更されたためでした。

試行錯誤と解決!

Storybookの公式ドキュメントを参考に、一つずつ問題となったファイルを修正しました。

1.  argTypesRegexの廃止: .storybook/preview.tsから該当する設定を削除し、args: { actions: { argTypesRegex: '^on[A-Z].*' } }を追加してv8の新しいグローバルアクション方式に変更しました。
2.  autodocs設定の変更: parameters: { docs: { autodocs: 'tag' } }の設定を削除し、preview.tsの最上位レベルにtags: ['autodocs']を追加して、新しいタグ方式に切り替えました。
3.  args型の厳格化: render関数のみで構成されたストーリーでProperty 'args' is missingエラーが発生しました。v8の厳格化された型に合わせて、renderを使用する場合でも{ args: { prop: fn() } }のように明示的にargsを追加して解決しました。
4.  xdg-openエラー: Storybook実行の最後にError: spawn xdg-open ENOENTエラーが発生しました。これはStorybookがコンテナ内でブラウザを開こうとした際、コンテナの基盤である'Alpine Linux'が容量を最小化するためにxdg-openのようなデスクトップユーティリティをデフォルトで含んでいないために発生した問題でした。package.jsonのstorybookスクリプトに--no-openフラグを追加して、この機能を無効化しました。

Migration guide from Storybook 7.x to 8.0


第3の関門:node_modulesを巡る大戦争

🚨 問題発生

Next.jsとStorybookのコードの問題を解決すると、私はより深く根本的な問題の沼に陥りました。既存のワークフローはdocker compose execでコンテナに接続し、bun i --save-text-lockfile(テキストベースのbun.lockを使用中)を実行するものでした。しかし、Next.js 14に上げた後、この方法を使うと、bun.lockファイルは更新されるものの、肝心のnode_modulesフォルダが空っぽになってしまうという致命的な問題が発生しました。ローカルにnode_modulesを生成しようとする全ての試みが、それぞれ異なる、しかし互いに連携したエラーを吐き出して失敗しました。

  • bun.lockを削除後bun installを試みる → No version matching (依存関係の衝突)
  • ローカルでbun installを試みる → SystemFdQuotaExceeded (ファイルシステムの過負荷)
  • インストール成功時 → 望まないbun.lockbファイルが生成される (開発ポリシー違反)

🤔 原因分析

この現象の根本的な原因は、Docker for Macのファイルシステム同期性能の限界でした。bun.lockという「一通の手紙」を書く小さな作業は成功しましたが、Next.js 14の膨大になったnode_modulesという「数万個の小包」を一度に書き込む作業は、Dockerの同期システムに過負荷を引き起こし、失敗してしまったのです。

1.  依存関係地獄: bun.lockという「正解集」がなくなったことで、package.jsonに隠れていたパッケージ間のバージョン衝突問題が水面上に現れました。
2.  ファイルシステムの過負荷: Next.js 14の膨大になったnode_modulesをBunの高速なスピードでローカルにインストールしようとしたため、Docker for Macのファイル同期機能が耐えられませんでした。
3.  ロックファイルの形式: 最新のBunはバイナリ形式のbun.lockbをデフォルトで使用するため、テキスト形式のbun.lockに固執しようとする試みは、絶えず問題を引き起こしました。

試行錯誤と解決!**

私はコンテナ内部でのインストールが不安定であることに気づき、ローカルのMacターミナルから直接bun installを実行する方式に切り替えました。この方法は幸いにもnode_modulesを正常に生成してくれましたが、今度は望まないbun.lockbファイルを作成し、既存のbun.lockポリシーと衝突しました。

テキストベースの bun.lock にこだわっていたのには重要な理由がありました。以前は、GitHub の Dependabot が Bun のバイナリロックファイル(bun.lockb)をサポートしていなかったためです。セキュリティ脆弱性の自動検出のために、これまでテキストベースの bun.lock を使用し、dependabot.yml に enable-beta-ecosystems: trueを設定して、ベータ機能であった bun のサポートを利用していました。
そんな中、この機会に改めて調査してみた結果、ついに公式に Dependabot が bunbun.lockb をサポートし始めたことを知りました。2025年2月13日 bun 公式サポートアップデート
これにより、問題解決の糸口が見え始めました!

早速リーダーに相談し、古くて不安定な bun.lock ポリシーに固執する代わりに、Bun のバイナリ形式である bun.lockb を新しい標準として採用することを提案しました。
この決定のおかげで、もはや--save-text-lockfileのような複雑한フラグは不要になり、公式のインストールコマンドはシンプルな bun install になりました。

🤖 Dependabot の修正と確認方法

# dependabot.yml
 
# 削除!
# enable-beta-ecosystems: true
 
# 修正!
#- package-ecosystem: "npm"
- package-ecosystem: "bun"
  directory: "/web"
  schedule:
    interval: "weekly"

dependabotdependabot

  1. 該当のGitHubリポジトリに移動します。
  2. 上部タブから Insights (インサイト) をクリックします。
  3. 左側のサイドバーから Dependency graph (依存関係グラフ) をクリックします。
  4. 表示されるタブの中から Dependabot タブをクリックします。
  5. Check for updatesボタンをクリックします。
    このボタンを押すと、Dependabot が即座に新しい dependabot.yml の設定に基づいて再スキャンを開始します。スキャン後、このページに何の設定エラーメッセージも表示されなければ、変更された設定が正常に認識されたことを意味します。

第4の関門:デプロイ環境の罠 (feat. マルチステージビルド)

🚨 問題発生

ステージングデプロイ用のDockerfileでビルドしたところ、builderステージでbun: not foundInvalid next.config.jsエラーが同時に発生しました。

🤔 原因分析

1.  bun: not found: デプロイ用のDockerfileはマルチステージビルドを使用していました。bunを使用したinstallerステージとは異なり、builderステージはFROM node:alpineで始まる全く別の環境であり、ここには当然bunがインストールされていませんでした。
2.  Invalid next.config.js: RUN bun run buildが実行されるビルド時点では、COPYでイメージに取り込んだ.envファイルがまだ環境変数として読み込まれておらず、next.config.jsが変数を読み取れなかったためです。

試行錯誤と解決!**

1.  builderステージにinstallerステージからbunの実行ファイルをコピーしてくる**COPY --from=installer /usr/local/bin/bun /usr/local/bin/bun** の一行を追加しました。
2.  dotenvパッケージをインストールし、next.config.jsの最上部でrequire('dotenv').config()を呼び出すことで、設定ファイルが自ら環境変数を読み込むように修正しました。

# build
FROM node:22.15.0-alpine3.20 AS builder
...
# path
ENV PATH="/usr/src/app/node_modules/.bin:$PATH"
# bun copy
COPY --from=installer /usr/local/bin/bun /usr/local/bin/bun
...
RUN bun run build

第5の関門:最終ワークフローの確立

🚨 問題発生

すべての問題を解決しましたが、この複雑な過程を毎回そのまま繰り返すのは困難でした。「では、依存関係を変更するたびに、正確にどうすればいいのか?」という、明確で再現可能な手順が必要でした。

🤔 原因分析

私が最終的に確立したアーキテクチャは、**「ローカルのMacにはVSCode用のnode_modulesを、Dockerコンテナ内には実行用のnode_modulesを別々に配置する」**というものでした。この構造では、依存関係を変更する際に、これら両方の環境を更新する必要があります。

  • ローカルでのbun install: Mac上のnode_modulesbun.lockbファイルを更新します。(VSCodeのため)
  • docker compose up --build: Dockerイメージ内のnode_modulesを更新します。(アプリケーション実行のため)

試行錯誤と解決!

私は、安定的で再現可能な最終的なワークフローを確立し、README.mdに記録しました。

アプリケーションディレクトリ : web

  • ローカル環境の更新 (bun install --cwd)
    いくつかの方法を試した結果、bun自体の機能である--cwdオプションを使用するのが最もクリーンでした。
# package.jsonを修正後、プロジェクトルートでこのコマンドを一つ実行するだけです。
bun install --cwd ./web

このコマンドは、フォルダを移動することなく、プロジェクトルートフォルダを基準にbun installを実行し、ローカルのnode_modulesとbun.lockbを更新してくれます。

  • Docker環境の更新 (docker compose up --build)
    ローカルファイル(package.json, bun.lockb)が更新されたので、--buildオプションを使用してDockerイメージを再ビルドし、コンテナの環境も最新の状態に合わせます。

エピローグ:私が得たもの

様々な問題を解決した末、私はついにNext.js 14へのアップグレードを無事に終えることができました。その過程で、「開発ガイドライン」と信じていたものが、実は現代的な開発環境の足を引っ張る「技術的負債」になり得ること、そしてそれを維持するためにどれだけ多くの隠れた努力が必要かを悟ることができました。

私が最終的に確立したREADME.mdの新しいルールは以下の通りです。

  • 普段の開発時: docker compose up -dでサービスを開始し、コーディングにのみ集中します。
  • 依存関係の変更時:
    1.  package.jsonを修正します。
    2.  プロジェクトルートでbun install --cwd ./webを実行し、ローカルの依存関係を更新します。
    3.  docker compose up --build -dでコンテナを再ビル드して実行します。

package.jsonの数字を一つ変えることが、開発環境全体を見直すきっかけになるとは思いもしませんでした。今回の経験で気づいたのは、本当の伏兵はコードの外にいるという事実でした。目に見えない環境の違い、慣れ親しんだ中に隠されたパッケージマネージャーの特性、そして技術的負債となってしまった過去のルールまで。
結局、予期せぬ問題と格闘しながら過ごしたその「試行錯誤」の時間は、散らばっていた点と点を線で結び、より強固な基盤を築く、未来のための確実な投資でした。