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.
- 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 - From Git Bash: the same with
/c/Users/..., watch it fail, then prefixMSYS_NO_PATHCONV=1 docker volume create dw-p3-vol, write from one container, read from another,docker volume inspect- Start PostgreSQL without a volume,
docker inspectits Mounts, remove it with and without-v docker cpa SQL file in, run it, copy it back, compare hashes- Time
dotnet buildinside the SDK image with the source on C: and in a volume - 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 location | Run 1 | Run 2 |
|---|---|---|
| C: folder through a bind mount | 12.69 s | 8.88 s |
| Named volume inside the engine | 6.76 s | 4.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.