Developer Debugging Guides2026-06-04·12 min read

Docker Compose Volumes: Mount, Persist & Share Data

Master docker compose volumes with working syntax, named volumes, bind mounts, and fixes for the most common data-loss and permission errors.

#docker#backend#database#devops#debugging

FlowQL Team

AI Search Optimization Experts

You spin up a Postgres container with Docker Compose, insert some test data, then run docker compose down. When you bring it back up, everything is gone. No error, no warning — the data just vanished.

This is the most common Docker trap for developers who are new to containerized workflows. The fix is volumes, and once you understand the three types and when to use each, the whole system clicks into place.

This guide covers the full docker compose volumes syntax with real working examples, the difference between named volumes and bind mounts, how to share data between services, and how to fix the permission and mount errors that catch everyone at least once.

Why Volumes Matter in Docker Compose

Containers are ephemeral by design. Every container has a writable layer on top of its image, but that layer is thrown away the moment you run docker compose down (with the --volumes flag) or docker rm. Without volumes, any file your app writes during runtime — database rows, uploads, generated files — disappears on the next teardown.

Volumes break the coupling between your container's lifecycle and your data's lifecycle. They live outside the container, managed either by Docker itself or anchored to a path on your host machine. Your container can be rebuilt, upgraded, or swapped out entirely, and the data stays put.

Docker's official volumes documentation lays out the storage driver internals if you want to go deeper. For most application development, you need to understand three things: named volumes, bind mounts, and tmpfs mounts.

The Three Types of Docker Volumes

Before writing any docker-compose.yml, know which type you need. Choosing the wrong one is the root cause of most data-loss and performance issues.

| Type | Managed by | Persists data | Best for | |---|---|---|---| | Named volume | Docker | Yes | Databases, persistent app data | | Bind mount | Host OS | Yes (host files) | Local dev, live code sync | | tmpfs mount | Memory (RAM) | No | Secrets, temp cache in prod |

Named volumes are stored under /var/lib/docker/volumes/ on Linux. Docker handles creation and cleanup. They're portable across hosts when used with docker volume export.

Bind mounts point directly at a directory on your machine. Whatever is in that directory is what the container sees — in real time. Ideal for development where you want the container to pick up file edits without rebuilding the image.

tmpfs mounts never hit disk. The data lives in RAM and is gone the moment the container stops. Use them for sensitive environment data or scratch space where persistence would be a liability.

Named Volumes: The Default Choice

How do named volumes work in Docker Compose?

Named volumes are the safest choice for any data that must survive container restarts. You declare the volume in two places: under the service's volumes: array (where you set the mount path inside the container) and under the top-level volumes: key (where you register it with Docker). If you skip the top-level declaration, Compose will throw an error.

Here is a minimal Postgres setup with a named volume:

# docker-compose.yml — named volume for Postgres data
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    volumes:
      # Syntax: <volume-name>:<container-path>
      - db-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

# Top-level volumes key — required for named volumes
volumes:
  db-data:

Run docker compose up -d, insert data, then run docker compose down. Run docker compose up -d again and the data is still there. The db-data volume lives at /var/lib/docker/volumes/yourproject_db-data/_data on the host — Compose prefixes the project name automatically.

To inspect what Docker knows about the volume:

# List all volumes on the host
docker volume ls

# Inspect a specific volume's mount point
docker volume inspect yourproject_db-data

Named volumes are also the right choice when you run Postgres inside Docker for local development — see the patterns in our Postgres Docker setup guide for a complete working stack with pgAdmin wired up alongside it.

Docker Compose volumes syntax reference

The full syntax for a volume entry under a service supports several options beyond the shorthand string form:

services:
  app:
    image: myapp:latest
    volumes:
      # Shorthand: <source>:<target>
      - app-data:/app/data

      # Long form — more explicit, supports all options
      - type: volume
        source: app-data
        target: /app/data
        read_only: false        # set true to prevent container writes
        volume:
          nocopy: true          # don't copy container data into volume on first run

volumes:
  app-data:
    driver: local               # default; swap for nfs, overlay2, etc.
    driver_opts:
      type: none
      o: bind
      device: /mnt/external-drive   # mount an NFS or external path as a named volume

The nocopy: true flag is worth knowing. By default, when Docker creates a named volume and mounts it into a container, it copies the contents of the container's directory at that path into the volume on the first run. For database images that already handle initialization themselves (like the official Postgres image), nocopy: true avoids unnecessary copying.

Bind Mounts: Sync Your Local Code

What is a bind mount in Docker Compose?

A bind mount maps a path on your host machine directly into the container at a specified path. Changes on the host appear in the container immediately, and vice versa. This makes bind mounts the standard approach for local development — your editor saves a file, and the running container (with hot reload) picks it up without a rebuild.

# docker-compose.yml — bind mount for live code sync
services:
  api:
    build: .
    volumes:
      # Syntax: <host-path>:<container-path>
      - ./src:/app/src            # sync local ./src into container
      - ./config:/app/config:ro   # :ro = read-only inside container
    ports:
      - "3000:3000"
    command: npm run dev

The ./src path is relative to the directory containing your docker-compose.yml. You can also use absolute paths:

volumes:
  - /home/myuser/projects/myapp/src:/app/src

Relative paths are almost always better for team projects — they work on any developer's machine without modification.

Watch out for node_modules. A classic bind mount trap: if you mount your entire project root into the container but the container has its own node_modules built during the image build, the bind mount will shadow (overwrite) the container's node_modules with your host's version — or an empty directory if you don't have node_modules locally. The fix is an anonymous volume on node_modules:

services:
  web:
    build: .
    volumes:
      - .:/app                      # bind mount entire project
      - /app/node_modules           # anonymous volume protects node_modules
    ports:
      - "3000:3000"
    command: npm run dev

The second volume entry — /app/node_modules with no source — creates an anonymous volume. Docker uses the directory from the image build and keeps it isolated from the host bind mount layered on top.

Bind mounts are also handy when debugging environment variable issues in containerized deploys. If you're seeing a service fail to start because a config file isn't where it expects, temporarily bind-mounting a local config file into the container narrows down whether the problem is the file content or the build process. The same approach applies when debugging Vercel build failures that you can't reproduce in CI — mounting local build output into a test container gets you closer to the production environment.

Sharing Volumes Between Services

How do you share a volume between two Docker Compose services?

Declare the volume at the top level once, then reference it in as many services as need it. Both containers will read and write to the same storage on disk.

A common use case: a background worker that processes files uploaded by a web service.

# docker-compose.yml — shared volume between web and worker
services:
  web:
    build: ./web
    volumes:
      - uploads:/app/uploads    # web writes uploaded files here
    ports:
      - "8080:8080"

  worker:
    build: ./worker
    volumes:
      - uploads:/data/uploads   # worker reads from the same storage
    depends_on:
      - web

volumes:
  uploads:
    driver: local

The paths inside the container don't have to match — web sees the volume at /app/uploads while worker sees it at /data/uploads. What matters is that both point to the same uploads volume name.

Coordination warning: Docker volumes don't implement any file locking at the storage level. If both services write to the same file simultaneously, you'll get data corruption. Design your services so only one writes to a given file or directory, or implement locking in your application logic.

You can also share a volume with a service defined in a separate Compose file by using external: true:

# docker-compose.worker.yml — consuming a volume defined elsewhere
volumes:
  uploads:
    external: true      # Docker expects this volume to already exist

This is useful in multi-project setups where a shared data layer is managed by one Compose stack and consumed by others.

If you're running Supabase in Docker alongside a custom service and hitting connection issues, the shared networking and volume patterns in our Supabase connection refused guide apply directly — both problems come from services not being able to reach each other through Docker's internal network.

Common Volume Errors and Fixes

"No such file or directory" on container start

This usually means the host path in a bind mount doesn't exist yet. Docker does not create the host directory automatically for bind mounts (unlike named volumes, which Docker creates on first run).

# Create the host directory before running Compose
mkdir -p ./data/uploads

docker compose up -d

Add the mkdir step to your project setup script or Makefile so it runs before docker compose up in fresh environments.

Permission denied writing to a volume

# Error you see in logs:
# open /app/data/output.log: permission denied

The container process runs as a non-root user (good security practice), but the mounted directory on the host is owned by root or a different UID. There are three clean fixes:

# Fix 1: Set the user in the Compose service config
services:
  app:
    image: myapp:latest
    user: "1000:1000"    # match the UID:GID of your host user
    volumes:
      - app-data:/app/data
# Fix 2: Fix ownership on the host directory (for bind mounts)
chown -R 1000:1000 ./data

# Fix 3: Fix ownership inside the container via Dockerfile
# In your Dockerfile:
# RUN mkdir -p /app/data && chown -R node:node /app/data

Check what UID your container process runs as with docker compose exec <service> id.

Volume data missing after docker compose down

If your data disappears after docker compose down, check two things:

  1. Are you using a named volume or a bind mount? If neither, your data was in the container's writable layer and is gone.
  2. Did you run docker compose down -v? The -v flag explicitly removes named volumes. Omit it to keep volumes when tearing down containers.
# This destroys named volumes — data is gone
docker compose down -v

# This keeps named volumes intact
docker compose down

Recovering data from a dropped volume isn't always possible — see the Docker volume backup docs for backup strategies before you hit this in production.

Stale bind mount caching on macOS and Windows

On Docker Desktop for macOS and Windows, bind mounts go through a translation layer between the VM and your host OS. In older Docker Desktop versions this causes file changes on the host to not propagate to the container fast enough for hot-reload to work reliably.

The fix is the delegated or cached mount consistency flag (Docker Desktop 4.x) or the newer VirtioFS file sharing backend in Docker Desktop 4.6+:

services:
  web:
    volumes:
      # Legacy fix for Docker Desktop < 4.6
      - .:/app:delegated

      # Better: enable VirtioFS in Docker Desktop preferences
      # then remove the consistency flag — default performance is fast

Check your Docker Desktop version and enable VirtioFS under Settings → General if it's available. It eliminates the latency problem entirely.

"Volume already exists with different options"

When you change a named volume's driver or driver_opts after it already exists, Docker throws this error. The volume was created with the old options and won't be updated in place.

# Inspect the existing volume options
docker volume inspect yourproject_db-data

# Remove the volume (warning: destroys data — back up first)
docker volume rm yourproject_db-data

# Re-run Compose to recreate it with the new options
docker compose up -d

Back up your data before removing a volume. The Docker volume backup guide shows how to docker run --rm a temporary container to tar the volume contents to your host.

For database-specific data management patterns — especially around init scripts, migrations, and seed data in Dockerized Postgres — the RLS policy debugging guide covers how data access issues surface differently in containerized vs hosted Supabase environments.

Conclusion

Docker Compose volumes aren't complicated once you understand the three types and the two-part declaration pattern. Named volumes for any data that must survive restarts. Bind mounts for local dev where you need live sync. tmpfs for ephemeral secrets and scratch space. Declare named volumes in both the service config and the top-level volumes: key, never run docker compose down -v unless you intend to wipe data, and add the anonymous volume trick anytime you bind-mount a directory that shadows node_modules or similar.

The errors that bring developers to a stop — lost data, permission denials, stale cache on macOS — all have clean fixes once you know where to look. Docker's compose file volumes reference and the bind mounts documentation are the authoritative sources for every option covered here.

When the volume configuration is correct but your database container is still crashing, your app still can't reach its data store, or a migration is failing in a way the logs don't explain — that's when a second pair of eyes pays off faster than another hour of solo debugging. FlowQL connects you live with a senior engineer who has debugged this exact class of problem before. Thirty minutes, screen share, fixed — or you don't pay.

Still blocked?

This fix didn't work for your setup? Get a senior engineer on your screen in 30 minutes — fixed or refunded.

Reserve My Spot →
Still stuck?

Get a senior engineer on your screen.

30-minute live screen-share sessions with a vetted senior dev. Fixed or fully refunded — no questions asked.

No spam. Just a heads-up when sessions open.

Related Articles