About

Postgresのボリュームパスが3年間間違っていたのに動いていた理由 — VOLUME宣言がこっそり付けた名前のないボリューム

データはパスではなくボリューム名についていく。downを使う人だけがDBを失う

docker-composeのPostgresのボリュームが db:/data/db になっていた。MongoDBのパスだ。3年前、最初にセットアップしたときに自分が入れたミスだった。それなのに自分はその間、DBが消えたことが一度もなかった。同僚は「コンテナを立て直すたびにDBが消える」と言っていた。

先に結論。パスが間違っていてもPostgresは普通に動く。イメージが VOLUME で宣言したパスに、Dockerが名前のないボリュームを付けるからだ。そのボリュームは down した瞬間に捨てられる。 手元のcomposeファイルのDBボリュームの行を一度見ておくといい。

何が付いていたのか

db:
  image: postgres:16
  volumes: ["db:/data/db"]   # Postgresはここを使わない

公式postgres 16イメージのDockerfileにはこの二行がある。

ENV PGDATA /var/lib/postgresql/data
VOLUME /var/lib/postgresql/data

VOLUME は「このパスはコンテナの外に置く」という宣言だ。composeがそのパスに何も付けなければ、Dockerが名前のない(anonymous)ボリュームを作って付ける。実際の姿はこうだった。

Before - db:/data/db container /data/db /var/lib/postgresql/data PGDATA · Postgres writes here named volume "db" — empty for 3 years anonymous volume a3f9c1… real data · made by VOLUME in the image down → up : a new empty one is made After - db:/var/lib/postgresql/data /var/lib/postgresql/data PGDATA named volume "db" — survives down

名前付きボリューム db は誰も使わない /data/db に付いていて、3年間空っぽだった。本当のデータはハッシュ名の名前のないボリュームにあった。

なぜ自分だけ無事だったのか — 習慣が分けた

コマンド 直す前(名前のないボリューム) 直した後(名前付きボリューム)
stop・start・restart 残る 残る
up -d でコンテナ再作成 残る 残る
down → up 空のDB 残る
rm の後 up、up -V 空のDB 残る
down -v、volume prune 空のDB 空のDB

up でコンテナを作り直すとき、composeは前のコンテナの名前のないボリュームを引き継ぐ。公式ドキュメントの -V, --renew-anon-volumes の説明が「前のコンテナからデータを持ってくる代わりに、名前のないボリュームを作り直す」なので、既定は引き継ぎだ。自分は up と stop しか使わず、同僚は down で片付ける習慣があった。バグはひとつなのに、症状は人によって違った。

最初に混乱したところ

「パスが同じならデータも残るはずでは」と思っていた。違った。コンテナを作り直すと、ファイルシステムもイメージから新しく展開される。データが残るのは、そのパスに同じ名前のボリュームがもう一度付くときだけだ。データはパスではなくボリューム名についていく。

/var/lib/postgresql/data はMacのパスでもない。コンテナの中のパスで、実際のファイルはDocker DesktopのVM内 /var/lib/docker/volumes/<プロジェクト名>_db/_data にある。Homebrewで入れたPostgresとも混ざらない。

自分のMacには、どこにも付いていないボリューム(docker volume ls -qf dangling=true)が32個あった。そのうちいくつが過去の down で捨てられたDBなのかは、まだ数えていない。

別のDBの行をコピーすると同じことが起きる

公式イメージの VOLUME はばらばらだ(各Dockerfile・Docker Hubの説明、2026-10-19確認)。

イメージ VOLUME
postgres 17以下 /var/lib/postgresql/data
postgres 18以上 /var/lib/postgresql(PGDATAは /var/lib/postgresql/18/docker)
mysql /var/lib/mysql
mongo /data/db、/data/configdb
redis /data

別プロジェクトのcomposeからボリュームの行だけ持ってくると、今回と同じことになる。Postgres 18に上げるときも同じ行を見直す。Docker Hubの説明は、18からは /var/lib/postgresql にマウントするよう書いている。17時代の :/var/lib/postgresql/data をそのまま残すと、PGDATAがその外に置かれる。

確かめ方

docker image inspect postgres:16 --format '{{json .Config.Volumes}}'
docker inspect $(docker compose ps -q db) --format '{{json .Mounts}}'

二つ目の結果で、PGDATAのパスに付いたボリュームの Name が64文字のハッシュなら、この罠にはまっている。

ボリューム三種類、いつどれを

種類 こんなとき
名前付きボリューム(db:/var/lib/postgresql/data) ローカル開発DBの基本。down しても残る
bind mount(./tmp/pg:/var/lib/postgresql/data) データをフォルダとして見たい、持ち運びたいとき
名前のないボリューム 捨ててもいいキャッシュ。DBには使わない

ローカル開発環境が人によって違う壊れ方をするなら、たいていこういう隠れた既定値がひとつ原因になっている。一緒に見てほしければ https://www.negabaro.com/contact から連絡してほしい。

動作フロー

1

イメージのDockerfileのVOLUMEがPGDATAのパスを「外に置く場所」として宣言

2

composeがそこに何も付けなければ、Dockerが名前のないボリュームを付ける

3

upで作り直すと前の名前のないボリュームを引き継ぐのでデータが残る

4

down → upだと新しい名前のないボリュームが付き、空のDBになる

← Portlessとは?localhostのポート番号をmyapp.localhostの名前に置き換える仕組み ローカル開発画面をMacアプリに — Electronではなくmanifest一枚とlaunchd →