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.
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.
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.
volumes:
- app-data:/var/www/html
They look similar in YAML but behave very differently.
| Bind Mount | Named Volume | |
|---|---|---|
| Syntax | ./host/path:/container/path | volume-name:/container/path |
| Where data lives | You choose the host path | Docker manages it |
| File visibility | Easily accessible from host and container | More difficult to access from host |
| Performance (macOS/Windows) | Slower due to file system translation | Faster (stays inside the Docker VM) |
| Performance (Linux) | Native speed | Native speed |
| Permissions | Host UID:GID must match container user | Docker handles ownership |
| Portability | Tied to host directory structure | Works anywhere Docker runs |
| Best for | Source code in development | Caches, databases, shared state between containers |
Survives docker compose down | Always (files are on your host) | Yes, unless you pass -v |
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:
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:
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.
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:
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.
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.
Professional developers choose Server Side Up to ship quality applications without surrendering control. Explore our tools and resources or work directly with us.

We're a community of 3,000+ members help each other level up our development skills.
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.
Be the first to know about our latest releases and product updates.