A container that cannot see your files is useless, and on Windows there are three ways to write a path, two shells that treat it differently, and one kind of storage that is not a Windows folder at all. This part mounts a folder from PowerShell and from Git Bash, watches what Git Bash does to the path, creates a named volume and finds out where its bytes are, catches the anonymous volumes that PostgreSQL leaves behind, copies files with docker cp, and measures the same build with the source on C: and on a Linux file system. Everything ran on this PC and every output is quoted.

Steps in this part
  1. From PowerShell: docker run --rm -v "${PWD}\data:/data" alpine ls -la /data, then let the container write a file and read it on Windows
  2. From Git Bash: the same with /c/Users/..., watch it fail, then prefix MSYS_NO_PATHCONV=1
  3. docker volume create dw-p3-vol, write from one container, read from another, docker volume inspect
  4. Start PostgreSQL without a volume, docker inspect its Mounts, remove it with and without -v
  5. docker cp a SQL file in, run it, copy it back, compare hashes
  6. Time dotnet build inside the SDK image with the source on C: and in a volume
  7. Read the PostgreSQL 18 image's data layout: VOLUME, PGDATA and show data_directory

A Windows folder inside a Linux container

From PowerShell all three spellings mounted the same folder: ${PWD}\data, C:\Users\you\clinic\data and C:/Users/you/clinic/data. The container wrote hello.txt and Get-Content read it back on Windows. What the container sees is not a Linux disk but a file share of drive C:, and its mount table says so:

C:\ on /data type 9p (rw,noatime,aname=drvfs;path=C:\;uid=0;gid=0;metadata;symlinkroot=/mnt/host/,cache=0x5,access=client,msize=65536,trans=fd,rfd=5,wfd=5)

Docker's documentation explains the arrangement: "For Docker Desktop, the daemon runs inside a Linux VM, not directly on the native host. Docker Desktop has built-in mechanisms that transparently handle bind mounts". The 9p share is that mechanism, and it is why files written on Windows showed up inside as -rwxrwxrwx while files the container created were -rw-r--r--, and why the timing section below looks the way it does.

What Git Bash does to your path

Git Bash rewrites arguments that look like Unix paths into Windows paths before Docker sees them. The simplest proof needs no mount at all:

docker run --rm alpine echo /data
C:/Program Files/Git/data

With a mount, -v /c/Users/you/clinic/data:/data arrived at Docker as C:\Users\you\clinic\data;C:\Program Files\Git\data, and ls /data failed with "No such file or directory". Engine 29.8.2 did not reject the mangled mount, which the documentation's example suggests it should; it created an empty folder named data;C next to the real one on Windows and ran the container against it. The fix that worked every time is the environment variable that turns the conversion off:

MSYS_NO_PATHCONV=1 docker run --rm -v /c/Users/.../data:/data alpine ls /data

Two more forms worked: a double slash at the start of the host path, //c/Users/..., and "$(pwd -W)/data:/data", which asks Git Bash for the Windows spelling. A double slash on the container side of the mount, ...://data, did not: Docker turned it into an anonymous volume with a nonsense name. And an unquoted C:\Users\... loses every backslash in bash and fails with "The system cannot find the file specified". If you work in Git Bash, put export MSYS_NO_PATHCONV=1 in your profile and forget the problem; the Fixes post has the short version.

Named volumes: Docker's own storage

docker volume create dw-p3-vol, one container writing kept.txt into it, a second reading it back. docker volume inspect tells you where it is:

"Mountpoint": "/var/lib/docker/volumes/dw-p3-vol/_data",

That is a Linux path inside the engine's VM. On Windows it does not exist. From a container, the volume is a real disk:

/dev/sde                1006.9G     26.5G    929.1G   3% /data

An ext4 file system of about a terabyte with 26.5 G used, which is the docker_data.vhdx from Part 1 seen from the inside; the bind mount in the previous section reported drive C: and its 1.8 TB instead. The documentation puts the choice plainly: "Volumes are the preferred mechanism for persisting data generated by and used by Docker containers", and "Use bind mounts if you need to access files or directories from both containers and the host." Database data goes in a volume; source code you edit on Windows is a bind mount.

The volumes you did not ask for

The PostgreSQL image declares a volume, {"/var/lib/postgresql":{}} in its image metadata, so every container from it gets an anonymous volume with a 64-character name, visible in docker inspect under Mounts. Remove the container with a plain docker rm and the volume stays, labelled com.docker.volume.anonymous; docker rm -v takes it with the container, and so does --rm at run time, which the documentation confirms: anonymous volumes "persist even if you remove the container that uses them, except if you use the --rm flag". Part 8 counts how many of these a few weeks of experiments leave behind.

docker cp, and a link that fooled the Windows client

docker cp ClinicApi\db\01-schema.sql dw-p3-db:/tmp/01-schema.sql put the file inside with mode 755 and the Windows modification time; psql -f ran it; copying it back out gave an identical hash. Copying out of a stopped container works too. The surprise was docker cp dw-p3-db:/etc/os-release .: in the Debian image that path is a symbolic link to ../usr/lib/os-release, and Windows received a symbolic link, not a file, which Get-Content could not read. The documented answer, docker cp -L, failed from the Windows client with "Could not find the file /etc/usr/lib/os-release", a wrong resolution of the relative link, while the Linux client of the same version followed it correctly. Copy the target instead: docker exec dw-p3-db readlink -f /etc/os-release, then docker cp that path.

Where the source lives decides the build time

The same dotnet build of the clinic API ran inside the SDK image twice with the source on C:, bind-mounted, and twice with the source copied into a named volume. The plan was to use a folder inside Ubuntu-24.04, but Docker's WSL integration is off for that distribution on this PC and the volume is the same kind of Linux file system.

Source locationRun 1Run 2
C: folder through a bind mount12.69 s8.88 s
Named volume inside the engine6.76 s4.33 s

Every run includes a package restore from nuget.org, because a --rm container starts with an empty package folder; with the packages kept in a volume as well, the pairs were 4.83 and 5.55 seconds on C: against 2.48 and 2.55 in the volume. About two to one in every pairing, which is two to four seconds for this one-file project and minutes for a real solution. The documentation's best practices page says the same in one sentence: "Performance is much higher when files are bind-mounted from the Linux filesystem, rather than accessed from the Windows host filesystem." These were single runs on a busy machine, so take the ratio, not the decimals.

Where PostgreSQL 18 keeps its data

Read-only, for Part 5: the 18 image declares its volume at /var/lib/postgresql and sets PGDATA=/var/lib/postgresql/18/docker; show data_directory answers the same versioned folder. The 16 image used /var/lib/postgresql/data for both. A volume mounted at the old path is not where 18 keeps its data, which is the whole story behind the most read Fixes post on this site, and Part 5 runs that case on purpose.

The rule of thumb: bind mount what you edit on Windows, volume what the container owns, quote paths in Git Bash or turn conversion off, and remove containers with -v.

Frequently asked

How do I mount a Windows folder into a Docker container?
From PowerShell, -v "${PWD}\data:/data" or a full C:\ path, quoted; forward slashes work too. Inside the container the folder arrives as a 9p file share of the drive, which is slower than a Linux file system but lets you edit on Windows and run in Linux. From Git Bash, set MSYS_NO_PATHCONV=1 first, or the shell rewrites /data into a Windows path before Docker sees it.
Where are Docker volumes stored on Windows?
Inside the engine's Linux VM, at /var/lib/docker/volumes/<name>/_data, which is not a Windows path. That directory sits on the docker_data.vhdx virtual disk under %LOCALAPPDATA%\Docker\wsl, and from a container it appears as an ext4 disk of about a terabyte. Use docker cp or a container to get files out of a volume; use a bind mount when Windows must see the files.
Why does Docker leave volumes behind after I remove a container?
Images such as postgres declare a VOLUME, so each container gets an anonymous volume with a long hexadecimal name. A plain docker rm keeps it; docker rm -v removes it with the container, and docker run --rm removes it at exit. List the leftovers with docker volume ls and remove them by name.

Next: Part 4, ports and networking: who owns a published port on Windows, the ranges Windows reserves, and two containers that find each other by name.