
目次
- TL;DR
- 背景: フルスタック Docker Compose の不満
- 問題 1: ホスト側に root 所有のビルド残骸が積もる
- 問題 2: 反復速度が遅い
- 解決策: 「DB だけ Docker、フロント/バックはホスト」構成へ
- モード A: 日常開発 (今回新設)
- モード B: フルスタック Docker (温存)
- docker-compose.db.yml のミニマム実装
- ハマりどころ: マイグレーションツールは pydantic-settings の .env を読まない
- 起動オーケストレータ
- 効果
- ところで、Docker は今いくら食ってるの?
- 段階的クリーンアップ
- ここまでで一段落…のはずが
- 真犯人: WSL2 の仮想ディスク ext4.vhdx
- ext4.vhdx がどこにあるか
- 圧縮手順
- compact vdisk が 98% で止まる/進まないとき
- 教訓
- ローカル開発の構成について
- Docker のディスク管理について
- スクリプト化について
- まとめ
TL;DR
- フルスタック Docker Compose で開発していたら、bind mount 経由で .venv や pycache が C ドライブに蓄積し、毎回手動掃除していた
- 「DB だけ Docker、フロント/バックはホストで実行」に切り替えたら、ホスト側の残骸が出なくなり、HMR で反復速度も劇的に改善した
- ついでに docker system df を打ったら 130GB 使っていた。docker image prune -a で 26GB、docker builder prune -a で 56GB 回収
- それでも C の空き容量が増えない。原因は WSL2 の仮想ディスク (ext4.vhdx) は中身を消しても自動で縮まない こと。diskpart の compact vdisk で初めて物理サイズが減る
- WSL2 を使う Docker Desktop / Rancher Desktop の利用者は、定期的にこの圧縮を回さないとディスクが食い潰される
背景: フルスタック Docker Compose の不満
Web アプリの典型的な構成 (フロントエンド SPA + バックエンド API + RDB) を、ローカル開発でも本番に近づけたくて、5コンテナを 1 つの Docker Compose で立ち上げていた。
services:
postgres: # DB
migrate: # alembic 等のマイグレーション
backend: # FastAPI / Flask / Express など
frontend: # Vite / Next.js など
nginx: # リバースプロキシ
backend と frontend には、ホットリロードのために bind mount を仕込んでいた。
backend:
volumes:
- ../backend:/app # ホストのソースをコンテナにマウント
- /app/.venv # コンテナ内 .venv をホストから隠す
frontend:
volumes:
- ../frontend:/app
- /app/node_modules
これで一見動いていたが、2 つの問題があった。
問題 1: ホスト側に root 所有のビルド残骸が積もる
bind mount でホストのソースツリーをコンテナの WORKDIR に持ち込むと、コンテナ内のビルド/インストール処理がホストのファイルシステムに書き戻される。 /app/.venv や /app/node_modules を anonymous volume で「マスク」していても、pycache やビルド中間生成物、テストキャッシュなどはホストに漏れ出してくる。
しかもそれらは コンテナ内の root 所有 で書かれるため、Windows 上のエクスプローラーから消そうとすると権限エラーで蹴られたり、削除に時間がかかったりする。 結果、毎回 docker compose down のあとに「謎のフォルダを手で消す」という作業が発生していた。
問題 2: 反復速度が遅い
frontend 側を npm run build && serve -s dist 形式で動かしていたため、コード 1 行直すたびにフルビルド。HMR は効かない。開発体験として完全に劣化していた。
解決策: 「DB だけ Docker、フロント/バックはホスト」構成へ
ローカル開発では、本番と同じ構成を再現することよりも フィードバックループの速さと環境のクリーンさ を優先したい。 本番相当の検証は CI や別環境でやればいい。
そこで構成を 2 つに分けた。
モード A: 日常開発 (今回新設)
ブラウザ
└─ http://localhost:5173 Vite dev server (HMR)
└─ /api/* → http://localhost:8000 (Vite の proxy 設定で転送)
└─ uvicorn (--reload)
└─ DATABASE_URL=postgresql://...@localhost:5432/...
└─ Docker container (postgres only)
- DB は docker-compose.db.yml という別ファイルで起動。named volume だけ を使い、bind mount は一切使わない → ホストにファイルが落ちない
- フロントは npm run dev (Vite dev server)。vite.config.ts の server.proxy で /api/* をバックエンドに転送するので、ローカル開発に nginx は要らない
- バックエンドは uvicorn --reload で起動。.env で DATABASE_URL を localhost:5432 に向ける
- Python 3.x や Node が必要だが、uv なら Python のバージョン管理まで丸ごと面倒を見てくれるので、システム Python を汚さない
モード B: フルスタック Docker (温存)
既存の docker-compose.yml (5 コンテナ版) はそのまま残しておく。本番デプロイ前のスモークテストや、別マシンに環境を持っていくときに便利だから。 ポイント: 既存 compose を消すのではなく、別ファイルで共存させる。 docker compose -f docker-compose.db.yml up -d のように、明示的に切り替える。
docker-compose.db.yml のミニマム実装
services:
postgres:
image: postgres:16-alpine
container_name: myapp-postgres-dev
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
- postgres-dev-data:/var/lib/postgresql/data # named volume → ホストに何も出ない
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
volumes:
postgres-dev-data:
ハマりどころ: マイグレーションツールは pydantic-settings の .env を読まない
バックエンドの設定読み込みに pydantic-settings を使っていると、Settings(env_file=".env") で勝手に環境変数が読まれて気持ちいい。 ところが Alembic のような独立したツールは別プロセスで動くため、alembic env.py の中で os.environ.get("DATABASE_URL") を直接読んでいる。 .env は 無視される。
そのまま alembic upgrade head を打つと、デフォルトの postgresql://...@db:5432/... (Docker network 内のホスト名) のままで「db ホストが解決できない」と落ちる。
対処: ローカル起動スクリプトの中で .env を手動でパースしてプロセスの環境変数に注入する。
PowerShell の例:
if (Test-Path ".env") {
Get-Content ".env" | ForEach-Object {
if ($ -match '^\s*([^#=]+?)\s*=\s*(.*)$') {
[Environment]::SetEnvironmentVariable($matches[1], $matches[2], "Process")
}
}
}
bash なら set -a; source .env; set +a の 1 行で済む。要するに「シェル経由でちゃんと export してから子プロセスを起動する」のが正しい姿。
起動オーケストレータ
PowerShell スクリプトを 5 本作って、dev-up.ps1 を叩けば DB → backend (別窓) → frontend (別窓) が立ち上がるようにした。 停止は dev-down.ps1 で DB だけ止める (フロント/バックの窓は手動で閉じる)。
# dev-up.ps1 抜粋
& "$PSScriptRoot\dev-db.ps1"
Start-Process powershell -ArgumentList "-NoExit","-File","$PSScriptRoot\dev-backend.ps1"
Start-Sleep -Seconds 3
Start-Process powershell -ArgumentList "-NoExit","-File","$PSScriptRoot\dev-frontend.ps1"
3 秒スリープを挟むのは、フロントが起動時にバックエンドへ ping を撃つようになっているとレース状態になるから。 気にならなければ不要。
効果
- ホスト側に root 所有ファイルが 新規生成されなくなった
- HMR が効くので、フロントエンドのコード変更がほぼ瞬時に反映される
- バックエンドも --reload でホットリロード
- DB のデータは named volume に隔離されているので、docker compose down -v で意図的に消さない限り永続化される
ところで、Docker は今いくら食ってるの?
新構成に切り替えたあと、ふと気になって叩いた。
> docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 425 20 58.49GB 26.23GB (44%)
Containers 40 15 86.85MB 86.85MB (99%)
Local Volumes 68 11 15.56GB 14.10GB (90%)
Build Cache 603 0 56.71GB 85.71MB
合計約 130GB。 Build Cache が 56GB あって、しかも全部 reclaimable (削除可能)。 これは即やる。
段階的クリーンアップ
リスクの低い順に。
1. ビルドキャッシュ削除 (最大効果・リスクほぼゼロ)
docker builder prune -a
次回ビルドが少し遅くなるだけで実害なし。56GB 回収。
2. 未使用イメージ削除
docker image prune -a
実行中コンテナが参照していないイメージを全て削除。 「過去にちょっと使った古いタグ」がごっそり消える。約 26GB 回収見込み。
これは時間がかかる。 425 イメージあると数十分かかることもある。 途中で docker images を別ウィンドウで叩けば進行中か確認できる。 途中で Ctrl+C しても、それまでに消えたイメージは戻らないだけで安全。
3. 未使用ボリューム削除 (注意が必要)
docker volume prune -a
DB データなど、永続化したいボリュームが消える可能性がある。 実行前に必ず docker volume ls で何が消えるか確認すること。 稼働中のコンテナがマウントしているボリュームは自動的に保護される。
4. システム全体を一発で
docker system prune -a --volumes
上記をまとめて実行する強力版。動作中以外の全部を消す。意味を完全に理解してから打つこと。
ここまでで一段落…のはずが
3 つ実行して合計 90GB 以上消したはず。 ところが C ドライブの空き容量はほとんど増えない。
真犯人: WSL2 の仮想ディスク ext4.vhdx
Docker Desktop も Rancher Desktop も、Windows 上では WSL2 のディストロの中で動いている。 コンテナのイメージ・ボリューム・キャッシュは、WSL2 の仮想ディスクファイル ext4.vhdx の中に格納される。
ここで重要な事実:
ext4.vhdx は「拡張可能」だが「自動縮小しない」。
中の Linux ファイルシステムから 90GB のファイルを消しても、ホスト側の .vhdx ファイルサイズは大きいまま据え置かれる。
つまり docker system prune は仮想ディスクの 中の使用量 を減らすだけで、ホスト Windows から見たファイルサイズは変わらない。 空き容量を Windows 側に返すには、明示的に圧縮する必要がある。
ext4.vhdx がどこにあるか
ツールによって場所が違う。総当たりで探すのが早い。
dir "$env:LOCALAPPDATA" -Recurse -Filter *.vhdx -ErrorAction SilentlyContinue
よくある場所:
| ツール | パス |
|---|---|
| Docker Desktop | %LOCALAPPDATA%\Docker\wsl\data\ext4.vhdx |
| Rancher Desktop | %LOCALAPPDATA%\rancher-desktop\distro-data\ext4.vhdx |
| 素の WSL ディストロ | %LOCALAPPDATA%\Packages\<ディストロのパッケージ名>\LocalState\ext4.vhdx |
筆者の環境 (Rancher Desktop 利用) では rancher-desktop\distro-data\ext4.vhdx が 89GB あった。 これが本命。
圧縮手順
# 1\. Docker Desktop / Rancher Desktop をタスクトレイから完全終了
# 2\. WSL を完全停止
wsl --shutdown
# 3\. すべてのディストロが Stopped になっているか確認
wsl -l -v
# 4\. 管理者権限の PowerShell で diskpart を起動
diskpart
diskpart のプロンプト内で 1 行ずつ:
select vdisk file="C:\Users\AppData\Local\rancher-desktop\distro-data\ext4.vhdx"
compact vdisk
exit
compact vdisk は数十分かかることがある。 89GB のファイルだとなおさら。
最後にツールを再起動すれば完了。
compact vdisk が 98% で止まる/進まないとき
経験上、よくある原因:
1. WSL が完全に停止していない — wsl -l -v で全部 Stopped か再確認
2. ツールのプロセスが残っている — タスクマネージャーでツール本体・付随プロセス (rdctl 等) を確認
3. 単に時間がかかっている — 80GB を超えると 98% 表示から完了まで数十分かかることがある。I/O が遅いと顕著
4. 空き容量不足 — compact は一時作業領域を必要とする。残り数 GB しかないと詰まる
5. アンチウイルスが vhdx をスキャンしている — Windows Defender 等が .vhdx をロックしているケース
完全にフリーズしたように見えても、Ctrl+C で中断すれば圧縮前の状態に戻るだけでデータが壊れることはない。とはいえ、まずは もう少し待つ のが第一選択。
教訓
ローカル開発の構成について
- 「本番と同じ構成」を強制すると、bind mount まわりで必ず歪みが出る。本番相当の検証用 compose と、日常開発用の軽量 compose を分けて共存させる のがベター
- フロントの dev server (Vite/Webpack 等) が持っている proxy 機能を使えば、ローカルでは nginx は要らない。本番では nginx を残す、というのは矛盾しない
- マイグレーションツールは .env を読まないことが多い。シェル側で環境変数を export してから子プロセスを起動する のが本筋。スクリプトでカバーすること
Docker のディスク管理について
- docker system df は 月 1 回くらい打つ習慣を持ったほうがいい。気付くと 100GB 超えている
- docker system prune だけでは Windows の空き容量は戻らない。WSL2 を使っている限り、compact vdisk までがワンセット
- イメージ数が 400 を超えてくると docker image prune -a は数十分かかる。寝る前に流すのが賢い
- ビルドキャッシュ (docker builder prune -a) は最もリスクが低くて効果が大きい。困ったらまずこれ
スクリプト化について
- 1 つのコマンドで再現できるようにしておくと、次回のオンボーディングが楽になる
- 「DB 起動 → 健康確認 → backend → frontend」のような順序依存のセットアップは、必ずスクリプトに落とす
- 小さい sleep を挟む程度の妥協は実用上問題ない。完璧を狙って雪だるま式に複雑化させるより、5 行で動くスクリプトを優先する
まとめ
ローカル開発のフリクションは「気付かないうちに支払っているコスト」の塊だ。今回は、
1. bind mount による C ドライブ汚染 → DB のみ Docker 化で根治
2. フルビルド+serve の遅さ → dev server に切り替えて HMR を取り戻す
3. Docker のキャッシュ肥大化 → docker system df で可視化、prune で削減
4. WSL2 vhdx の自動縮小しない問題 → compact vdisk で物理サイズを返す
の 4 点を一気に解消できた。回収した容量はざっと 100GB。半日の作業で得られる効果としては悪くない。
似たような構成で開発している人は、まずは docker system df を打つところから始めてみてほしい。







![Microsoft Power BI [実践] 入門 ―― BI初心者でもすぐできる! リアルタイム分析・可視化の手引きとリファレンス](/assets/img/banner-power-bi.c9bd875.png)
![Microsoft Power Apps ローコード開発[実践]入門――ノンプログラマーにやさしいアプリ開発の手引きとリファレンス](/assets/img/banner-powerplatform-2.213ebee.png)
![Microsoft PowerPlatformローコード開発[活用]入門 ――現場で使える業務アプリのレシピ集](/assets/img/banner-powerplatform-1.a01c0c2.png)
