🐳

Yarn 4 移行で出会った落とし穴と Docker 開発環境の再現性を支えるために工夫したこと

に公開

こんにちは、Tsucchiiです。

とあるプロジェクトで Yarn 4 (Berry) へ移行した際、Docker 環境で
yarn prisma が動かない」「node_modules が古いまま」といった問題に直面しました。
この記事では、その原因と対処法、Corepack・Volume運用を通じて得た知見を紹介します。


1. 移行の背景

Next.js + Docker の開発環境で、node_modules名前付きボリューム(named volume) で切り離して管理しています。
(ソースコードは bind mount、依存は named volume という構成)

# docker-compose.yml(抜粋)
services:
  web:
    volumes:
      - ./web:/app
      - node_modules:/app/node_modules
      # ...
volumes:
  node_modules:
  # ...

この構成では、entrypoint.sh でコンテナ起動時に /app/package.json/app/yarn.lock のハッシュを計算し、
前回保存した値と比較します。差分があれば(または .yarn_hash が無ければ)
yarn install --immutable を実行して node_modules を最新化する仕組みになっています。

#!/bin/bash
# web/entrypoint.sh(移行前のイメージ)

if [ -f /app/.yarn_hash ]; then
  OLD_HASH=$(cat /app/.yarn_hash)
  CURRENT_HASH=$(md5sum /app/package.json /app/yarn.lock | sort | md5sum | cut -d' ' -f1)

  if [ "$OLD_HASH" != "$CURRENT_HASH" ]; then
    echo "Dependencies changed, updating node_modules..."
    yarn install --immutable
    echo "$CURRENT_HASH" > /app/.yarn_hash
  fi
else
  yarn install --immutable
  md5sum /app/package.json /app/yarn.lock | sort | md5sum | cut -d' ' -f1 > /app/.yarn_hash
fi

exec "$@"

しかし当時は Dockerfile で Yarn のバージョンを固定していなかったため、
docker-compose up 時に Yarn 1 が実行され、yarn.lock が書き換わる問題が発生。
再現性を高めるために Yarn 4 (Berry) へ統一することにしました。


2. 最初に行った変更 ─ Corepack で Yarn を固定化

「Docker 上でも Yarn 4.6.0 を使う」ため、まず次の対応を行いました。

2-1. Dockerfile で Corepack を有効化

# docker/web/Dockerfile.dev(一部抜粋)
ARG YARN_VERSION=4.6.0

RUN corepack enable && corepack prepare "yarn@${YARN_VERSION}" --activate
RUN YARN_ENABLE_SCRIPTS=false yarn install --immutable

2-2. entrypoint でも Yarn 4 を起動時に有効化

# web/entrypoint.sh(Yarn 4 強制後)
YARN_VERSION=${YARN_VERSION:-4.6.0}

if command -v corepack >/dev/null 2>&1; then
  corepack enable >/dev/null 2>&1
  corepack prepare "yarn@${YARN_VERSION}" --activate >/dev/null 2>&1
fi

# この時点では 後述のPATH 補正はまだ入っていません

この時点で Dockerfile と entrypoint の双方で Yarn 4.6.0 が使われる状態になりました。


3. 変更によって発生した問題

しかし「これで移行完了」とはいかず、複数のトラブルが顕在化しました。

3-1. yarn prisma / yarn nextcommand not found

$ yarn prisma migrate dev
Usage Error: Couldn't find a script named "prisma".

3-2. PATH 補正を忘れると next が見つからない

Corepack で Yarn 4 を有効にしただけでは /app/node_modules/.binPATH に含まれず、
command not found: next が頻発。

3-3. named volume の初期化で古い node_modules がコピーされる

docker-compose down -v → docker-compose up をすると、
新しく追加した依存だけ module not found になるケースが発生(理由は後述)。


4. 各トラブルの原因と解決策

4-1. CLI 呼び出しを npx / yarn exec に統一

Yarn 1 までは yarn prismayarn next のような 省略形 CLI が使えましたが、
Yarn 4 (Berry) ではこの形式が廃止されました。

そこで、package.jsonscriptsnpx に統一し、Makefile や README といったドキュメントも更新します。

- "dev": "prisma generate && next dev --turbopack",
+ "dev": "npx prisma generate && npx next dev --turbopack",
コマンド Yarn 1 Yarn 4
yarn next ✅ 可 ❌ エラー
yarn exec next ✅ 可 ✅ 可
npx next ✅ 可 ✅ 可

4-2. entrypoint で PATH を補正

corepack enable / corepack prepareYarn 4 の実行ファイルを用意する だけで、
シェルの PATH には手を加えません。

Yarn 4(Berry)は yarn run 実行時に一時的に .bin を追加しますが、
exec "$@" で普通のシェルコマンドを起動する場合は PATH に .bin が入らず、
command not found: next になります。

# web/entrypoint.sh(最終形の抜粋)
export PATH="/app/node_modules/.bin:$PATH"

💡 補足: PATH を補正しないと command not found: next エラーが解消されません。


4-3. named volume の初期化を正しく扱う

Docker の named volume は、初回マウント時にイメージ内の /app/node_modules を丸ごとコピー します。
そのため、古いイメージを使って docker-compose up を行うと、古い依存が volume に複製されたままになります。

※ entrypoint のハッシュチェックは「lockfile と node_modules が一致」と誤認し、
yarn install をスキップしてしまいます。

運用ルール

依存を追加・更新したときは必ず以下をセットで実行します。

docker-compose down -v   # volume 削除
docker-compose build     # イメージ再ビルド
docker-compose up -d     # 最新依存で起動

5. Volumes と bind mount の違い

項目 bind mount (./web:/app) named volume (node_modules:/app/node_modules)
中身の実体 ホストのファイルを直接参照 Docker が管理する専用ストレージ
反映タイミング ホスト変更が即反映(ホットリロード向き) 初期化時にイメージからコピー、その後は独立
I/O パフォーマンス macOS/Windows は遅くなりがち Linux 上で完結するため高速
ネイティブモジュール ホストと OS 不一致で不整合発生 コンテナ内ビルドで再現性が高い

named volume を使うと再現性は上がりますが、
“最初にイメージ内の /app/node_modules がコピーされる” という仕組みを理解しておく必要があります。
古い依存を持ったイメージのまま down -v → up すると、そのイメージの内容が volume に複製されてしまいます。


6. CI/CD にも効果 ─ ビルド時間が半分に

Yarn 4 へ移行し、yarn install --immutable に統一した結果、
ステージングデプロイ時間が 12〜15分 → 約7分 に短縮 しました。

主な理由は以下の通りです。

  • Dockerfile から不要な yarn add canvas などを削除し、キャッシュ効率を改善
  • CI ランナーでも Yarn 4 を使用し、lockfile の揺れや再インストールを防止

7. 学びとまとめ

  • Yarn 4 では CLI を npx / yarn exec に統一

  • Corepack で Yarn バージョンを固定し、PATH を補正

    • corepack enable → corepack prepare → export PATH=/app/node_modules/.bin:$PATH
  • named volume 初期化時は イメージが初期値としてコピーされる

    • docker-compose down -v → build → up を徹底し、古い依存を防ぐ

8. トラブルシューティングに役立ったコマンド

# Yarn 4(Berry)が有効か確認
docker-compose run --rm web yarn --version

# Prisma / Next CLI の動作確認
docker-compose run --rm web npx prisma --version
docker-compose run --rm web npx next --version

# named volume の中身を確認(Volume 名は環境に合わせて)
docker volume ls
docker run --rm -v <project>_node_modules:/data alpine ls /data

# 依存を確実に反映
docker-compose down -v
docker-compose build
docker-compose up -d

9. 同じ課題に直面している方へ

Yarn 4 への移行は lockfile 揺れを防ぐ うえで非常に有効ですが、
従来の CLI や Docker 運用とは挙動が異なります。

特に named volume の初期化の仕組み を理解しておくことで、
依存追加時のトラブルを未然に防ぐことができます。

この記事が同じ課題に直面している方の一助になれば幸いです。
質問やフィードバックはお気軽にどうぞ!

Discussion