Fixing NGINX Bind Mount Empty Directory — Host Folder Overwrites Container Content

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:\docker used in compose file)
  • Container Port Mapping8081:80
  • Target Mount Path/usr/share/nginx/html (NGINX default web root)
  • Relevant Configdocker-compose.yaml with volumes section; Dockerfile with VOLUME instruction

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.html50x.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

BehaviorBind MountNamed Volume
Populates with container content on creationNo — host content obscures container contentYes — copies container content into volume
Host filesystem visible inside containerYes — direct host path mappingNo — managed by Docker
Suitable for live developmentYesNo (limited)
Suitable for productionWith cautionYes

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

(Last verified: 17 June 2026 — Docker Engine 28.0+, Windows 11 / WSL2)