Docker Compose volumes: bind mount or named volume?
Learn when to use bind mounts or named volumes for config and app data, avoid permission traps, and inspect, back up, or migrate volumes without losing files.
Bind mount or named volume: di short answer
Docker Compose volumes get two kinds, and di choice dey depend on who own di files. Use bind mount for files wey you dey write and read by yourself, like config, templates, and static sites. Use named volume for data wey di application own, like database files, search indexes, and uploaded media. Bind mount dey point to path for di host wey you fit open with editor. Named volume na storage wey Docker create and track for you, and you access am through Docker.
Both dey show under di same volumes: key inside service, na why dem dey mix up. Di difference na di left side of di colon. If di left side start with . or /, na host path, so na bind mount. Anything else na name, so na named volume, and you must also declare dat name for di top-level volumes: block.
The two syntaxes in a compose file
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: nginx:1.27
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site:/usr/share/nginx/html:ro
volumes:
pgdata:pgdata:/var/lib/postgresql/data na named volume. ./nginx.conf:/etc/nginx/nginx.conf na bind mount, and :ro dey mount am read only, wey be the correct default for config wey container no suppose ever rewrite. If you forget the top-level volumes: entry, Compose go stop with service "db" refers to undefined volume pgdata.
Bring am up and list wetin Docker create:
docker compose up -d
docker volume lsThe volume no be called pgdata. E dey called <project>_pgdata, where project name dey default to the name of the directory wey hold the compose file. Directory wey dem call myapp go give myapp_pgdata. This one matter because if you rename the directory, you go get new empty volume, and application go look like say e lose im data. E no lose am: the old volume still dey listed by docker volume ls. Pin the name with name: for the compose file, or set COMPOSE_PROJECT_NAME, if the directory fit move. Settings like that belong with your other Compose environment files and secrets.
Why permission errors only dey affect bind mounts
Na this be the biggest practical difference, and one rule cause am: named volume wey empty for first use dey get content from the image, but bind mount no dey do am.
When Docker mounts empty named volume over directory wey already get content for the image, e copy that content enter the volume, including the ownership and modes wey the image set. Official Postgres image releases /var/lib/postgresql/data wey its own postgres user own, so the volume ownership go use that same numeric id, and the database go start.
Bind mount dey do the opposite. Anything wey dey for the host na wetin the container go see, including ownership, and the image content for that path go hide. If the host directory no exist, Docker daemon go create am, and daemon dey run as root, so you go get directory wey root:root own. Container process wey dey run as non-root user no fit write to am:
PermissionError: [Errno 13] Permission denied: '/data/app.db'The fix na to make the numbers agree. Ownership across bind mount dey compare with numeric user id, no be name, because container get its own /etc/passwd. User wey dem call app inside the container no mean anything for the host. Uid 1000 mean uid 1000 for both sides.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id dey print the uid wey the container process really dey run as. Make the host directory use that number, or pin the container to your number with user: "1000:1000" inside the service. To pin user: dey cleaner for application wey you write yourself. To chown the host directory dey safer for image wey you no write, because some images dey start entrypoint as root, drop privileges, and expect specific ownership underneath.
Two more traps dey worth knowing. For Fedora, RHEL and other systems wey SELinux (security-enhanced Linux) dey enforce, bind mount go get denied until you relabel am. So add :z for path wey containers share, or :Z for path wey only one container suppose use, written as - ./data:/data:Z. Also, bind mount of one file, instead of directory, go break when editor replace the file instead of writing inside am, because mount dey follow the original inode. Container go continue to see the old content until you restart am. Mount the parent directory when dem dey edit the file often.
Performance: where di difference dey real
For Linux server, both types dey pass through the same kernel path, so the throughput difference small enough that you no suppose choose based on am. Named volumes wey dey use the default local driver dey live for the same filesystem wey the rest of Docker dey use, under /var/lib/docker/volumes/. Bind mount dey live anywhere wey you point am to.
Di difference dey show for Docker Desktop for macOS and Windows, where containers dey run inside virtual machine. Bind mount for there dey cross from host filesystem enter that virtual machine through file sharing layer. Workloads wey dey do many small file operations, like Node.js dependency tree or PHP framework cache, go slow down noticeably. Named volumes dey stay inside the virtual machine and no dey pay that cost. Na why plenty development compose files dey bind-mount source directory but declare named volume over node_modules.
Di other real difference na where di bytes dey land. Bind mount to /mnt/backup puts data for that disk. Named volume lands for any filesystem wey dey hold /var/lib/docker, and for VPS, na usually root disk. Database wey dey grow inside named volume fit fill the same disk wey your system logs dey use. Check am before e become incident:
docker system df -v
df -h /var/lib/dockerdocker system df -v dey list every volume with e size, and dey mark the ones wey no container dey refer to again.
Volume wey get name na how to inspect am
Named volume no be black box. Ask Docker where e dey:
docker volume inspect myapp_pgdataThe Mountpoint field dey give real host path, normally /var/lib/docker/volumes/myapp_pgdata/_data. You fit read am with sudo ls, and e useful for quick check. No treat am like place to edit files. If you write there as root, e go recreate the ownership problem wey we describe above. The path na detail of the local driver, and other volume drivers no share am.
The safe way to look inside na to use throwaway container wey mount the volume:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /volThis one work for any driver. E go see the same permissions wey the real container dey see. E no go leave anything behind because of --rm.
Back up each kind
A bind mount na ordinary directory, so any file-level backup tool go handle am already. Point the backup to the host path, and you don finish. Named volume need one extra step, because the tool must enter inside am. Mount the volume and one host directory inside the same short-lived container, then write an archive:
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .Restore am by reversing the process into a fresh volume:
docker volume create myapp_pgdata_restored
docker run --rm \
-v myapp_pgdata_restored:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /datatar dey preserve numeric ownership when e run as root inside the container. Na this one dey keep the restored volume usable by the application.
One warning apply to both kinds. If you copy database files while the database dey run, you go get archive of data wey still dey change. E fit restore into corrupt state. Stop the service first, or dump through the database own tool, as shown for docker compose exec -T db pg_dump -U postgres appdb > appdb.sql. This one dey produce plain file wey you fit include for normal encrypted restic backup routine together with your compose files.
Migrating bind mount go named volume
Di move na copy, e no be rename, and e dey take about one minute.
docker compose down
docker volume create myapp_pgdata
docker run --rm \
-v "$PWD/data":/from \
-v myapp_pgdata:/to \
alpine sh -c 'cp -a /from/. /to/'cp -a dey keep ownership, modes and timestamps, so di container user wey fit read di old directory fit still read di new volume. Then change di service make e use pgdata:/var/lib/postgresql/data, add pgdata to di top-level volumes: block, run docker compose up -d, and read di application logs before you delete di old directory. To go di other way, use di same command but swap /from and /to.
Remember one thing as you dey test. docker compose down dey leave named volumes as dem be, but docker compose down -v dey delete every named volume wey di project declare, and you no fit undo am. A bind mount dey survive both, because Docker never own dat directory. If di lifecycle commands still dey new to you, di Docker Compose basics guide for a VPS dey explain dem step by step.
How to choose am service by service
Ask who dey write the file. Config wey you edit for text editor and commit to git belong inside bind mount, mounted :ro, because you want make e dey visible and versioned. Application state wey you never open by hand belong inside named volume, because Docker dey set the permissions correctly and the data no depend on host path.
The mixed case na media. Application dey write photo library, but you still dey manage am, and e often big reach make you want use specific disk. Bind mount am to path for that disk, then set the ownership deliberately one time. Na the pattern wey most self-hosted stacks dey use: named volumes for databases and caches, bind mounts for config and for the large directory wey matter to you.
FAQ
Wetin be di difference between bind mount and named volume?
Bind mount maps path for di host enter di container, so both sides dey see di same directory and you fit edit am with normal tools. Named volume na storage wey Docker dey create and manage, wey dem dey refer to by name and declare for top-level volumes: block. Di main difference na ownership: use bind mounts for config wey you dey maintain, and named volumes for data wey di application dey maintain.
Why I dey get "permission denied" with bind mount but not named volume?
Empty named volume dey get copy of di image data, so e inherit di ownership wey di image set, and di container user fit write to am. Bind mount dey show di host directory exactly as e be. If Docker need create that directory, e make root own am. Run docker compose exec <service> id to see di numeric id wey di container dey use, then sudo chown -R <uid>:<gid> di host directory, or set user: "1000:1000" for di service.
Where Docker dey store named volumes for disk?
With default local driver, dem dey live under /var/lib/docker/volumes/<volume>/_data, and docker volume inspect <volume> dey print di exact Mountpoint. Read am if you need check something, but only write to am through container, because if you edit am as root for host, e go change ownership in ways wey di container no expect.
How I fit back up named volume?
Run short-lived container wey mount both di volume and host directory, then archive from one to di other with docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data .. For database, use di database own tool to dump am instead of copying live files, because file copy wey happen while writes still dey run fit restore into corrupt state.
Docker compose down dey delete my volumes?
docker compose down dey remove containers and networks, and e leave named volumes for ground. docker compose down -v still dey delete every named volume wey di project declare, permanently. Neither command dey remove bind mounts, because that directory belong to di host, no be Docker.