Docker Volume Mount Fails After VPN Connects: Check the Daemon
Your bind mount is fine. A VPN changes which daemon answers the client, so print the endpoint, prove the path there, and stop editing the volume.
Nearly every write-up of this problem blames the volume, and that is exactly why the fix takes so long. You had a bind mount that worked, the corporate VPN connected, and docker compose up now fails with a missing source path, an empty directory, or a mount the engine refuses to create. In the units I have cleaned up, the Compose file had not changed and the path was correct. What changed was which daemon received the request, and the mount error was simply the first place that difference became visible.
The volume never moved; the request did
A -v /srv/app:/app flag is not a directory copy performed by your shell. The client turns it into an API request, and the daemon that answers resolves /srv/app against its own filesystem. When both sides run on the same kernel, that path is the one you see in your file manager. When the endpoint changes, the same string is looked up on a machine that has never heard of your project directory, and the daemon either reports the source as missing or quietly creates an empty directory in its place.
This is also why a named volume keeps working while a bind mount does not. -v appdata:/app names storage the daemon owns, so no host path has to exist anywhere in particular. If swapping one service from a bind mount to a named volume makes the error disappear, you have not fixed the problem. You have confirmed that the client and the daemon disagree about which filesystem the path belongs to, and that disagreement returns the moment you need a real host directory again.
Print the endpoint before you touch the mount
Several settings can point the client at an endpoint, and they do not always agree: a --context flag, DOCKER_CONTEXT, DOCKER_HOST in the current shell, and the context saved in the client configuration. Rather than reasoning about precedence, print all of them and compare the answers.
docker context show
docker context ls
docker version
docker info --format "{{.Name}} {{.OperatingSystem}} {{.ServerVersion}}"
The client config lives at ~/.docker/config.json on Linux and macOS and at %USERPROFILE%\.docker\config.json on Windows, and a value exported in the current terminal overrides the saved one for that session only. If the endpoint is a hostname instead of a local socket, name resolution and routes decide where it lands, and those are precisely what a VPN client rewrites. A tunnel that adds a route or swaps the resolver can send a name that used to mean this laptop to an address inside the corporate network.
Compare state instead of configuration. Run docker ps -a and docker images with the tunnel down, then again with it up. A different list of containers is stronger evidence than any config file, because it shows that a different daemon answered your request.
Ask the daemon whether the path exists there
Once you know which daemon you reached, make it look at the directory. The --mount form fails with a clearer message than the short flag, because it validates the source before it starts the container:
docker run --rm --mount type=bind,source=/srv/app,target=/app alpine ls -la /app
If that reports the source as missing, create it on the machine that answered, or point the service at a named volume and copy the data in. What you should not do is recreate volumes and reinstall the engine before this check. Those actions change the environment enough to hide the mismatch while leaving it in place.
One more path runs somewhere else entirely: pulling an image is daemon work, not client work. A successful pull only proves that the daemon can reach a registry, and a working bind mount only proves that a path resolved. Test each path where it actually executes, or you will spend an afternoon on the wrong one.
Intranet names fail for a different reason
The second half of the same report is usually that the container cannot reach intranet.corp.example. That is a separate mechanism, not a second volume problem. A container gets its /etc/resolv.conf when it starts, generated from the daemon resolver settings, so a DNS server the VPN added after the container was created will not appear in it until the container is recreated.
The Docker embedded resolver on a user-defined network forwards external lookups to the host configured servers, which helps when the VPN rewrites the host resolver and hurts when split DNS sends an internal zone to a server the daemon no longer sees. A narrow, explicit mapping sidesteps the argument for names that never change:
services:
web:
extra_hosts:
- "intranet.corp.example:10.20.30.40"
This writes an entry into the container hosts file, so the container never learns anything about the tunnel, and it also freezes the address. When the internal host moves, the entry stays wrong until someone edits the Compose file. Use it for a fixed appliance or a pinned database, and let DNS handle names that move.
What a reinstall actually changes
Reinstalling Docker Desktop recreates the VM, the socket, and the context list, and the mount starts working again. That outcome is real and it teaches nothing, which is why the same failure returns after the next VPN update. The reinstall replaced the endpoint the client was using; it did not reconcile the path your project names with the filesystem the daemon actually reads.
My order of work is short. Confirm which endpoint the client will use. Confirm which daemon answered by comparing container lists. Ask that daemon to list the source path. Only then edit the mount, add a hosts entry, or change a volume type. If you maintain the Compose file for a team that works over a VPN, prefer named volumes for data the daemon should own, so that a host path never becomes part of the contract.