Docker Compose volumes: bind mount or named volume?
Learn when to use bind mounts or named volumes for config and data, avoid file permission traps, and inspect, back up, or migrate Docker Compose storage.
Bind mount or named volume: di short answer
Docker Compose volumes get two kinds, and di choice na who own the 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 the application own, like database files, search indexes, and uploaded media. Bind mount point to path for the 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 the same volumes: key inside service, na why people dey mix dem up. Di difference dey for the left side of the colon. Left side wey start with . or / na host path, so na bind mount. Anything else na name, so na named volume, and you must also declare that name inside the top-level volumes: block.
Compose file inside syntax dem
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 mount am read only. Na 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 name no be pgdata. E be <project>_pgdata, because project name by default na 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 the 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 this one belong with your other Compose environment files and secrets.
Why permission errors dey only affect bind mounts
Na this one be the biggest practical difference, and e come from one rule: named volume wey empty for first use dey seeded from the image, but bind mount no dey.
When Docker mount empty named volume over directory wey already get content for image, e copy that content enter volume, with the ownership and modes wey image set. Official Postgres image releases /var/lib/postgresql/data wey its own postgres user own, so volume go come out owned by that same numeric id, and database go start.
Bind mount dey do the opposite. Anything wey dey for host na wetin container go see, ownership join, and image content for that path go hide. If 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 match. Ownership across bind mount dey compare by numeric user id, no be by name, because container get its own /etc/passwd. User wey dem call app inside container no mean anything for 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 container process actually dey run as. Match host directory to that number, or pin container to your number with user: "1000:1000" for the service. Pinning user: dey cleaner for application wey you write yourself. Chowning 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 enforcing, bind mount go deny access 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 see the old content until you restart am. Mount the parent directory when dem dey edit the file often.
Performance: where the difference dey real
For Linux server, both types dey pass through the same kernel path, so the throughput difference small enough say you no suppose choose based on that. Named volumes wey dey use the default local driver dey live for the same filesystem as the rest of Docker, under /var/lib/docker/volumes/, while bind mount dey live anywhere wey you point am.
The difference dey show for Docker Desktop on 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 plenty 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 the source directory but declare named volume over node_modules.
The other real difference na where the bytes dey land. Bind mount to /mnt/backup puts data for that disk. Named volume lands on 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 turn to incident:
docker system df -v
df -h /var/lib/dockerdocker system df -v lists every volume with its size, and marks the ones no container refers to any more.
Yenà named volume
Named volume no be black box. Ask Docker where e dey:
docker volume inspect myapp_pgdataThe Mountpoint field dey show real host path, normally /var/lib/docker/volumes/myapp_pgdata/_data. You fit read am with sudo ls, and this one useful for quick check. No treat am as place to edit files. If you write there as root, you go recreate the ownership problem wey we describe above. The path na detail of 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 see the same permissions wey the real container see, and e leave nothing behind because of --rm.
Backing up each kind
Bind mount na ordinary directory, so any file-level backup tool already fit handle am. Point the backup to the host path and you don finish. Named volume need one extra step, because the tool need 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 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, and 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 produce plain file wey you fit include inside normal encrypted restic backup routine together with your compose files.
Move bind mount go named volume
The 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 the container user wey fit read the old directory still fit read the new volume. Then change the service make e use pgdata:/var/lib/postgresql/data, add pgdata to the top-level volumes: block, run docker compose up -d, and read the application logs before you delete the old directory. To go the other way, use the same command but swap /from and /to.
Make you remember one thing while you dey test. docker compose down no dey touch named volumes, but docker compose down -v dey delete every named volume wey the project declare, and you no fit undo am. Bind mount dey survive both, because Docker never own that directory. If the lifecycle commands still new to you, the Docker Compose basics guide for VPS go explain how dem work.
Choosing service one by one
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 visible and versioned. Application state wey you no dey open by hand belong inside named volume, because Docker go 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 enough to need specific disk. Bind mount am to path for that disk, then set the ownership deliberately one time. Na this pattern most self-hosted stacks dey use: named volumes for databases and caches, bind mounts for config and the big directory wey matter to you. Support desk like Chatwoot wey dey run for VPS fit enter exactly there, with Postgres for named volume and uploaded attachments for path wey you fit point backup to.
FAQ
Wetin be the difference between a bind mount and a named volume?
A bind mount maps one path for the host into the container, so both sides see the same directory and you fit edit am with normal tools. A named volume na storage wey Docker create and manage, and you refer to am by name for the top-level volumes: block. The main difference na ownership: use bind mounts for config wey you dey maintain, and named volumes for data wey the application dey maintain.
Why I dey get "permission denied" with bind mount but not named volume?
Docker dey seed empty named volume from the image, so e inherit the ownership wey the image set and the container user fit write to am. Bind mount dey show the host directory exactly as e be, and if Docker need create that directory, e make root own am. Run docker compose exec <service> id to see the numeric id wey the container dey use, then sudo chown -R <uid>:<gid> the host directory, or set user: "1000:1000" for the service.
Where Docker dey store named volumes for disk?
With the default local driver, dem dey under /var/lib/docker/volumes/<volume>/_data, and docker volume inspect <volume> go print the exact Mountpoint. Read am if you need check something, but write to am only through a container, because editing as root for the host fit change ownership for ways wey the container no expect.
How I fit back up a named volume?
Run one short-lived container wey mount both the volume and a host directory, then archive from one go the other with docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data .. For database, use the 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 removes containers and networks, and e leaves named volumes for their place. docker compose down -v also deletes every named volume wey the project declare, permanently. Neither command dey remove bind mounts, because that directory belong to the host, no be Docker.