Environment Context: NGINX on Docker with Windows Host Paths
- Component/Version: NGINX (official image, built from custom Dockerfile)
- Deployment Method: Docker Compose with custom build context
- Host OS: Windows (path
c:\dockerused in compose file) - Container Port Mapping:
8081:80 - Target Mount Path:
/usr/share/nginx/html(NGINX default web root) - Relevant Config:
docker-compose.yamlwith volumes section;DockerfilewithVOLUMEinstruction
The Symptom: Empty HTML Directory After Bind Mount
The NGINX container starts successfully and the port mapping functions as expected. However, when navigating to the web server, the default NGINX welcome page is not served. Instead, the browser returns a 403 Forbidden or directory listing with no files.
Inside the container, the mounted directory (/usr/share/nginx/html) appears empty:
docker exec -it <container_id> ls -la /usr/share/nginx/html
The host directory (c:\docker) is also empty. Files that were copied into the container image during the docker build phase—whether via COPY or ADD instructions in the Dockerfile—are not present at the mount point.
This occurs because the bind mount obscures the container’s pre-existing directory contents with the host directory’s contents at container startup. Since the host folder is empty, the container sees an empty directory regardless of what was built into the image.
The Solution: Three Approaches to Preserve Container Content
Approach 1: Pre-Populate the Host Directory (Recommended for Development)
Copy the container’s default content to the host directory before starting the container, or use an initialization step.
Step 1 — Start a temporary container to extract the default HTML files:
docker run --rm -d --name nginx-temp nginx:latest docker cp nginx-temp:/usr/share/nginx/html/. c:\docker\ docker stop nginx-temp
Step 2 — Verify the host directory now contains the default NGINX files:
dir c:\docker
Expected output includes index.html, 50x.html, and other static assets.
Step 3 — Start your compose stack normally:
docker-compose up -d
The bind mount now presents the host directory’s content (populated with the default files) to the container, and you can edit files on the host with changes reflected immediately in the container.
Approach 2: Use an Entrypoint Initialization Script
Modify the Dockerfile to copy default content into the mounted directory at runtime, rather than at build time. This approach works regardless of whether the host directory is empty.
Dockerfile modification:
FROM nginx:latest # Copy default content to a staging location COPY ./html-default /usr/share/nginx/html-staging # Entrypoint script that copies staging content to the mount point if empty COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["nginx", "-g", "daemon off;"]
entrypoint.sh:
#!/bin/sh
set -e
# If the mounted directory is empty, populate it from staging
if [ -z "$(ls -A /usr/share/nginx/html)" ]; then
cp -r /usr/share/nginx/html-staging/* /usr/share/nginx/html/
fi
exec "$@"
Approach 3: Use Named Volumes Instead of Bind Mounts (Recommended for Production)
Named volumes initialize with the container’s content at creation time, unlike bind mounts.
docker-compose.yaml modification:
version: '3.8'
services:
nginx:
build: .
ports:
- "8081:80"
volumes:
- nginx-html:/usr/share/nginx/html
volumes:
nginx-html:
On first startup, Docker creates the named volume and populates it with the contents of /usr/share/nginx/html from the container image. Subsequent container restarts preserve the data.
To modify files, use docker exec or attach to the volume container:
docker run --rm -v nginx-html:/data alpine cp /data/index.html ./index.html
Note: Named volumes do not provide direct host filesystem access for live editing. For development workflows requiring host-side file editing, Approach 1 with a pre-populated bind mount is more suitable.
Verification
After applying any of the above solutions, verify the mount is functioning correctly:
# Check container logs for errors docker logs <container_id> # Verify files are present inside the container docker exec <container_id> ls -la /usr/share/nginx/html # Test the web endpoint curl http://localhost:8081
Root Cause Summary
| Behavior | Bind Mount | Named Volume |
|---|---|---|
| Populates with container content on creation | No — host content obscures container content | Yes — copies container content into volume |
| Host filesystem visible inside container | Yes — direct host path mapping | No — managed by Docker |
| Suitable for live development | Yes | No (limited) |
| Suitable for production | With caution | Yes |
The core issue stems from misunderstanding Docker’s bind mount semantics: bind mounts do not merge content—they replace the container directory with the host directory’s contents. This is analogous to mount --bind /src /target on Linux systems.
References
- Official Docker Documentation — Bind Mounts
- Official Docker Documentation — Use Bind Mounts (Getting Started)
- Official Docker Documentation — Sharing Local Files with Containers
- Docker Community Forums — Bind Mount Files from Container Not Showing to Host
(Last verified: 17 June 2026 — Docker Engine 28.0+, Windows 11 / WSL2)