本記事はFIXERが提供する「cloud.config Tech Blog」に掲載された「ローカル開発環境を見直したら、Docker が C ドライブを 130GB 圧迫していた話」を再編集したものです。
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. WSL2vhdxの自動縮小しない問題 → compact vdiskで物理サイズを返す
の4点を一気に解消できた。回収した容量はざっと100GB。半日の作業で得られる効果としては悪くない。
似たような構成で開発している人は、まずはdocker system dfを打つところから始めてみてほしい。
藤野元規/FIXER
(ふじの もとき)
2022年度からFIXERに入社しました。
本記事はアフィリエイトプログラムによる収益を得ている場合があります


この連載の記事
-
TECH
機械科卒・ITエンジニア就職から一年、やって良かったこと -
TECH
Chrome DevTools MCPとは? Claude Codeとの連携でWebアプリ開発体験が劇的に変わった -
TECH
MobSF(Mobile Security Framework)でできること、動かない理由 -
TECH
3週間の自動テストが半日に! Playwrightの使い方の基本 -
TECH
Next.jsで静的テスト環境を構築し、GitHub Actionsで自動化してみた -
TECH
クイズ:正規表現で「0~255」のすべてにマッチするのはどれ? -
TECH
ミニPCサーバーにFluxを導入、GitOpsで自動デプロイする方法 -
TECH
アプリ開発の手戻りを防ぐ 「入力チェック(バリデーション)」の設計方法 -
TECH
Webアプリを使いやすく! 「入力チェック(バリデーション)」の正しい考え方 -
TECH
Androidの16KBページサイズ対応 Flutter開発者向けチェックリスト -
TECH
「SOSの出し方を知ろう」 新卒入社から1年、学んだことを振り返る - この連載の一覧へ


