Our Postgres Volume Path Was Wrong for 3 Years and Nothing Broke — The Anonymous Volume Behind VOLUME
Data follows the volume name, not the path. Only people who run down lose the database
Our docker-compose file mounted the Postgres volume at db:/data/db. That's MongoDB's path. I put it there myself when I set the project up three years ago. In all that time my database never disappeared, while a colleague said his vanished every time he brought the containers back up.
Short version: a wrong path doesn't stop Postgres. Docker attaches an anonymous volume to the path the image declares with VOLUME, and that volume is dropped the moment you run down. Worth checking the DB volume line in your own compose file.
What was actually mounted
db:
image: postgres:16
volumes: ["db:/data/db"] # Postgres never writes here
The official postgres 16 Dockerfile has these two lines:
ENV PGDATA /var/lib/postgresql/data
VOLUME /var/lib/postgresql/data
VOLUME says "keep this path outside the container." If compose mounts nothing there, Docker creates an anonymous volume and attaches it. So the real picture was this:
The named volume db sat on /data/db, which nobody used, empty for three years. The real data lived in an anonymous volume with a hash for a name.
Why only I was fine: habits
| Command | Before (anonymous volume) | After (named volume) |
|---|---|---|
stop · start · restart |
kept | kept |
up -d recreating the container |
kept | kept |
down → up |
empty DB | kept |
rm then up, up -V |
empty DB | kept |
down -v, volume prune |
empty DB | empty DB |
When up recreates a container, compose carries the previous container's anonymous volumes over. The docs describe -V, --renew-anon-volumes as "Recreate anonymous volumes instead of retrieving data from the previous containers," so carrying over is the default. I only used up and stop. My colleague cleaned up with down. One bug, two different symptoms.
Where I got confused
I assumed "same path, so the data should still be there." Wrong. A new container unpacks a fresh filesystem from the image. Data survives only when a volume with the same name is attached to that path again. Data follows the volume name, not the path.
/var/lib/postgresql/data isn't a path on the Mac either. It's inside the container, and the files live in the Docker Desktop VM under /var/lib/docker/volumes/<project>_db/_data. It never mixes with a Homebrew Postgres.
My Mac had 32 volumes attached to nothing (docker volume ls -qf dangling=true). I haven't counted yet how many are databases abandoned by past downs.
Copy another database's line and it happens again
Official images declare different VOLUMEs (each Dockerfile and the Docker Hub page, checked 2026-10-19):
| Image | VOLUME |
|---|---|
| postgres 17 and below | /var/lib/postgresql/data |
| postgres 18 and above | /var/lib/postgresql (PGDATA is /var/lib/postgresql/18/docker) |
| mysql | /var/lib/mysql |
| mongo | /data/db, /data/configdb |
| redis | /data |
Borrow just the volume line from another project's compose file and you get exactly this. Look at the same line again when moving to Postgres 18: the Docker Hub page says to mount /var/lib/postgresql from 18 on. Keep the old :/var/lib/postgresql/data line and PGDATA ends up outside it.
How to check
docker image inspect postgres:16 --format '{{json .Config.Volumes}}'
docker inspect $(docker compose ps -q db) --format '{{json .Mounts}}'
If the volume on the PGDATA path in the second output has a 64-character hash as its Name, you're in this trap.
Three kinds of volume, and when to use each
| Kind | Use it when |
|---|---|
named volume (db:/var/lib/postgresql/data) |
the default for a local dev DB. Survives down |
bind mount (./tmp/pg:/var/lib/postgresql/data) |
you want to see the data as a folder or carry it around |
| anonymous volume | throwaway caches. Not for databases |
When a local dev setup breaks differently for different people, it's usually one hidden default like this. If you want a second pair of eyes, reach us at https://www.negabaro.com/contact.
How It Works
The image's Dockerfile declares the PGDATA path as a VOLUME
If compose mounts nothing there, Docker attaches an anonymous volume
Recreating with up carries the old anonymous volume over, so data stays
down then up attaches a new anonymous volume: empty DB