
はじめに:Windowsだけビルドが遅い
同じSvelteKitプロジェクトをビルドしているのに、MacBook Air M1では3分台、WindowsのDocker環境では8分程度かかっていました。
Windows機のスペックは、AMD Ryzen 9 8945HS、メモリ96GB。MacBook Air M1はメモリ16GBです。単純なスペックだけを見るとWindowsのほうが圧倒的に強そうですが、実際にはMacのほうがかなり速いという結果になりました。
Before
約8分
Windows Docker
Windows側ファイルシステムでビルド
After
3分台
Windows + WSL2
/home配下へ移動してビルド
Reference
約3分
MacBook Air M1
メモリ16GB
🎯 結論
原因はCPUやメモリ不足ではなく、 Windows側ファイルシステム上のプロジェクトをDockerコンテナへマウントしていたこと でした。WSL2のLinuxファイルシステム、つまり/home/ユーザー名 配下へプロジェクトを移すことで、ビルド時間がMacに近い3分台まで改善しました。
この記事では、コマンドプロンプト・PowerShell・WSLの違いから、 そもそもなぜWindows Dockerでビルドが遅くなりやすいのか (ここが一番のポイントです)、そしてWSL2を使ったDocker開発環境の作り方までを、順を追って整理します。
今回の環境
まず、今回比較した環境です。単純なマシンスペックだけで見ると、Windows機のほうがかなり余裕があります。
| 環境 | スペック | ビルド時間 | 補足 |
|---|---|---|---|
| Mac | MacBook Air M1 / Memory 16GB | 約3分 | 比較用 |
| Windows(変更前) | Windows 11 / AMD Ryzen 9 8945HS / Memory 96GB | 約8分 | Windows側のプロジェクトをDockerでビルド |
| Windows(変更後) | 同じWindows機 + WSL2 Ubuntu | 3分台 | /home 配下のプロジェクトをDockerでビルド |
対象プロジェクトは、SvelteKit / Vite / Tailwind CSS v4 / TypeScript / Bun / Docker Compose を使った構成です。Vite や Tailwind CSS のビルドは「細かいファイルを大量に読み書きする」処理が多いため、ファイルシステムの差がビルド時間にそのまま出やすい構成でした。後述しますが、この「大量の細かいファイルアクセス」が今回のボトルネックの正体です。
前提知識:コマンドプロンプト・PowerShell・WSLの違い
Windowsで開発環境を作ると、コマンドプロンプト、PowerShell、WSLという似たような入口が出てきます。「なぜ遅いのか」を理解する土台になるので、まずはそれぞれの役割を整理します。
コマンドプロンプト
昔からあるWindows標準のコマンド実行環境です。cmd.exe と呼ばれ、dir やcopy などWindows系のコマンドを扱います。
PowerShell
Windows向けの高機能なシェルです。管理者作業やスクリプトに強く、wsl --install やwsl --set-default などのWSL操作でもよく使います。
WSL
Windows上でLinux環境を動かす仕組みです。UbuntuなどをWindows内で動かし、Linuxのファイルシステム、bash、apt、git、dockerコマンドを扱えます。
✨ Docker開発ではWSL側を使う
Dockerコンテナは基本的にLinux環境で動きます。そのためWeb開発では、PowerShellやコマンドプロンプトからWindows側のファイルを扱うより、WSLのUbuntu内でプロジェクトを扱ったほうが相性がよくなります。なぜそうなるのかは、次のセクションで掘り下げます。
Windows Dockerでビルドが遅くなりやすい理由
変更前は、Windows側のファイルシステム(C:\Users\... )にプロジェクトを置いた状態でDockerビルドをしていました。図にすると、こういう流れです。
遅くなりやすい構成
Windows側ファイルシステムをDockerへマウントする流れ
Before
C:\Users\...\project
⇢遅⇢
bind mount
⇢
Dockerコンテナ
⇢
bun run build
コンテナがファイルを読むたびに、Windows側ファイルシステムとの「境界」をまたぎます。
なぜ「ファイルシステムをまたぐ」と遅いのか
ここが今回の核心です。少し仕組みの話になりますが、初心者の方こそ押さえておくと、似た問題に出会ったときに自分で原因を切り分けられるようになります。
WSL2は、実は Windows上で動く軽量な仮想マシン(VM) で、本物のLinuxカーネルと独自のLinuxファイルシステム(ext4)を持っています。そしてWSL2から見えるファイルの置き場所には、性能がまったく違う2種類があります。
🟢 Linux側ファイルシステム(速い)
/home/ユーザー名/... 配下。WSL2というVMが自分のディスク(ext4)として直接持っている領域です。Linuxのアプリ(Dockerコンテナ含む)から見ると、地続きの「自宅」のような場所で、ファイルアクセスのオーバーヘッドがほぼありません。
▲ ここに「境界」があり、またぐたびに変換コストがかかる ▼
🔴 Windows側ファイルシステム(遅い)
/mnt/c/... (=WindowsのC:\ )配下。WSLからは見えますが、実体はVMの外にあるWindowsのディスクです。アクセスのたびに、VMの境界を越えてWindowsへ問い合わせる仕組み(9Pというプロトコル)を経由します。
たとえるなら、Linux側は「同じ部屋にある本棚」、Windows側は「隣の建物にいる司書さんに毎回取り寄せを依頼する書庫」のようなものです。1冊や2冊なら違いは気になりません。ところが、 何万回もファイルを開け閉めする処理 になると、1回ごとの取り寄せコストが積み重なって、数分単位の差になります。
ビルドは「小さなファイル」を大量に触る処理
そして、まさにフロントエンドのビルドがこの弱点を突きます。SvelteKit / Vite / Tailwind CSS / TypeScript のビルドでは、次のような大量の細かいファイルアクセスが発生します。
node_modules
依存パッケージが数千〜数万ファイル単位で展開され、ビルド時に大量に読み込まれます。
Vite の依存解決
import を辿りながら多数のモジュールを開き、.vite キャッシュへ細かく書き出します。
Tailwind のクラス検出
ソース全体をスキャンして使われているクラスを抽出するため、ファイル走査が広範囲に及びます。
.svelte-kit / build 出力
中間生成物や成果物を細かく書き出し、その後また読み直す場面も多くあります。
これらの「1回1回は軽いが、回数が膨大な」アクセスすべてに境界をまたぐコストが乗ると、合計で大きな差になります。これが、変更前に約8分かかっていた正体です。
⚠️ ️ CPUやメモリだけでは判断できない
Ryzen 9 + メモリ96GBでも、DockerコンテナからWindows側ファイルシステムを大量に読む構成では、CPUやメモリではなく ファイルI/O(しかも境界をまたぐI/O) がボトルネックになります。今回のように「高スペックWindowsよりMacBook Airのほうが速い」という逆転現象も、これで説明がつきます。
Docker公式のWSL2ベストプラクティスでも、bind mountのファイルI/O性能を最大化するには、ソースコードやbind mount対象のデータをLinuxコンテナ側、つまり WSL2のLinuxファイルシステム側に置くこと が明確に推奨されています。今回の改善は、まさにこの推奨に沿った対応です。
改善のポイント:WSLの /home 配下にプロジェクトを置く
WSLを使うだけでは不十分 です。重要なのは、プロジェクトの実体を/home/ユーザー名 配下、つまり前述のLinux側ファイルシステムに置くことです。
改善後の構成
After
ex) /home/[user_name]/projects/[project_name]
⇢
bind mount
⇢
Dockerコンテナ
⇢
bun run build
Linuxファイルシステム上のプロジェクトを、同じLinux内のコンテナから扱うため、境界をまたがず高速です。
まず30秒でできる診断:いま自分はどこにいるか
「WSLのターミナルを開いている=速い」ではありません。判断のカギは、開いているターミナルではなく プロジェクトの実体がどこにあるか です。まずはpwd で現在地を確認しましょう。
$ pwd
# 🔴 これだと実体はWindows側(遅い)
/mnt/c/Users/[user_name]/projects/[project_name]
# 🟢 これならLinux側(速い)
/home/[user_name]/projects/[project_name]| 配置場所 | 評価 | 理由 |
|---|---|---|
| /home/[user_name]/projects/[project_name] | おすすめ | WSLのLinuxファイルシステム上にあり、境界をまたがない |
| /mnt/c/Users/... | 避けたい | WSLから見えていても実体はWindows側ファイルシステム |
| C:\Users\...\project | 避けたい | Dockerコンテナから大量アクセスすると遅くなりやすい |
✨ 「WSLを使っているか」より「どこに置いているか」
WSLのターミナルを開いていても、pwd の結果が/mnt/c/... なら、まだWindows側ファイルシステム上で作業しています。/home/... 配下に置き直すのがポイントです。
WSL2の導入と基本設定
WSLをまだ使っていない場合は、PowerShellを管理者で開いてインストールします。
Step 1:WSLをインストール
PS> wsl --installディストリビューションを明示してインストールする場合は、以下のように実行します(既定でもUbuntuが入ります)。
PS> wsl --install -d Ubuntu✨ インストール後は再起動を
初回インストール時はWindowsの再起動を求められることがあります。再起動後にUbuntuが自動で立ち上がり、Linux側のユーザー名とパスワードの初期設定を求められます。
Step 2:ディストリビューションを確認
PS> wsl -l -v
NAME STATE VERSION
* Ubuntu Stopped 2VERSION が2 になっていることを確認してください。1 の場合はwsl --set-version Ubuntu 2 で2に変換できます(WSL2でこそファイルI/Oの恩恵が得られます)。
Step 3:デフォルトをUbuntuにする
PS> wsl --set-default UbuntuWSLを完全停止したい場合は、以下を使います(設定変更後の反映や、調子が悪いときの再起動に便利です)。
PS> wsl --shutdown Ubuntuへログインしてホームディレクトリを確認する
Ubuntuへログインしたら、まずユーザー名と現在位置を確認します。
$ whoami
<user_name>
$ cd ~
$ pwd
/home/<user_name> ここが/home/<user_name> になっていればOKです。この配下にプロジェクトを置きます。
$ cd ~
$ mkdir -p projects
$ cd projects✨ Windows側からも見える
WSL内のファイルは、Windowsのエクスプローラーから\\wsl.localhost\Ubuntu\home\<user_name>\projects のように参照できます(従来の\\wsl$\Ubuntu\... 表記も使えます)。ただし、編集や開発作業はこのパスを直接いじるのではなく、VSCodeのRemote WSLから行うのがおすすめです。
Docker DesktopのWSL連携を有効にする
Docker Desktopを使う場合は、WSL2 backendとWSL Integrationを有効にします。
Docker Desktop側で確認する設定
- ✔️ Docker Desktopを起動する
- ✔️ Settings → General → Use the WSL 2 based engine を有効にする
- ✔️ Settings → Resources → WSL Integration → Ubuntu を有効にする
- ✔️ Apply & Restartで反映する
WSL内で以下を実行し、Dockerコマンドが使えることを確認します。
$ docker version
$ docker compose version WSL内にプロジェクトを配置する
Gitリポジトリから取得する場合は、 WSL内の~/projects 配下で cloneします。Windows側でcloneしてWSLから参照する、という遠回りは避けてください。
$ cd ~/projects
$ git clone <repository-url> <project_name>
$ cd <project_name>✨ cloneはWSL内で。改行コードのトラブルも防げる
WSL内でcloneしておくと、速度面だけでなく改行コード(CRLF/LF)の混乱も避けやすくなります。Windows側のGitでcloneするとファイルがCRLFに変換され、Linuxコンテナ内のシェルスクリプトが動かない、といった地味なハマりが起きがちです。
VSCodeで開く場合は、WSL内のプロジェクトディレクトリからcode . を実行します。
$ cd ~/projects/<project_name>
$ code .✨ Remote WSLで開く
VSCode左下にWSL: Ubuntu のような表示が出ていれば、WSL上のプロジェクトを開けています。Windows側からC:\... のフォルダとして開くのではなく、WSL側から開くのがポイントです(VSCode拡張「WSL」が必要です)。
Docker Composeで開発環境を起動する
プロジェクトディレクトリにdocker-compose.yml がある場合は、そのディレクトリでDocker Composeを実行します。
$ cd ~/projects/<project_name>
$ docker compose up -d --buildログを見る場合は以下です。
$ docker compose logs -fコンテナに入る場合は、サービス名を指定します。
$ docker compose exec app bash
# bashがないイメージの場合
$ docker compose exec app sh docker-compose.ymlのvolume設定で注意すること
開発時はソースコードをコンテナへbind mountします。ただし、node_modules や.svelte-kit のようにファイル数が多いディレクトリは、 named volumeに逃がす と安定しやすくなります。ホスト側のファイルでコンテナ内の生成物を上書きしてしまう事故も防げます。
services:
app:
build:
context: .
working_dir: /app/<project_name>
volumes:
- .:/app/<project_name>
- node_modules:/app/<project_name>/node_modules # ← named volumeへ
- sveltekit_cache:/app/<project_name>/.svelte-kit # ← named volumeへ
ports:
- "5160:5160"
command: bun run dev
volumes:
node_modules:
sveltekit_cache:✨ ポイント
.:/app/<project_name> は、WSL上の/home/<user_name>/projects/<project_name> をコンテナへマウントします。Windows側のC:\... ではないため、ここまで整えてはじめてファイルI/Oのボトルネックを避けられます。
SvelteKit / Viteでブラウザからアクセスする場合は、dev serverを コンテナの外から見えるように--host 0.0.0.0 を付けます。さらに、上のdocker-compose.yml で公開しているポート(ここでは5160 )と一致するよう--port も明示しておくと、ポートのズレによる「つながらない」を防げます。
{
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 5160 --mode dev"
}
}💡 ポート番号は3か所をそろえる
「Composeのports (5160:5160 )」「Viteの--port 」「ブラウザでアクセスするURL」の3か所を同じ番号にそろえます。Viteは既定だと5173 で起動するため、--port を省略するとComposeの公開ポートと食い違ってアクセスできなくなります。
Windows側のブラウザからは、通常どおり以下でアクセスできます。
http://localhost:5160 ステージング・本番ビルドはDockerfile内で完結させる
開発中はbind mountでホットリロードを使うのが便利です。一方、ステージング・本番用のビルド資材を作るときは、Dockerfileのbuilder stageでCOPY . . してビルドする構成に寄せると、環境差分を減らしやすくなります。bind mountに依存しない分、CI/CDサーバーなど手元と違う環境でも結果がブレにくくなります。
| 用途 | おすすめ構成 | 理由 |
|---|---|---|
| 開発 | bind mount +bun run dev | ソース変更を即反映しやすい |
| ステージング・本番ビルド | Dockerfile内でCOPY . . → build | 再現性が高く、ホスト依存を減らしやすい |
FROM oven/bun:latest AS builder
WORKDIR /app/<project_name>
# 依存だけ先にコピー → install をキャッシュに乗せる
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# 残りのソースをコピーしてビルド
COPY . .
RUN bun run build:staging✨ 依存のコピーを先にする理由
package.json とbun.lock を先にコピーしてからbun install すると、ソースだけ変更したビルドではinstall 層がキャッシュされ、再ビルドが速くなります。bun.lock はBun 1.2以降で標準になったテキスト形式のロックファイルです(古いbun.lockb を使っている場合はそちらを指定してください)。
よくあるハマりポイント
❓ WSLを使っているのに速くならない
まずpwd を確認します。/mnt/c/Users/... なら、WSLから見えているだけで実体はWindows側ファイルシステムです。/home/ユーザー名/projects/... 配下へプロジェクトを移してください。コピーではなく、WSL内でgit clone し直すのが確実です。
$ pwd
/home/<user_name>/projects/<project_name>❓ wsl -u ユーザー名 でユーザーが見つからない
Windowsのユーザー名とWSL内のLinuxユーザー名は別物です。rootでログインして/etc/passwd を確認し、必要ならadduser で作成します。
$ grep <user_name> /etc/passwd
# 存在しない場合(rootで実行)
# adduser <user_name>
# usermod -aG sudo <user_name>❓ systemd user session の警告が出る
ログインできていて、whoami やpwd が正常なら、開発作業自体は続けられる場合があります。気になる場合は/etc/wsl.conf の[boot] systemd=true 設定や、WSLの更新状況を確認します。
$ whoami
$ pwd
$ ps -p 1 -o comm=
systemd ← systemd が PID 1 なら有効❓ localhostでアクセスできない
Docker Composeのポート公開、SvelteKit/Viteの--host 0.0.0.0 、そして--port がComposeの公開ポートと一致しているかを確認します。コンテナ側だけでlocalhost に閉じている(--host がない)と、Windows側ブラウザからアクセスできません。
まとめ
この記事で学んだこと
- ✔️ WindowsのDockerビルドが遅い場合、まずCPUやメモリよりファイル配置を疑う
- ✔️ WSL2は軽量VMで、
/home(Linux側・速い)と/mnt/c(Windows側・遅い)で性能が大きく違う - ✔️ 遅さの正体は、境界をまたぐファイルI/O。ビルドは小さなファイルを大量に触るため差が出やすい
- ✔️ プロジェクトは
/home/ユーザー名/projects配下に置き、WSL内でgit cloneする - ✔️ Docker ComposeはWSL内のプロジェクトディレクトリから実行する
- ✔️ VSCodeは
code .でRemote WSLとして開く - ✔️
node_modulesや.svelte-kitはnamed volumeに逃がすと安定しやすい - ✔️ Compose・Vite・ブラウザURLのポート番号は3か所そろえる
- ✔️ ステージング・本番ビルドはDockerfile内で完結させると再現性を高めやすい
🚀
「WSLを使う」だけでなく「/home配下に置く」ことが本質
今回の改善で、Windows環境でもMacBook Air M1に近い3分台でビルドできるようになりました。
WindowsでDockerビルドが遅いと感じたら、Dockerfileやマシンスペックを疑う前に、まずpwd でプロジェクトの配置場所を確認するのがおすすめです。
Windows側は遅くなりやすい
/mnt/c は避ける
/home 配下へ配置
Remote WSLで開く
Docker ComposeはWSL内から