pnpm + Next.js standalone + Docker で5回ハマった話 [第9回]

この記事で分かること pnpmのsymlink構造がNext.js standaloneでなぜ壊れるのか cp -rLで解決できない場合の対処法 Docker multi-stage buildでのsymlink解決パターン 5回のfix PRから得た教訓 背景 Saruは5つのNext.jsフロントエンド(Landing / System / Provider / Reseller / Consumer)をpnpmモノレポで管理している。開発環境ではvolume mountで動かすのでDockerfileは不要だが、本番・デモ環境へのデプロイにはDockerイメージが必要になる。 Next.jsの output: 'standalone' を使えば、必要なファイルだけを .next/standalone/ にトレースしてくれる。これをAlpineベースのrunnerステージにコピーすれば軽量なイメージができる——はずだった。 ハマり1: MODULE_NOT_FOUND(#557) 症状 Error: Cannot find module 'next/dist/compiled/next-server/app-page.runtime.prod.js' コンテナを docker run した瞬間にクラッシュ。 原因 pnpmは node_modules をsymlinkベースで構築する。例えば: standalone/node_modules/next → ../../node_modules/.pnpm/next@14.2.x/node_modules/next Next.jsの @vercel/nft(Node File Tracing)はこのsymlink構造をそのままstandalone出力にコピーする。builderステージ内ではsymlink先が存在するので問題ないが、COPY --from=builder でrunnerステージに持ってくると、symlink先のディレクトリが存在しない。 builder (standalone作成時) runner (COPYした後) ├── standalone/ ├── standalone/ │ └── node_modules/ │ └── node_modules/ │ └── next → ../../.pnpm/... │ └── next → ../../.pnpm/... └── node_modules/.pnpm/... ✅ 存在 └── (なし) ❌ 試した解決策 1 RUN cp -rL /app/apps/system/.next/standalone /app/standalone cp -rL はsymlinkを辿って実ファイルをコピーするPOSIXコマンド。これで一発解決——と思った。 ...

February 16, 2026 · 5 分 · ko-chan

pnpm + Next.js Standalone + Docker: 5 Failures Before Success [Part 9]

What You Will Learn Why pnpm symlinks break in Next.js standalone Docker builds When cp -rL is not enough Symlink resolution patterns in Docker multi-stage builds Lessons from 5 consecutive fix PRs Background Saru manages 5 Next.js frontends (Landing / System / Provider / Reseller / Consumer) in a pnpm monorepo. In development, we use volume mounts so Dockerfiles are not needed. But deploying to production or demo environments requires Docker images. ...

February 16, 2026 · 8 分 · ko-chan

Landmines and Solutions in Self-Hosted CI/CD: 15 Runners x Shared Docker Environment [Part 7]

What You Will Learn Problem and solution patterns in self-hosted runner environments How to prevent resource contention on a shared Docker daemon The port assignment problem killed by the birthday paradox How to investigate when “CI that was working suddenly breaks” Introduction In Part 2, I wrote about automating E2E tests using WebAuthn and Mailpit. The tests themselves work fine. The problem was the CI infrastructure. Saru has 4 frontends x 4 backend APIs. E2E tests run independently for each portal, plus there are cross-portal tests (integration tests between portals). Running these in parallel means 7+ jobs executing simultaneously. ...

February 12, 2026 · 12 分 · ko-chan

セルフホストCI/CDで踏んだ地雷と解決策:15ランナー × 共有Docker環境の闘い【第7回】

この記事で得られること セルフホストランナー環境で起きる問題と対策パターン 共有Docker daemonでのリソース競合の防ぎ方 誕生日のパラドックスに殺されるポート割り当ての話 「動いていたCIが突然壊れる」原因の調査手法 はじめに 第2回で、WebAuthnやMailpitを使ったE2Eテスト自動化について書いた。テスト自体は問題なく動く。問題はCIインフラの方だった。 Saruは4つのフロントエンド × 4つのバックエンドAPIを持つ。E2Eテストは各ポータルごとに独立して実行し、さらにクロスポータルテスト(ポータル間の連携テスト)もある。これらを並列実行すると、7つ以上のジョブが同時に走る。 当初はGitHub-hostedランナーを使っていたが、E2Eテストにはデータベース、Keycloak、メールサーバーが必要で、Dockerコンテナを大量に起動する。GitHub-hostedでは毎回のセットアップに時間がかかり、並列実行のコスト効率も悪い。 そこでセルフホストランナーに移行した。その判断は正しかったが、新しい種類の問題が大量に発生した。 本記事では、セルフホストCI環境で遭遇した問題と、その解決策を時系列で記録する。同じ構成を採用する人の参考になれば幸いだ。 1. Docker Desktop/WSL2が不安定すぎた 最初の構成 最初はWindows上のDocker Desktop(WSL2バックエンド)でランナーを動かしていた。構成はシンプルだ: Windows Host └─ WSL2 └─ Docker Desktop └─ GitHub Actions Runner × N 問題は「コンテナがランダムに死ぬ」こと。E2Eテスト中にPostgreSQLコンテナが突然消えたり、Keycloakが応答しなくなったりする。docker inspectするとExit Code: 137(SIGKILL)で殺されている。 原因を追うと、Docker Desktop/WSL2の仮想化レイヤーが問題だった: コンテナ → Docker Engine → WSL2 → Hyper-V → Windows WSL2自体がHyper-Vの軽量VMとして動いており、そこにさらにDockerが乗る。メモリプレッシャーが高まるとWSL2がOOM Killerを発動し、Dockerコンテナが無差別に殺される。 Hyper-V VMへの移行 解決策として、WSL2を経由せず直接Hyper-V上にUbuntu VMを立てた: コンテナ → Docker Engine → Ubuntu VM → Hyper-V → Windows 項目 値 VM名 saru-ci-runner OS Ubuntu 24.04 vCPU 16 メモリ 64GB ディスク 200GB ネットワーク External Switch(ブリッジ) Hyper-V VMの方が仮想化レイヤーが少なく、メモリ管理もWSL2より安定している。WSL2は動的メモリ割り当てで「ホストと共有」する(デフォルトでホストRAMの50%または8GBまで)が、Hyper-V VMは固定メモリを割り当てるため、OOM Killerに殺されるリスクが低い。 ...

February 12, 2026 · 5 分 · ko-chan