Archive note, October 2025: This was written while I
was learning Docker through a real WordPress project. The core
distinction between named volumes and bind mounts remains useful.
I edited a CSS file in a normal folder on my Mac, refreshed local
WordPress, and the change appeared inside a Linux container.
No upload. No image rebuild. No file-copy command.
The explanation was not synchronization magic. WordPress and my
editor were looking at the same mounted directory through two filesystem
paths.
Container storage is
not project storage
A container has a writable filesystem layer above its image. Changes
made there normally survive a stop and start of that same container, but
they are tied to the container. Remove and recreate it, and that
writable layer is not a dependable place for application data.
Docker mounts place important data outside that layer.
For WordPress development, I used two kinds:
services:
db:
volumes:
- db_data:/var/lib/mysql
wordpress:
volumes:
- wp_data:/var/www/html
- ./wp-theme:/var/www/html/wp-content/themes/my-child-theme
volumes:
db_data:
wp_data:
db_data and wp_data are named volumes.
./wp-theme is a bind mount.
Named volumes
A named volume is managed by Docker. The Compose file refers to it by
name rather than by a normal host path:
- db_data:/var/lib/mysql
This is appropriate for database storage because:
- Docker manages its location and lifecycle.
- Linux containers see a native filesystem.
- The data persists when containers are recreated.
- I do not need to edit MariaDB’s internal files directly.
Named volumes are inspectable and back upable, but they are
intentionally less convenient than an ordinary project directory.
Bind mounts
A bind mount maps a specific host path into the container:
- ./wp-theme:/var/www/html/wp-content/themes/my-child-theme
The left side is the project directory on the Mac. The right side is
where WordPress expects the child theme inside the container.
When PHP opens:
/var/www/html/wp-content/themes/my-child-theme/functions.php
Docker presents the file from:
./wp-theme/functions.php
There is no separate deployment copy inside the container. That is
why saving locally changes what WordPress reads on its next request.
Bind mounts are appropriate for:
- Source code
- Configuration under active development
- Files that belong in Git
- Anything I need to inspect and edit with host tools
They are less attractive for database internals because
host/container permission differences and cross-platform filesystem
translation can hurt reliability and performance.
The path on the right
matters
WordPress identifies a theme by its directory and stylesheet
metadata. If the database expects my-child-theme but the
mount targets generatepress-child, WordPress sees a
different theme.
The container path must match the application expectation:
- ./wp-theme:/var/www/html/wp-content/themes/my-child-theme
A useful diagnostic is:
docker exec my-wp \
ls -la /var/www/html/wp-content/themes/my-child-theme
If that directory is empty or missing, check:
- Does
./wp-themeexist? - Did Compose start from the expected project?
- Is the target path spelled correctly?
- Was the container recreated after changing the mount
configuration?
docker compose restart restarts an existing container.
It does not necessarily apply a changed Compose definition. Recreate it
with:
docker compose up -d --force-recreate
or bring the project down and up again.
A mount can hide image
content
Mounts overlay the target path.
If an image already contains files at /some/path and a
bind mount is attached there, the mounted directory hides those image
files for the life of the container. They were not deleted; they are
simply obscured.
This explains a common surprise: mounting an empty host directory
over a populated container directory makes the container directory
appear empty.
For my theme, that behavior was desirable because the host directory
was the source of truth. It would be dangerous if done accidentally over
all of wp-content.
Permissions
The container’s web server runs under a Linux user such as
www-data. Host files belong to my macOS account. Docker
Desktop mediates much of that mismatch, but Linux hosts expose it more
directly.
For theme source, I generally wanted:
- The host editor to own and modify files.
- The container to read them.
- WordPress not to rewrite theme source.
A read-only mount can enforce that:
- ./wp-theme:/var/www/html/wp-content/themes/my-child-theme:ro
For local workflows that genuinely require WordPress to write into
the mount, ownership and permissions need deliberate handling.
Recursively changing source files to the container’s numeric UID is not
always the best answer, especially on a shared repository.
Verifying both sides
The quickest proof is to compare the same file:
# Host
shasum -a 256 ./wp-theme/style.css
# Container
docker exec my-wp \
sha256sum /var/www/html/wp-content/themes/my-child-theme/style.css
Matching checksums confirm that both paths expose the same bytes.
To inspect the mount definition:
docker inspect my-wp
The Mounts section shows the source, destination, and
mount type.
macOS performance
Docker Desktop runs Linux containers inside a virtualized
environment. Accessing many small host files through a bind mount can be
slower than accessing a named volume inside the Linux filesystem.
For a small WordPress child theme, the trade-off was worthwhile. For
MariaDB, node_modules, or build caches, named volumes or
container-native storage are often better.
The practical rule I took away was:
- Bind mount what humans edit.
- Use named volumes for service-managed state.
- Do not let an individual container be the only owner of important
data.
Why understanding this
mattered
Before the local environment, theme development felt like moving
files between worlds. The bind mount made the relationship explicit: one
source directory, presented in two contexts.
That understanding made troubleshooting easier too. If WordPress did
not show a change, I could ask whether the problem was:
- The host file
- The mount
- The container path
- WordPress caching
- Browser caching
“Docker is not seeing my file” stopped being a mysterious category of
failure. It became a short chain I could inspect one link at a time.