E-NO Logo
EN FR
Docker volumes 9 Min Read

Docker volumes and bind mounts for stateful services: practical implementation guide

calendar_today Published: 2026-07-08
update Last Updated: 2026-07-08
analytics SEO Efficiency: 97%
Technical guide illustration for Docker volumes and bind mounts for stateful services: practical implementation guide.

Intro

Stateful services such as PostgreSQL, MySQL, Redis, and file stores need durable storage that outlives containers. Docker gives you two main choices:

  • Volumes: Managed by Docker. Portable, safe defaults, and good cross-platform behavior.
  • Bind mounts: Direct host paths. Great for local development and rapid iteration; riskier in production without strict controls.

This guide shows how to pick between them, declare storage in Docker Compose, set correct file permissions, run reliable backups and restores, test locally, and avoid common production pitfalls. It is written for operators who need clear steps and copy-paste examples.

Key concepts in 3 minutes

  • Named volume: docker-managed storage identified by a name. Lives under Docker's data directory.
  • Bind mount: maps a specific host path into the container. You fully control the path and contents.
  • tmpfs: in-memory mount that vanishes on restart; useful for secrets or ephemeral caches.
  • Persistence boundary: the set of directories whose data must be kept safe across upgrades and redeploys (for example, /var/lib/postgresql/data).

When to use each:

  • Prefer volumes for databases and services that must run consistently across OSes and machines.
  • Use bind mounts in local dev when you want to edit files on the host and see instant changes inside a container.

Workflow Overview

  1. Identify data directories
  • Read the image docs or Dockerfile to find the data path (for example, /var/lib/postgresql/data).
  1. Choose storage type
  • Production default: named volumes.
  • Local dev: bind mounts for source code or static assets; volumes for database data.
  1. Declare in Compose
  • Use top-level volumes: to define named volumes.
  • Use service-level volumes: to mount them at the correct paths.
  1. Set the container user and permissions
  • Match user IDs between host and container or chown data directories at build or runtime.
  1. Bring up services and write a test record
  • Verify that data survives container restarts and image upgrades.
  1. Back up and restore
  • Practice both before you need them. Keep scripts in version control.
  1. Monitor and maintain
  • Track free space, prune unused volumes safely, and rotate backups.

Compose examples

Example: PostgreSQL with a named volume

docker-compose.yml:

overversion: "3.9"
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    volumes:
      - db_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  db_data: {}

Run:

docker compose up -d

Confirm persistence:

docker exec -it $(docker compose ps -q db) psql -U app -d appdb -c "CREATE TABLE t(x int); INSERT INTO t VALUES (1); SELECT * FROM t;"
docker compose restart db
# Re-run the SELECT to confirm the row is still present

Example: Nginx serving local static files via bind mount (dev only)

overversion: "3.9"
services:
  web:
    image: nginx:1.27-alpine
    volumes:
      - ./public:/usr/share/nginx/html: ro
    ports:
      - "8080:80"

This gives instant feedback while editing files on the host. Use volumes or an image artifact in production to avoid path drift and permission surprises.

Dockerfile example for safe permissions

Run containers as a non-root user and ensure data directories are owned by that user.

FROM postgres:16
# Example pattern: create a non-root user with a stable UID/GID
# (Many official DB images already run as a dedicated user.)
# The below is a generic template if you need a custom image.
RUN groupadd -g 1001 app && useradd -u 1001 -g 1001 -m app
# Prepare an application data directory and set ownership
RUN mkdir -p /data && chown -R app: app /data
USER app
# The database image may still manage /var/lib/postgresql/data; adjust as needed

Notes:

  • Keep UID/GID stable across environments to avoid Permission denied errors on bind mounts.
  • If you must run as root for initialization, drop to a non-root user with USER for normal operation.

File permissions and ownership

Common issues and fixes:

  • Symptom: Permission denied on a bind mount.
  • Check the container process UID/GID: docker exec -it <cid> id.
  • On the host, set ownership to match: sudo chown -R 1001:1001 ./data.
  • Consider POSIX ACLs when multiple users need access: setfacl -m u:1001: rwx ./data.
  • SELinux (RHEL, Fedora, CentOS): add :Z (private) or :z (shared) to bind mounts, e.g., ./data:/var/lib/app: Z.
  • Windows and macOS file sharing: ensure the path is allowed in Docker Desktop settings.

Backups and restores

Backups should be scripted and tested. Use database-aware tools where possible.

Back up a named volume with tar

Create a backup archive of a Docker volume:

# Back up volume db_data into backups/db_data_$(date +%F).tgz
mkdir -p backups
V=backups/db_data_$(date +%F).tgz
docker run --rm \
  -v db_data:/data: ro \
  -v "$(pwd)":/backup \
  alpine:3.19 sh -c "tar -czf /backup/$(basename "$V") -C /data ."
ls -lh "$V"

Restore to a fresh volume:

# Stop the service and create a clean volume
docker compose down
docker volume rm db_data || true
docker volume create db_data
# Restore from archive
docker run --rm \
  -v db_data:/data \
  -v "$(pwd)":/backup \
  alpine:3.19 sh -c "tar -xzf /backup/db_data_YYYY-MM-DD.tgz -C /data"
# Bring services back
docker compose up -d

Back up a bind mount with tar

# Assuming ./data is bind-mounted into the container
mkdir -p backups
 tar -czf backups/data_$(date +%F).tgz -C ./data .

Logical database backups (consistent snapshots)

PostgreSQL example:

# Dump schema and data
CID=$(docker compose ps -q db)
docker exec -T "$CID" pg_dump -U app -d appdb -F c -f /tmp/appdb.dump
# Copy to host
docker cp "$CID":/tmp/appdb.dump ./backups/appdb_$(date +%F).dump

Restore:

CID=$(docker compose ps -q db)
# Create a fresh, empty database if needed
docker exec -T "$CID" dropdb -U app --if-exists appdb
docker exec -T "$CID" createdb -U app appdb
# Restore from dump
cat ./backups/appdb_YYYY-MM-DD.dump | docker exec -i "$CID" pg_restore -U app -d appdb --clean

MySQL example:

CID=$(docker compose ps -q db)
docker exec -T "$CID" mysqldump -uapp -psecret appdb > ./backups/mysql_appdb_$(date +%F).sql
# Restore
cat ./backups/mysql_appdb_YYYY-MM-DD.sql | docker exec -i "$CID" mysql -uapp -psecret appdb

Tips:

  • Prefer logical dumps for databases to ensure consistency without stopping the service.
  • For file stores, use rsync with checksums and exclude transient paths.

Local Pilot Plan

Goal: prove end-to-end persistence, backup, and restore with minimal risk.

Scope:

  • One database container (PostgreSQL), one named volume, one table with a test row, one backup archive, one restore into a clean volume.

Steps:

  1. Start services
docker compose up -d
  1. Write test data
CID=$(docker compose ps -q db)
docker exec -it "$CID" psql -U app -d appdb -c "CREATE TABLE pilot(k text); INSERT INTO pilot VALUES ('ok');"
  1. Verify persistence across restart
docker compose restart db
docker exec -it "$CID" psql -U app -d appdb -c "SELECT * FROM pilot;"
  1. Create a volume backup
mkdir -p backups
V=backups/db_data_$(date +%F).tgz
docker run --rm -v db_data:/data: ro -v "$(pwd)":/backup alpine:3.19 \
  sh -c "tar -czf /backup/$(basename \"$V\") -C /data ."
  1. Simulate disaster and restore
docker compose down
docker volume rm db_data || true
docker volume create db_data
docker run --rm -v db_data:/data -v "$(pwd)":/backup alpine:3.19 \
  sh -c "tar -xzf /backup/$(basename \"$V\") -C /data"
docker compose up -d
CID=$(docker compose ps -q db)
docker exec -it "$CID" psql -U app -d appdb -c "SELECT * FROM pilot;"
  1. Document results and keep the commands as scripts in your repo.

Success criteria:

  • Data present after restart and after restore.
  • No permission errors in container logs.

Production guidance and pitfalls

  • Do not bind mount arbitrary host paths in production. Prefer named volumes for portability and predictable permissions.
  • Pin container users. Use a stable UID/GID and avoid running the main process as root.
  • Use read-only mounts where possible. For example, mount config files as :ro and only grant :rw to data dirs.
  • Cross-OS considerations: bind mounts behave differently across Linux, macOS, and Windows. Volumes avoid most surprises.
  • SELinux: remember :Z or :z for bind mounts on SELinux hosts.
  • Performance on macOS/Windows: bind mounts can be slower. Consider volumes or dedicated sync tools.
  • Backups: practice both volume-level tar backups and logical database dumps. Store at least one off-host copy.
  • Capacity: monitor disk usage. Prune unused volumes with care: docker volume ls, docker volume rm, and docker volume prune (review before pruning).

Troubleshooting quick reference

  • Permission denied on startup:
  • Check container UID: docker exec -it <cid> id.
  • Fix host ownership: sudo chown -R <uid>:<gid> <host-path>.
  • Verify mount options, SELinux flags, and read-only vs read-write.
  • Data did not persist:
  • Confirm the correct mount path matches the app's data directory.
  • docker inspect <cid> and review the Mounts section.
  • In Compose, confirm the service uses the intended named volume, not an anonymous one.
  • Volume name confusion:
  • List volumes: docker volume ls.
  • Inspect: docker volume inspect <name> to see the mountpoint and consumers.
  • Windows path issues:
  • Use absolute paths for bind mounts and ensure the drive is shared in Docker Desktop.

Handy docker commands

  • Create and inspect volumes:
docker volume create mydata
docker volume inspect mydata
  • Run with a named volume:
docker run -d --name app -v mydata:/var/lib/app myimage: tag
  • Run with a bind mount (dev):
docker run -d --name web -v "$(pwd)/public:/usr/share/nginx/html: ro" -p 8080:80 nginx: alpine
  • Copy a one-off file out of a container (not a backup strategy):
docker cp <cid>:/path/in/container ./local/path

Conclusion

For stateful services, default to named volumes in production, use bind mounts selectively in development, and always run with explicit users and correct permissions. Declare volumes clearly in Compose, verify persistence locally, and script backups and restores before rollout. Start with a narrow pilot you can inspect end to end, then expand to additional services once the workflow is proven.

Article Quality Score

Reader usefulness 97%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL