Compose supports three flavors of persistent storage, and choosing the right one matters. A bind mount maps a host path into the container, written as ./local:/app, with optional read-only mode :ro or a consistency mode like consistency: cached for macOS performance. Bind mounts are ideal for source code in development because edits on the host appear immediately in the container, but they make it easy to clobber image-baked files. A named volume is managed by Docker and survives container recreation, declared at the bottom of the compose file under a top-level volumes: key and referenced in the service as pg_data:/var/lib/postgresql/data. An anonymous volume is created implicitly when you write a path with no name, like /var/lib/postgresql/data by itself; it survives container restarts but is removed by docker compose down -v.
The most important data-persistence pattern for stateful services like databases is the named volume. For Postgres, the data directory lives at /var/lib/postgresql/data, and on first start the entrypoint initializes an empty database there. If you bind-mount that path or omit a volume entirely, every container recreation wipes your data. The canonical recipe declares a named volume at the bottom of the file and mounts it on the database service. To list all named volumes in a project, use docker volume ls; Compose prefixes them with the project name, so they appear as <project>_<volume>. To find the host path where a named volume actually lives, run docker volume inspect <name> and look at the Mountpoint field.
There are two more storage options worth knowing. A tmpfs: mount is in-memory scratch space that's lost on container restart, useful for caches and intermediate files you don't want hitting disk. Anonymous volumes also solve a famous bind-mount footgun: when you mount your source over /app, you wipe out any pre-installed dependencies like node_modules that were baked into the image. The standard fix is to declare an anonymous volume on the dependency directory after the bind mount, so the host mount provides source code but the image's installed files are preserved. For init scripts that should run only on the first creation of an empty database, mount SQL or shell files into /docker-entrypoint-initdb.d/; the Postgres and MySQL images execute these files alphabetically, so prefix them with 01_, 02_ to control order. Finally, docker compose down -v removes everything including volumes, so treat that flag as destructive and never use it casually on a database you care about.