Laravel 開発環境が 1リクエスト 7 秒から 0.05 秒になるまで — WSL2 移行の記録
こんにちはウェブネーションの山下です。
皆様の開発環境でDocker Desktopを利用していますでしょうか?
私は現在メインの開発環境でWindowsのWSL2で実行されるDockerにローカルのフォルダをマウントさせてマウント先のファイルを編集する形で作業することが多かったのですが、WSLとローカルとの通信で開発環境が重くなってしまいE2Eテストの実行が困難になったりと諸々の問題が発生しておりました。
そこで前々からやろうとは考えていた作業ファイルをWSL内のUbuntsに移行してみまして、大幅な速度改善が出来ましたので記事といたします。

背景と症状
Windows 11 上の Docker Desktop(WSL2 バックエンド)で Laravel 13 を開発していて、ソースを C:\workspace からコンテナにバインドマウントしていました。この構成で 1 リクエストが 7 秒かかっていました。
構成は PHP 8.4 / php-fpm + nginx + PostgreSQL + Filament で、app コンテナは 2,000 ファイル超をインクルードします。症状は 3 つありました。
- ログイン前の登録画面でさえウォーム後で 7 秒。管理画面のログインは 9 秒でした。
- Playwright の E2E(76 件・並列 4)が、実行の先頭でまとめて持ち時間切れになります。一番短いテストでも 72 秒かかりました。
- 過去には
errno=12 (ENOMEM)で PHP がクラスを読めなくなることもありました。バインドマウント越しの dentry / inode キャッシュが数 GB 積み上がるためです。
「開発環境は遅いもの」と受け入れて wsl --shutdown でしのいでいましたが、ある日はそれでも戻らなくなりました。そこでこの日は、推測をやめて全部測ることにしました。
原因の切り分け
遅さのほぼ全部は opcache の再検証でした。
まず疑ったものを順に潰しました。VM のメモリは 7.3 GB 空きで swap も余裕があり、ロードアベレージは 12 コアで 1.8。同居する別プロジェクトのコンテナも遊んでいました。PostgreSQL は 20 クエリで 14 ms。php-fpm の opcache は 1,286 スクリプトをキャッシュしていて、メモリも余っていました。つまりコンパイルはし直していません。
残ったのはファイルシステムです。コンテナ内で stat() を 2,000 回回すと 4.5 秒、つまり 1 件 2.25 ms かかっていました。マウントは 9p(aname=drvfs;path=C:\)で、Windows のドライブを WSL の drvfs が Plan 9 経由で見せている経路です。
ここで opcache.revalidate_freq = 2 と結びつきました。この設定では 2 秒経つと次のリクエストがキャッシュ済みの全ファイルを stat し直します。約 1,300 ファイル × 2.25 ms で約 3 秒、Composer のオートロードや Blade の更新確認の stat を足すと 6 秒前後になります。
検証はコンテナ内に一時の ini を置いて php-fpm を再読み込みするだけで済みました。
| 条件 | /register 1 ページ |
|---|---|
現状(revalidate_freq=2) | 7.0 秒 |
| Composer classmap を最適化 | 7.1 秒(効果なし) |
revalidate_freq=300 を一時適用 | 0.65 秒 |
7 秒のうち 6.3 秒が再検証でした。アプリ自体は 0.65 秒で動いています。
対策:.wslconfig の virtiofs=true(不採用)
virtiofs は速くなりましたが、読み出しが壊れたので使えませんでした(WSL 2.5.9.0・Windows 11 での結果)。
WSL には [wsl2] の virtiofs=true という実験的な設定があり、Windows ドライブの共有を Plan 9 から VirtioFS に替えられます。有効にするとコンテナ内のマウントが 9p から virtiofs に変わり、数字は確かに良くなりました。stat 1 件 2.25 ms → 0.83 ms、/register 7.0 秒 → 2.6 秒、artisan 起動 10.5 秒 → 4.5 秒です。
問題は 2 つ出ました。
- マウント内の全ファイルが root 所有に見えます。chmod は効きますが chown は効きません。Blade は compiled view に
touch()で更新時刻を書きますが、utime は所有者しか呼べず、www-data の php-fpm はビューを触るたびに 500 になります。開発だけ php-fpm を root で動かす回避策で一度は通しました。 - 読み出しが間欠的に壊れます。
vendor/composer/autoload_static.php(1.5 MB)の md5 はホストとコンテナで一致しているのに、コンテナ内のphp -lが実行ごとに別の行(10558、4529、8257、1221)で Parse error になり、PHP のfile_get_contentsの md5 も 3 回に 1 回違う値でした。queue worker と scheduler は起動できず、ログにバイナリのゴミが出ました。
2 番目はデータの完全性の問題なので、速さに関係なく使えません。設定を外して wsl --shutdown で元に戻しました。見分け方は簡単で、コンテナ内で同じファイルの md5sum を数回打って値が揺れたらアウトです。同じ頃に報告されている所有者の不具合(WSL #40719)と自動マウントの不具合(WSL #40790)もあります。
本命: リポジトリを WSL2 Ubuntu の ext4 へ移す
ソースを Ubuntu-24.04 の ~/workspace/docker/toritsugi に置き、Ubuntu のシェルから docker compose を打つようにしました。これで stat が 1 件 1.5 µs になり、遅さの土台が消えました。
手順は次のとおりです。作業時間はテストの実行を含めて約 1 時間でした。
- Docker Desktop の Settings → Resources → WSL integration で Ubuntu-24.04 を有効にします。
- Windows 側で
docker compose down。-vは付けません。DB・S3 エミュレータ・DynamoDB Local は名前付きボリュームなので、compose のname:が同じなら移転後もそのまま繋がります。開発 DB の会員 958 件・取引 175 件は一切失いませんでした。 - Ubuntu に Node 22 を入れ、Windows 側の作業ツリーから
git clone。/mnt/c経由のコピーではなく clone にし、vendorとnode_modulesは作り直します。.envだけ持ち込みます。 docker compose build→composer install→npm install && npm run build→docker compose up -d。Vite のビルドはもう node コンテナを経由しなくて済みます。- PHPUnit 全件と E2E 全件を回して見比べます。
ここで 2 つ踏みました。どちらも「ext4 では所有者が本物になる」ことが原因です。Windows のマウントでは権限が無視されていたので、今までは表に出ませんでした。
1 つ目は、Ubuntu の既定ユーザーが root だったことです。wp-env(お知らせ用の WordPress)は WP-CLI が root での実行を拒みます。一般ユーザーを作って /etc/wsl.conf の [user] default= で既定にし、docker グループに入れてクローンを chown しました。WordPress の DB は移転前に wp db export - で書き出し、新しいインスタンスに wp db import - で流し込みました。
2 つ目は、root が作った laravel.log に www-data が追記できなかったことです。docker compose exec app php artisan test は root で走ります。それが作ったログファイルに php-fpm(www-data)が書けず、ログを 1 行出すリクエストが全部 500 になりました。E2E では SMS 認証コードの送信だけが落ちるという見え方をしました。対処はコンテナの www-data をホストのユーザーと同じ uid 1000 に揃え、app / worker / scheduler をすべて user: www-data で動かすことです。
本番は args を渡さないので uid 33 のままです。php-fpm は非 root でも動き(プールの user 指定は無視される旨の警告が出るだけ)、docker compose exec も同じ uid で走るので、誰が作ったファイルでも所有者が食い違いません。
結果
ページ応答は 140 分の 1、テストは 2〜5 倍速くなり、E2E の持ち時間切れは消えました。すべて同じマシン・同じコードでの実測です。
| 項目 | Windows マウント(9p) | WSL2 Ubuntu(ext4) |
|---|---|---|
| stat 2,000 件 | 4,500 ms | 3 ms |
/register 1 ページ | 7.0 秒 | 0.05 秒 |
/admin/login(Filament) | 8.9 秒 | 0.12 秒 |
php artisan の起動 | 10.5 秒 | 未計測(体感では即時) |
| PHPUnit 全件(1,163 件) | 約 15 分 | 3 分 17 秒 |
| E2E 全件(76 件・並列 4) | 10.7 分・先頭 4 件が持ち時間切れ | 5.0 分・全件成功 |
opcache の revalidate_freq は ext4 なら再検証が数 ms で済むので、元の 2 秒に戻しても支障がありません。
まとめ
いままで数秒かかっていた処理が即時反映されるレベルで実行可能になりました。VS Codeでファイルの読み書きをする際も拡張機能を使用すれば問題なく操作可能です。
WSL + ローカルマウント環境で動作が重く感じている方は作業フォルダをUbuntuへ移行してみてはいかがでしょうか?