Docker Compose Bind Mounts vs Named Volumes: When to Use Each

Jay Rogers

April 2nd, 2026

A question came up on GitHub that I've seen asked many times over the years: a user had a Laravel app running with multiple containers (web, task scheduler, Horizon), and the bootstrap cache generated by artisan optimize in the web container wasn't visible to the other containers.

Their fix was a named volume shared across all containers. That totally works. But it made me realize a lot of people don't know when to reach for a bind mount vs a named volume. Let's clear it up.

What's the difference?

Docker Compose gives you two ways to persist and share data: bind mounts and named volumes.

A bind mount maps a path on your host directly into the container. You see both sides. Edit a file on your laptop, the container sees it immediately.

Bind mount
volumes:
  - ./src:/var/www/html

A named volume is managed by Docker. You give it a name, Docker handles where the data lives on disk.

Named volume
volumes:
  - app-data:/var/www/html

They look similar in YAML but behave very differently.

Comparison table

Bind MountNamed Volume
Syntax./host/path:/container/pathvolume-name:/container/path
Where data livesYou choose the host pathDocker manages it
File visibilityEasily accessible from host and containerMore difficult to access from host
Performance (macOS/Windows)Slower due to file system translationFaster (stays inside the Docker VM)
Performance (Linux)Native speedNative speed
PermissionsHost UID:GID must match container userDocker handles ownership
PortabilityTied to host directory structureWorks anywhere Docker runs
Best forSource code in developmentCaches, databases, shared state between containers
Survives docker compose downAlways (files are on your host)Yes, unless you pass -v

The permissions gotcha with bind mounts

Bind mounts are great for development because you can edit code locally and see changes live. This is how we use Spin for local development. But there's a catch.

When you bind mount a directory, the container sees files owned by whatever UID:GID they have on your host. By default, our serversideup/php images run as www-data with UID:GID 33:33 on Debian (or 82:82 on Alpine). If your host user is 1000:1000, the container might not be able to write to your mounted files. Or the container creates files owned by 33:33 and now you need sudo to delete them on your host.

We built tooling to fix this. At build time, you can align the container's user ID with your host:

Dockerfile
FROM serversideup/php:8.5-fpm-nginx-bookworm AS base

FROM base AS development
USER root

ARG USER_ID
ARG GROUP_ID

RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID && \
    docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID

USER www-data

Then pass your host UID/GID as build args in your compose.yml:

compose.yml
services:
  php:
    build:
      context: .
      target: development
      args:
        USER_ID: ${UID}
        GROUP_ID: ${GID}
    volumes:
      - .:/var/www/html

No permission conflicts, no sudo needed. Check out our full guide on Understanding File Permissions for more details.

Spin handles the UID/GID alignment automatically, so you don't have to think about any of this.

Solving the multi-container cache problem with named volumes

Named volumes don't have the permissions headache because Docker manages the ownership. They're the right tool when multiple containers need to share generated data.

Back to the GitHub discussion: the user runs artisan optimize in the web container via Laravel Automations, which caches config, routes, and events into bootstrap/cache/. The task scheduler and Horizon containers need to read that same cache. A named volume makes this simple:

compose.yml
services:
  web:
    image: my/laravel-app
    environment:
      - AUTORUN_ENABLED=true
    volumes:
      - bootstrap-cache:/var/www/html/bootstrap/cache
      - app-storage:/var/www/html/storage

  task:
    image: my/laravel-app
    command: ["php", "/var/www/html/artisan", "schedule:work"]
    volumes:
      - bootstrap-cache:/var/www/html/bootstrap/cache
      - app-storage:/var/www/html/storage

  horizon:
    image: my/laravel-app
    command: ["php", "/var/www/html/artisan", "horizon"]
    volumes:
      - bootstrap-cache:/var/www/html/bootstrap/cache
      - app-storage:/var/www/html/storage

volumes:
  bootstrap-cache:
  app-storage:

Named volumes work well here because the files are generated by the container, not edited by a developer. All containers run as the same user inside the same image, so permissions just work. Other good candidates: database data (mysql-data:/var/lib/mysql), Redis persistence, and any shared storage directory.

Wrapping up

Bind mounts for code you're editing. Named volumes for data the containers generate. Most projects end up using both depending on the environment. If you're running into file permission issues with bind mounts, match the container's UID:GID to your host user, or let Spin handle it for you.

Have questions? Jump into our Discord and let us know.

Want to work together?

Professional developers choose Server Side Up to ship quality applications without surrendering control. Explore our tools and resources or work directly with us.

Join our community

We're a community of 3,000+ members help each other level up our development skills.

Platinum Sponsors

Active Discord Members

We help each other through the challenges and share our knowledge when we learn something cool.

Stars on GitHub

Our community is active and growing.

Newsletter Subscribers

We send periodic updates what we're learning and what new tools are available. No spam. No BS.

Sign up for our newsletter

Be the first to know about our latest releases and product updates.

    Privacy first. No spam. No sharing. Just updates.