LEARN · DEBUGGING GUIDE

Docker Volume Mount Not Showing Files: A Practical Debugging Guide

When Docker mounts appear empty or files don't sync, the problem is almost never 'Docker is broken.' It's usually host path mistakes, mount type confusion, or container user mismatches.

IntermediateDocker9 min read

What this usually means

The root cause typically falls into three categories: host path resolution errors (relative vs absolute, symlinks), mount type confusion (bind vs volume, tmpfs), or user namespace mismatches (UID/GID inside container vs host). Docker does not fail loudly when a mount target is empty; it just creates an empty directory at the mount point if the source doesn't exist. For named volumes, first-time use creates an empty volume, and files from the image are hidden under the mount. The container's user may not have read/write permissions on the host files. Also, Docker for Mac/Windows uses a gRPC FUSE mount for bind mounts, which can cause caching delays or missed writes.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Run 'docker inspect <container> | jq .[].Mounts' to see the exact source, destination, and mode of each mount
  • 2Check if the host path exists and is accessible: 'ls -la /path/on/host' — if it doesn't exist, Docker creates it as an empty directory
  • 3Verify the mount inside the container: 'docker exec <container> ls -la /path/in/container' — compare with host content
  • 4Test with a simple bind mount: 'docker run --rm -v /tmp/test:/data alpine ls /data' — if empty, create a file in /tmp/test first
  • 5If using Docker Compose, run 'docker compose config' to see the resolved mount paths
  • 6Check container user: 'docker exec <container> id' — compare with file ownership on host
  • 7On Docker for Mac, check file system event propagation: 'docker run --rm -v /tmp:/host busybox stat /host/testfile' after creating testfile on host
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchdocker inspect output for Mounts array
  • searchHost filesystem permissions: ls -la /path/on/host
  • searchContainer filesystem at mount point: docker exec <container> ls -la /path/in/container
  • searchDocker Compose YAML file (volumes: section) — check for missing ./ or relative paths
  • searchdocker-compose config resolved output
  • searchContainer logs if application writes to mount: docker logs <container>
  • searchSystem log for FUSE errors (macOS/Windows): Console.app or Event Viewer
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningHost path doesn't exist: Docker creates an empty directory instead of failing
  • warningRelative path in Compose or -v misinterpreted: './data' vs 'data' (named volume vs bind mount)
  • warningNamed volume initialized empty: first use copies no data from image; mount hides image content
  • warningUser ID mismatch: container runs as root but host files owned by 1000:1000, or vice versa
  • warningBind mount on Docker for Mac/Windows uses gRPC FUSE: files may not appear for seconds or minutes
  • warningSELinux or AppArmor blocking access: 'docker run --security-opt label=disable' as test
  • warningMount is a tmpfs or anonymous volume: not persisted to host
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAlways use absolute paths for bind mounts: /home/user/data instead of ./data
  • buildPrepend ./ for relative paths in Docker Compose: - ./data:/container/data
  • buildFor named volumes, copy initial data manually: 'docker cp ./seeddata container:/mount' or use a setup container
  • buildFix permissions: either change host file ownership to match container UID (chown 1000:1000 files) or run container with --user matching host UID
  • buildOn Mac/Windows, force sync: 'docker run --rm -v /tmp:/host busybox touch /host/sync' or use osxfs caching options (cached, delegated)
  • buildUse 'docker volume create' with explicit driver options if needed (e.g., NFS, local with device)
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedCreate a test file on host: echo 'hello' > /tmp/testfile; inside container: docker exec <container> cat /tmp/testfile
  • verifiedWrite a file inside container: docker exec <container> sh -c 'echo world > /data/world'; check on host: cat /data/world
  • verifiedCheck mount flags in 'docker inspect': look for RW: true and Mode: z,Z for SELinux
  • verifiedRun 'docker system df' to confirm volume size matches expected data
  • verifiedTest with a fresh container: docker run --rm -v /absolute/path:/data alpine ls /data
  • verifiedIf using Compose, 'docker compose down && docker compose up -d' and recheck
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDo not use trailing slashes inconsistently: /data vs /data/ can cause unexpected behavior
  • warningDo not assume named volumes are initialized with image data — they are not; use .dockerignore and COPY
  • warningDo not change permissions on host files while container is writing — can cause races
  • warningAvoid relative paths in -v flag: -v data:/data creates a named volume, not a bind mount
  • warningDo not ignore SELinux: if files are missing, try 'docker run --security-opt label=disable'
  • warningDo not use symlinks in host paths: Docker resolves the symlink, but target must exist
( 07 )War story

The Missing Config Files in Production

Senior Backend EngineerDocker 24.0, Docker Compose 2.20, Node.js 18, PostgreSQL 15, Ubuntu 22.04

Timeline

  1. 09:15Deploy new version of config-service with updated docker-compose.yml
  2. 09:18Monitoring alerts: service returning 500 errors, 'config file not found'
  3. 09:22SSH into host, check container: docker exec config-service ls /app/config — empty directory
  4. 09:25Check host path: ls -la /opt/config — contains config files with correct permissions
  5. 09:30Run docker inspect config-service | jq .[].Mounts — source is 'config_data', not '/opt/config'
  6. 09:32Check docker-compose.yml: volumes: - config_data:/app/config — missing host path!
  7. 09:35Fix: change to - /opt/config:/app/config, redeploy
  8. 09:40Service recovers, config files present inside container

During a routine deployment, I updated the docker-compose.yml for our config-service. I had intended to bind mount the host directory /opt/config into the container at /app/config. But in the volumes section I wrote just 'config_data:/app/config', thinking config_data was a relative path. Docker interpreted it as a named volume called 'config_data', which was empty. The service started, looked for config files in /app/config, found nothing, and crashed.

The first symptom was HTTP 500s. I ssh'd in and ran docker exec config-service ls /app/config — empty. Then I checked the host: /opt/config had 20 YAML files. That was a red flag. I ran docker inspect and saw the source was 'config_data', not '/opt/config'. The compose file had no host prefix, so Docker treated it as a named volume. The named volume was empty because it was created fresh.

I fixed it quickly by changing the volume to an absolute bind mount: /opt/config:/app/config. Then I ran docker compose up -d --force-recreate. The service came up healthy. The lesson: always use absolute paths for bind mounts in production, and always verify with docker inspect after a deployment. I also added a pre-flight check in our CI that validates mount paths.

Root cause

Typo in docker-compose.yml: used named volume syntax instead of bind mount (missing host path prefix).

The fix

Changed volume definition to absolute bind mount: /opt/config:/app/config.

The lesson

Always use absolute paths for bind mounts in production Composes. Never rely on relative paths or assume Docker will warn you. Inspect mounts after every deployment.

( 08 )Understanding Docker Volume Types: Bind vs Named vs tmpfs

Bind mounts map a specific host path directly into the container. If the host path doesn't exist, Docker creates an empty directory at that path. This is the most common surprise: you expect a warning, but you get an empty mount. Named volumes are managed by Docker and stored in /var/lib/docker/volumes/. They are initialized empty on creation, even if the image has files at the mount point — the image content is hidden. tmpfs mounts are in-memory and never persisted. When you write volumes: in Compose, a simple string like 'data:/app/data' creates a named volume, whereas './data:/app/data' creates a bind mount. The leading dot or slash is critical.

To troubleshoot, always check the 'Type' field in docker inspect Mounts. 'bind' means host path, 'volume' means Docker volume. For bind mounts, the 'Source' should be an absolute path. For volumes, 'Source' is a Docker volume name or ID. Also check 'Mode' for SELinux labels (z, Z) which can cause permission issues.

( 10 )User ID and Permission Mismatches

Containers often run as root (UID 0) or a non-root user (e.g., node:1000). Host files have Unix permissions. If the container user doesn't have read/write access to the mounted files, you'll see 'Permission denied' or the files appear as owned by nobody. Check with docker exec <container> id and compare to host file ownership (ls -n). The fix: either run the container with --user matching the host UID (e.g., --user 1000:1000) or adjust host permissions (chown -R 1000:1000 /host/path).

For named volumes, Docker creates the volume directory with root ownership. If the container runs as non-root, it may not be able to write. Solution: either set user in Dockerfile, or use a volume driver that respects permissions, or initialize the volume with correct ownership in an entrypoint script.

( 11 )Docker Compose Volume Syntax Pitfalls

Docker Compose volume syntax is subtle. A volume entry like 'data:/app/data' creates a named volume called 'data'. To bind mount, use './data:/app/data' or '/absolute/path:/app/data'. Many engineers use the short syntax (just 'data') and expect a bind mount. Always run docker compose config to see the resolved volumes section. It will show the actual source paths. Also note that top-level volumes: declares named volumes, but if you use a bind mount you don't need to declare it there.

Another common mistake: using environment variables in the host path without ensuring they are set. For example, '${HOME}/data:/app/data' may resolve to an empty string if $HOME is not set in the docker-compose context, resulting in '/data:/app/data' which is a named volume. Use '~' expansion carefully — Docker does not expand '~' in paths. Always use absolute paths or $PWD.

( 12 )Filesystem Caching and Delayed Writes on macOS/Windows

Docker Desktop on macOS and Windows uses a filesystem sharing mechanism that can introduce caching. On macOS, bind mounts use gRPC FUSE (since Docker 4.30) or the legacy osxfs. Files written on the host may not appear in the container immediately (or vice versa). The delay can be up to a few seconds. To mitigate, use mount flags: :cached (container's view is eventually consistent) or :delegated (container's writes are deferred). But these trade consistency for performance.

If you suspect caching, run a sync command: on host, run 'docker run --rm -v /tmp:/host busybox stat /host/testfile' after creating testfile. If it doesn't appear, try 'docker run --rm -v /tmp:/host busybox sh -c "echo sync > /host/sync"'. For production on macOS, consider using named volumes (which use the Linux VM's native filesystem) instead of bind mounts. On Linux, this issue does not exist.

Frequently asked questions

Why does my Docker named volume show an empty directory even though the image has files at that location?

Named volumes are initialized empty. When you mount a named volume at a path that already has files in the image, those files are hidden by the empty mount. Docker does not copy image content into the volume. To include initial data, either use a bind mount to a host directory with the files, or run a one-time setup container that copies files into the volume.

I used -v /myapp/data:/app/data but inside the container /app/data is empty. What went wrong?

First, check if /myapp/data exists on the host. If it doesn't, Docker creates it as an empty directory. Use absolute paths and verify with 'ls -la /myapp/data'. Also, ensure there are no symlinks or spaces in the path. Run 'docker inspect' to confirm the mount source.

How do I fix 'Permission denied' when accessing mounted files inside a container?

Check the UID of the container process with 'docker exec <container> id'. Compare with the owner of the files on the host (ls -n). Either change the host file ownership to match the container UID (e.g., chown -R 1000:1000 /host/path) or run the container with --user flag matching the host UID. For named volumes, consider using a volume driver that supports permissions.

Why do my bind mount changes not appear immediately on Docker for Mac?

Docker Desktop for Mac uses gRPC FUSE (or osxfs) which can introduce caching delays of a few seconds. Use mount flags like ':cached' or ':delegated' to control consistency. For immediate sync, trigger a file operation (e.g., touch a file). Alternatively, use named volumes which bypass the host filesystem and are faster.

In Docker Compose, what's the difference between 'volumes: - ./data:/app/data' and 'volumes: - data:/app/data'?

The first is a bind mount: it maps the host directory './data' (relative to the Compose file) to '/app/data'. The second creates a Docker named volume called 'data' (or references an existing one). Named volumes are stored in Docker's storage directory and persist independently of the host filesystem. To avoid confusion, always use absolute paths or start with './' for bind mounts.