About

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:

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

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

1

The image's Dockerfile declares the PGDATA path as a VOLUME

2

If compose mounts nothing there, Docker attaches an anonymous volume

3

Recreating with up carries the old anonymous volume over, so data stays

4

down then up attaches a new anonymous volume: empty DB

← What Is Portless? How It Replaces localhost Port Numbers with Names Turning a Local Dev Page into a Mac App — One Manifest and launchd, No Electron →