Agent Skills: Correct (bind mount)

>

UncategorizedID: yorch/claude-skills/postgres-18-docker-volume

Install this agent skill to your local

pnpm dlx add-skill https://github.com/yorch/claude-skills/tree/HEAD/skills/postgres-18-docker-volume

Skill Files

Browse the full folder contents for postgres-18-docker-volume.

Download Skill

Loading file tree…

skills/postgres-18-docker-volume/SKILL.md

Skill Metadata

Name
postgres-18-docker-volume
Description
>

What changed in PostgreSQL 18

PostgreSQL 17 and earlier Docker images declared their persistent storage at:

VOLUME /var/lib/postgresql/data

PostgreSQL 18 changed this. The official postgres:18 (and postgres:18-alpine) images now declare:

VOLUME /var/lib/postgresql

And PGDATA was moved from /var/lib/postgresql/data to /var/lib/postgresql/18/docker.

Why this causes silent data loss

Docker's VOLUME instruction means: "this path needs persistent storage." When you add a bind mount (e.g., ./data/postgres:/var/lib/postgresql/data) for a postgres:18 container, Docker sees it as mounting a different path than the one declared in VOLUME. So Docker silently creates an anonymous volume at /var/lib/postgresql — the real data goes there, not into your bind mount. Everything appears to work: migrations run, seeds succeed, the app connects. But the data is in a randomly-named ephemeral volume, not where you think.

The failure mode is usually discovered at the worst moment: after docker compose down -v, a docker volume prune, or a fresh docker compose up that starts with an empty database.

Correct volume paths by version

This applies to both bind mounts and named volumes — the target path inside the container determines where Docker routes the data.

| Postgres version | Correct volume target (bind mount or named volume) | | ---------------- | -------------------------------------------------- | | 15, 16, 17 | /var/lib/postgresql/data | | 18+ | /var/lib/postgresql |

For postgres:18-alpine and postgres:18:

# Correct (bind mount)
volumes:
  - ./data/postgres:/var/lib/postgresql

# Correct (named volume)
volumes:
  - postgres_data:/var/lib/postgresql

# Wrong for either type (silent data loss — data goes to anonymous volume)
volumes:
  - ./data/postgres:/var/lib/postgresql/data
  # or
  - postgres_data:/var/lib/postgresql/data

For postgres:17-alpine and earlier:

# Correct (bind mount)
volumes:
  - ./data/postgres:/var/lib/postgresql/data

# Correct (named volume)
volumes:
  - postgres_data:/var/lib/postgresql/data

How to verify which path a postgres image uses

# Check the VOLUME declaration for any postgres image
docker inspect postgres:18-alpine | jq '.[0].Config.Volumes'
# → { "/var/lib/postgresql": {} }   ← PG18: mount here

docker inspect postgres:17-alpine | jq '.[0].Config.Volumes'
# → { "/var/lib/postgresql/data": {} }   ← PG17: mount here

If Docker is not available, check the Dockerfile on Docker Hub:

  • For postgres:18+, search for VOLUME /var/lib/postgresql (no /data suffix)

Reviewing a docker-compose file

When you see a postgres service, verify:

  1. What image tag is used? If it's postgres:18, postgres:18-alpine, or newer, the target must be /var/lib/postgresql.
  2. Is there a bind mount or named volume? Check the target path (the right side of the :). It must match the version table above. Both bind mounts and named volumes are affected — the target path is what matters, not the volume type.

Named volumes do NOT automatically use the correct path. Docker mounts them wherever you specify. If you write postgres_data:/var/lib/postgresql/data against postgres:18, Docker creates an anonymous volume at /var/lib/postgresql (the VOLUME-declared path) and mounts postgres_data at the wrong subdirectory. PG18 writes to the anonymous volume, not to postgres_data.

Flag any mismatch. Silently wrong mounts are worse than errors — they give no warning.

Migrating existing data from PG17 → PG18

If a user is upgrading from postgres:17 to postgres:18 and already has data:

  1. Do not just change the mount path. PostgreSQL 18 uses a new on-disk format — you must pg_dump / pg_restore across major versions, not just remount.
  2. Dump from the running PG17 container:
    docker exec -t <pg17-container> pg_dumpall -U <user> > dump.sql
    
  3. Stop PG17, switch image to postgres:18, update the volume mount path to /var/lib/postgresql, bring it up fresh, then restore:
    docker exec -i <pg18-container> psql -U <user> < dump.sql
    
  4. Verify data integrity before removing the old volume.

Quick-fix checklist

When reviewing or writing a docker-compose file with postgres:18+:

  • [ ] Volume target is /var/lib/postgresql (not /var/lib/postgresql/data)
  • [ ] POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB are set (required for healthcheck)
  • [ ] Healthcheck uses pg_isready -U <user> so dependent services wait for readiness
  • [ ] Data directory in the host path is separate from previous major-version data (postgres 18 data is not backward-compatible with postgres 17)