Монтирование тома Docker не удаётся после подключения VPN: проверьте демон
Ваше bind-монтирование в порядке. VPN меняет, какой демон отвечает клиенту, поэтому выведите endpoint, докажите путь там и перестаньте править том.
Почти каждый текст об этой проблеме винит том, и именно поэтому исправление занимает так много времени. У вас было работающее bind-монтирование, корпоративный VPN подключился, и docker compose up теперь оно падает из-за отсутствующего пути к источнику, пустого каталога или монтирования, которое движок отказывается создавать. В средах, которые я приводил в порядок, файл Compose не менялся, а путь был верным. Изменилось то, какой демон получил запрос, и ошибка монтирования была лишь первым местом, где эта разница стала видна.
Том никуда не переезжал; переехал запрос
Флаг -v /srv/app:/app не является копированием каталога, которое выполняет ваша оболочка. Клиент превращает его в запрос API, а демон, который отвечает, разрешает /srv/app относительно своей собственной файловой системы. Когда обе стороны работают на одном ядре, этот путь — тот, который вы видите в файловом менеджере. Когда endpoint меняется, та же строка ищется на машине, которая никогда не слышала о каталоге вашего проекта, и демон либо сообщает, что источник отсутствует, либо тихо создаёт на его месте пустой каталог.
Вот почему именованный том продолжает работать, а bind-монтирование — нет. -v appdata:/app называет хранилище, которым владеет демон, поэтому никакой путь хоста не обязан существовать в определённом месте. Если замена одного сервиса с bind-монтирования на именованный том заставляет ошибку исчезнуть, вы не исправили проблему. Вы подтвердили, что клиент и демон расходятся в том, какой файловой системе принадлежит путь, и это расхождение возвращается в тот момент, когда вам снова нужен настоящий каталог хоста.
Выведите endpoint, прежде чем трогать монтирование
Несколько настроек могут направить клиент на endpoint, и они не всегда согласны: флаг --context , DOCKER_CONTEXT, DOCKER_HOST в текущей оболочке, и контекст, сохранённый в конфигурации клиента. Вместо рассуждений о приоритете выведите все и сравните ответы.
docker context show
docker context ls
docker version
docker info --format "{{.Name}} {{.OperatingSystem}} {{.ServerVersion}}"
Конфигурация клиента находится в ~/.docker/config.json на Linux и macOS и в %USERPROFILE%\.docker\config.json на Windows, а значение, экспортированное в текущем терминале, переопределяет сохранённое только для этой сессии. Если endpoint — это имя хоста, а не локальный сокет, разрешение имён и маршруты решают, куда он попадёт, и именно их переписывает клиент VPN. Туннель, который добавляет маршрут или подменяет резолвер, может отправить имя, которое раньше означало этот ноутбук, на адрес внутри корпоративной сети.
Сравнивайте состояние, а не конфигурацию. Запустите docker ps -a и docker images с выключенным туннелем, а затем снова с включённым. Другой список контейнеров — более сильное доказательство, чем любой файл конфигурации, потому что он показывает, что на ваш запрос ответил другой демон.
Спросите демон, существует ли путь там
Когда вы знаете, до какого демона добрались, заставьте его посмотреть на каталог. --mount форма падает с более ясным сообщением, чем короткий флаг, потому что проверяет источник до запуска контейнера:
docker run --rm --mount type=bind,source=/srv/app,target=/app alpine ls -la /app
Если он сообщает, что источник отсутствует, создайте его на машине, которая ответила, или направьте сервис на именованный том и скопируйте туда данные. Чего делать не стоит — это пересоздавать тома и переустанавливать движок до этой проверки. Такие действия достаточно меняют окружение, чтобы скрыть несоответствие, но оставляют его на месте.
Ещё один путь проходит совсем в другом месте: загрузка образа — работа демона, а не клиента. Успешная загрузка доказывает лишь то, что демон может добраться до registry, а работающее bind-монтирование — лишь то, что путь разрешился. Проверяйте каждый путь там, где он действительно выполняется, иначе потратите полдня не на то.
Имена интранета не работают по другой причине
Вторая половина того же отчёта обычно состоит в том, что контейнер не может достучаться до intranet.corp.example. Это отдельный механизм, а не вторая проблема с томом. Контейнер получает свой /etc/resolv.conf при запуске, сгенерированный из настроек резолвера демона, поэтому DNS-сервер, который VPN добавил после создания контейнера, не появится там, пока контейнер не будет пересоздан.
Встроенный резолвер Docker в пользовательской сети пересылает внешние запросы на настроенные серверы хоста, что помогает, когда VPN переписывает резолвер хоста, и вредит, когда разделённый DNS отправляет внутреннюю зону на сервер, который демон больше не видит. Узкое, явное сопоставление обходит спор для имён, которые никогда не меняются:
services:
web:
extra_hosts:
- "intranet.corp.example:10.20.30.40"
Это записывает запись в файл hosts контейнера, поэтому контейнер ничего не узнаёт о туннеле, а также замораживает адрес. Когда внутренний хост переезжает, запись остаётся неверной, пока кто-нибудь не отредактирует файл Compose. Используйте это для фиксированного устройства или закреплённой базы данных, а имена, которые перемещаются, оставьте DNS.
Что на самом деле меняет переустановка
Переустановка Docker Desktop пересоздаёт VM, сокет и список контекстов, и монтирование снова начинает работать. Этот результат реален и ничему не учит, поэтому та же поломка возвращается после следующего обновления VPN. Переустановка заменила endpoint, который использовал клиент; она не согласовала путь, который называет ваш проект, с файловой системой, которую демон действительно читает.
Мой порядок работы короток. Подтвердите, какой endpoint будет использовать клиент. Подтвердите, какой демон ответил, сравнив списки контейнеров. Попросите этот демон перечислить путь к источнику. Только после этого правьте монтирование, добавляйте запись hosts или меняйте тип тома. Если вы поддерживаете файл Compose для команды, которая работает через VPN, предпочитайте именованные тома для данных, которыми должен владеть демон, чтобы путь хоста никогда не стал частью договора.