VPN 连接后 Docker 卷挂载失败:先检查守护进程
你的 bind mount 没有问题。VPN 会改变由哪个守护进程响应客户端,所以请打印 endpoint、在那里验证路径,并停止改动卷。
几乎所有关于这个问题的文章都怪罪卷,而这恰恰是修复拖得这么久的原因。你原本有一个能用的 bind mount,公司 VPN 连上之后, docker compose up 就变成了因源路径缺失、目录为空或引擎拒绝创建挂载而失败。在我清理过的环境里,Compose 文件并没有改过,路径也是对的。真正变化的是哪个守护进程收到了请求,而挂载报错只是这个差异最先暴露出来的地方。
卷从来没有移动过;移动的是请求
一个 -v /srv/app:/app 参数并不是你的 shell 所做的目录复制。客户端把它变成一次 API 请求,而应答的守护进程会按照自己的文件系统去解析 /srv/app 。当两端运行在同一个内核上时,那个路径就是你在文件管理器里看到的路径。一旦 endpoint 变了,同一个字符串就会在一台从没听说过你项目目录的机器上被查找,守护进程要么报告源不存在,要么悄悄在原地建一个空目录。
这也是为什么命名卷继续可用,而 bind mount 不行。 -v appdata:/app 命名的是守护进程自己拥有的存储,因此不需要有任何宿主机路径存在于某个特定位置。如果把一个服务从 bind mount 换成命名卷就让报错消失了,你并没有修好问题。你只是确认了客户端和守护进程对这条路径属于哪个文件系统并不一致,而这种不一致会在你再次需要真实宿主机目录的那一刻回来。
动挂载之前先把 endpoint 打印出来
有好几处设置能把客户端指向某个 endpoint,而它们并不总是一致: --context 参数、 DOCKER_CONTEXT、 DOCKER_HOST 当前 shell 里的设置,以及客户端配置里保存的 context。与其去推理优先级,不如把它们全部打印出来对比答案。
docker context show
docker context ls
docker version
docker info --format "{{.Name}} {{.OperatingSystem}} {{.ServerVersion}}"
客户端配置位于 ~/.docker/config.json (Linux 和 macOS),以及 %USERPROFILE%\.docker\config.json (Windows),而在当前终端里 export 的值只对那个会话覆盖已保存的值。如果 endpoint 是主机名而不是本地 socket,那么名字解析和路由就决定了它落到哪里,而这正是 VPN 客户端会改写的东西。一条添加了路由或换掉解析器的隧道,可以把一个过去指这台笔记本的名字送到公司网络内部的地址上。
比较状态,而不是比较配置。在隧道断开时运行 docker ps -a 和 docker images ,然后在隧道连上时再运行一次。容器列表不同,比任何配置文件都更有说服力,因为它说明应答你请求的是另一个守护进程。
问守护进程那条路径在它那里是否存在
一旦知道连到了哪个守护进程,就让它去看那个目录。 --mount 这种写法给出的报错比短参数更清楚,因为它会在启动容器之前先校验源:
docker run --rm --mount type=bind,source=/srv/app,target=/app alpine ls -la /app
如果它报告源不存在,就在应答的那台机器上把它建出来,或者把服务指向命名卷并把数据拷进去。你不该做的是在这次检查之前重建卷、重装引擎。那些操作对环境的改动足以把不一致藏起来,却又把它留在原处。
还有一条路径完全跑在别处:拉取 image 是守护进程的活儿,不是客户端的活儿。拉取成功只能证明守护进程能连上 registry,bind mount 能用也只证明某条路径被解析了。要在每条路径真正执行的地方去测,否则你会把一下午花在错的那条上。
内网名字失败是另一套原因
同一份报告的后半段通常是容器访问不到 intranet.corp.example。那是另一套机制,不是第二个卷的问题。容器在启动时拿到自己的 /etc/resolv.conf ,它是根据守护进程的解析器设置生成的,所以在容器创建之后 VPN 才加上的 DNS 服务器不会出现在里面,除非容器被重建。
在用户自定义网络上,Docker 的内嵌解析器会把外部查询转发给宿主机上配置的服务器,这在 VPN 改写宿主机解析器时是好事,而在分离 DNS 把内部区域发给守护进程已经看不到的服务器时就是坏事。对于永远不变的名字,一条窄而明确的映射可以绕开这场争论:
services:
web:
extra_hosts:
- "intranet.corp.example:10.20.30.40"
这会往容器的 hosts 文件里写一条记录,因此容器对隧道一无所知,同时也把地址冻结了。当内部主机迁移时,这条记录会一直错到有人去改 Compose 文件。把它用在固定设备或钉死的数据库上,会移动的名字交给 DNS。
重装到底改变了什么
重装 Docker Desktop 会重建 VM、socket 和 context 列表,挂载又开始工作了。这个结果是真的,但它什么也没教会你,所以下一次 VPN 更新之后同样的故障还会回来。重装只是替换了客户端正在使用的 endpoint;它并没有把你项目命名的路径和守护进程真正读取的文件系统对上。
我的工作顺序很短。先确认客户端会用哪个 endpoint。再通过对比容器列表确认是哪个守护进程应答的。然后让那个守护进程列出源路径。只有到这时才去改挂载、加 hosts 记录或换卷类型。如果你要为通过 VPN 工作的团队维护 Compose 文件,优先给本该由守护进程拥有的数据用命名卷,这样宿主机路径就永远不会成为契约的一部分。