Как направить Docker-контейнеры через VPN
Порты контейнера исчезают при network_mode: service:gluetun. Разберите общую сетевую область имён и рабочий compose-файл для Gluetun v3.41.3.
Почему порты исчезают при маршрутизации Docker-контейнеров через VPN
Чтобы маршрутизировать Docker-контейнеры через VPN, подключите туннель к одному контейнеру, а остальные присоедините к его сетевому пространству имён с помощью network_mode: "service:gluetun". Именно это присоединение часто вызывает вопросы. У присоединённого контейнера больше нет собственной сети, поэтому вместе с ней исчезают опубликованные порты и имя Docker-сервиса. Публикуйте порты на VPN-контейнере. Остальные контейнеры смогут обращаться к приложению по имени VPN-контейнера.
Если оставить блок ports: в конфигурации присоединённого контейнера, Docker вообще откажется его создавать:
Error response from daemon: conflicting options: port publishing and the container type network modeЗдесь используется Gluetun — контейнер, который подключается к коммерческому провайдеру VPN (виртуальной частной сети) по WireGuard или OpenVPN и использует собственный firewall. По состоянию на August 2026 актуальна версия v3.41.3. В примерах используется Mullvad с WireGuard, поэтому вам потребуются учётная запись и ключ от провайдера. Если вы предпочитаете завершать туннель на собственном оборудовании, развёртывание собственного сервера WireGuard на VPS создаёт другую сторону соединения, а wg-easy в Docker предоставляет для этого web-интерфейс.
Что на самом деле делает network_mode: "service:gluetun"
Обычно каждый контейнер Docker получает собственное сетевое пространство имён: собственные интерфейсы, таблицу маршрутизации, правила межсетевого экрана и прослушиваемые сокеты. Режим service: пропускает этот шаг и запускает контейнер в пространстве имён gluetun. Одно пространство имён означает один IP-адрес, и это меняет шесть аспектов.
- У приложения нет собственного адреса. Оно использует адрес gluetun.
- Приложение не подключено ни к одной сети Docker, поэтому его имя сервиса не регистрируется и не разрешается. Другие контейнеры должны использовать
gluetun. - Контейнеры внутри пространства имён взаимодействуют друг с другом через
localhost. - Два контейнера в одном пространстве имён не могут прослушивать один и тот же порт. В документации Gluetun это указано прямо: обходного решения нет.
- Возможности относятся к контейнеру, а не к пространству имён. Gluetun получает
NET_ADMINи/dev/net/tun, поскольку создаёт туннельный интерфейс. Подключённый контейнер их не наследует. - Compose отклоняет любой файл, в котором один сервис задаёт одновременно
network_modeиnetworks. Подключите gluetun к своим сетям, и приложение будет использовать это подключение.
Перезапуск gluetun отключает всё, что к нему подключено. Это задокументированное поведение. Поэтому gluetun перезапускает процесс VPN внутри контейнера, а не завершает работу при сбое соединения. После самостоятельного перезапуска или пересоздания gluetun перезапустите подключённые к нему контейнеры.
Рабочий compose-файл
services:
gluetun:
image: qmcgaw/gluetun:v3
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- SERVER_CITIES=Amsterdam
- TZ=Europe/Amsterdam
env_file:
- ./gluetun.env
volumes:
- ./gluetun:/gluetun
ports:
- 127.0.0.1:8080:8080/tcp
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- ./downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedТег :v3 — это последний стабильный релиз серии v3. Тег :latest указывает на последний коммит ветки master, то есть на нестабильную версию разработки. Поэтому закрепите :v3 на машине, которую не хотите отлаживать во вторник.
WEBUI_PORT=8080 должен совпадать с опубликованным портом, поскольку qBittorrent привязывается внутри пространства имён gluetun, а правило публикации направляет трафик с хоста на порт 8080 в этом пространстве имён. Если изменить только одно из этих чисел, порт не будет отвечать. 127.0.0.1:8080:8080 оставляет веб-интерфейс доступным только через loopback-адрес хоста. Запись 8080:8080 без IP-адреса публикует порт на всех интерфейсах и добавляет собственное правило firewall. Именно поэтому опубликованные Docker-порты обходят ufw.
Запустите стек, затем проверьте его в следующем порядке:
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30В выводе docker compose ps должен быть указан gluetun со статусом healthy, а qbittorrent — со статусом running. Затем проверьте внешний адрес из пространства имён, поскольку именно эта проверка определяет результат всех остальных:
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"Поле ip в этом JSON должно содержать адрес вашего VPN-провайдера. Если там указан адрес самого сервера, приложение не использует туннель, и приведённая ниже схема не будет работать.
Храните ключи отдельно от файла compose
gluetun.env хранит учётные данные и не добавляется в git:
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32Оба значения берутся из файла конфигурации WireGuard, который вы создаёте в личном кабинете провайдера. Установите для файла права 600. Учитывайте ограничение: ключ не попадёт в ваш репозиторий, но docker inspect gluetun по-прежнему выводит все переменные окружения для любого пользователя, имеющего доступ к Docker socket. В материале Файлы окружения и секреты в Docker Compose описаны более надёжные варианты.
Как контейнер вне туннеля взаимодействует с контейнером внутри него
Оба направления работают, и для каждого используется своё имя. Контейнерам нужна общая Docker network. Это network Gluetun, поскольку у подключённого контейнера нет собственной сети. В разделе Как устроены сети Docker Compose описаны значения по умолчанию.
Для подключения снаружи к контейнеру внутри используйте имя Gluetun и порт, на котором приложение принимает подключения. Контейнер reverse proxy обращается к веб-интерфейсу qBittorrent по адресу gluetun:8080. Запись ports: для этого не требуется, поскольку трафик между контейнерами остаётся в Docker network и не проходит через порт хоста.
Для подключения изнутри наружу используйте имя сервиса другого контейнера, например postgres:5432. Начиная с v3.41 Gluetun разрешает имена других контейнеров из своего namespace. Если имя не разрешается, закрепите v3.41 или более новую версию.
Firewall Gluetun определяет, кто может открывать подключения к нему. Трафик из собственной Docker network Gluetun разрешён. Подключение из другой подсети, с ноутбука в вашей LAN или из контейнера в отдельной bridge network блокируется, пока вы не укажете эту подсеть:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24Документированное значение параметра однозначно: это список подсетей через запятую, которым разрешено обращаться к Gluetun и контейнерам, использующим его сетевой стек.
Входящие подключения из Интернета — отдельная проблема. Пиры torrent-клиента подключаются со стороны VPN, поэтому публикация порта 6881 на хосте им не помогает. Вам нужен перенаправленный порт от провайдера. Этот порт нужно указать в FIREWALL_VPN_INPUT_PORTS: параметр разрешает порты со стороны VPN-сервера. Именно это чаще всего остаётся ненастроенным в медиастеках, созданных с помощью Docker Compose.
Аварийное отключение: что происходит при разрыве туннеля
Эта схема оправдывает свою сложность при сбоях. У подключённого контейнера нет второго маршрута. Единственный путь наружу проходит через общее с Gluetun сетевое пространство имён, поэтому при недоступном туннеле резервного маршрута нет. Межсетевой экран Gluetun обеспечивает то же правило с другой стороны: исходящий трафик проходит через туннель или к endpoint VPN-сервера, а весь остальной трафик отбрасывается. Нет периода, в течение которого пакеты могут выйти через обычный интерфейс, пока клиент повторно подключается.
Gluetun отслеживает собственное подключение. Каждую минуту он отправляет ICMP echo (ping) на адреса из HEALTH_ICMP_TARGET_IPS, значением по умолчанию для которых является 1.1.1.1,8.8.8.8. Каждые пять минут он устанавливает полноценное TCP- и TLS-соединение (transport layer security) с HEALTH_TARGET_ADDRESSES, значение по умолчанию — cloudflare.com:443,github.com:443. Если эти проверки завершаются ошибкой, Gluetun перезапускает VPN внутри контейнера и записывает событие в журнал:
WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeoutИзучайте журналы подключённого контейнера с учётом этого порядка. Строки вроде connection refused, operation not permitted и i/o timeout внутри приложения являются следствием неработающего туннеля, а не причиной. В документации Gluetun это указано прямо, поскольку администраторы сообщают о следствии и часами ищут несуществующую причину.
HEALTH_RESTART_VPN=on включён по умолчанию и должен оставаться включённым. Отключайте его только при диагностике конкретной неисправности, поскольку после отключения неработающий туннель останется неработающим.
Порядок запуска: не допускайте запуска стека до установления туннеля
В образе настроена проверка состояния Docker:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckЭта команда запускает отдельную краткоживущую копию gluetun. Она запрашивает health server работающего экземпляра по адресу http://127.0.0.1:9999/. Исправный туннель возвращает 200 OK. При неисправности он возвращает 500 Internal server error со строкой ошибки, после чего контейнер получает статус unhealthy уже после одной неудачной проверки.
Именно condition: service_healthy ожидает выполнения этого условия. Обычный depends_on: [gluetun] ожидает только запуска контейнера. Это происходит за несколько секунд до завершения handshake, поэтому приложение запускается при недоступной сети и часто прекращает работу после первой попытки подключения. В разделе Проверки состояния в Docker Compose описаны синтаксис и поля, задающие интервалы и задержки.
Есть одно ограничение, о котором часто забывают. Compose проверяет это условие один раз — при создании контейнера. Если gluetun позднее перейдёт в состояние unhealthy, Compose не остановит и не перезапустит приложение. В этом случае используется встроенное auto-healing gluetun. Поэтому перезапускается процесс VPN, а не контейнер.
Проверьте утечку DNS до того, как доверять настройке
DNS (система доменных имён) — это утечка, которая сохраняется даже при правильно работающем туннеле. Gluetun запускает собственный резолвер внутри namespace и по умолчанию пересылает запросы через DoT (DNS over TLS) в Cloudflare: DNS_UPSTREAM_RESOLVER_TYPE=dot и DNS_UPSTREAM_RESOLVERS=cloudflare. Не изменяйте эти параметры: тогда запросы DNS будут зашифрованы и переданы через туннель.
Эту схему нарушает параметр DNS_UPSTREAM_PLAIN_ADDRESSES. К нему обращаются, когда имя не удаётся разрешить и требуется, чтобы вместо этого ответил роутер или резолвер провайдера. В документации Gluetun прямо указано последствие: весь DNS-трафик не будет проходить через VPN-туннель и выйдет за его пределы. Ваш трафик останется приватным. Список запрашиваемых имён хостов — нет. Аналогичная ошибка в варианте с WireGuard описана в разделе DNS, который перестаёт разрешать имена через туннель WireGuard.
Для проверки задайте HTTPPROXY=on в gluetun и опубликуйте 8888:8888/tcp, затем укажите этот proxy в браузере и откройте тест утечки DNS. В результате должен быть указан ваш провайдер или Cloudflare, но не домашний роутер. В собственной документации Gluetun предупреждает, что некоторые тесты утечки могут показывать нетипичные результаты: резолвер внутри namespace является локальным кэширующим посредником, а не сервером, который в итоге формирует ответ. Неверная страна или резолвер вашего ISP — это признаки реальной проблемы.
Добавление Tailscale рядом с VPN sidecar и определение приоритета маршрутизации
Tailscale — это overlay network на базе WireGuard для доступа к собственным машинам. Его часто запускают рядом с VPN провайдера, чтобы сохранить административный доступ к стеку. Эти компоненты редко конфликтуют. Причина указана в документации Tailscale: по умолчанию он работает как overlay network, маршрутизирует трафик только между устройствами с запущенным Tailscale и не затрагивает публичный интернет-трафик.
Ответ зависит от одной настройки.
- Tailscale в собственном контейнере с конфигурацией по умолчанию не видит исходящий трафик приложения. Весь этот трафик передаёт Gluetun. Tailscale обращается к приложению через
gluetun:8080, как и любой другой внешний контейнер. - Tailscale подключён к namespace gluetun с помощью
network_mode: "service:gluetun": ему требуются собственныеcap_addдляnet_adminиnet_raw, поскольку capabilities не передаются вместе с namespace. В режиме userspace networking по умолчанию включёнTS_USERSPACE. В этом режиме tailscaled не создаёт интерфейс и работает как SOCKS5- или HTTP-прокси, поэтому не может изменить маршрутизацию. Весь трафик по-прежнему передаёт Gluetun. - То же самое при использовании
TS_USERSPACE=false: tailscaled создаёт tunnel device и устанавливает маршруты, но только для диапазона tailnet100.64.0.0/10и любых subnet routes, объявленных с помощьюTS_ROUTES. Публичный трафик по-прежнему выходит через gluetun. - Любой из перечисленных вариантов с выбранным exit node,
sudo tailscale set --exit-node=<exit-node-ip>: Tailscale устанавливает маршрут по умолчанию и получает приоритет. Не объединяйте этот режим с gluetun. Маршрут по умолчанию должен иметь одного владельца.
Если Tailscale работает внутри туннеля, заметен побочный эффект. Его узлы видят адрес VPN-провайдера, поэтому соединение чаще переключается на relays. В tailscale status рядом с узлом отображается relay "..." вместо direct, если это произошло. Соединение продолжает работать, но становится медленнее. Если вам нужен только overlay, начните с различий между обычным WireGuard и Tailscale.
Что ломается и какое сообщение вы увидите
Docker отказывается создавать контейнер приложения. Error response from daemon: conflicting options: port publishing and the container type network mode означает, что блок ports: всё ещё назначен подключённому сервису. Перенесите его в gluetun.
Compose отклоняет весь файл. Сервис не может одновременно задавать network_mode и networks. Укажите сети для gluetun.
Другой контейнер не может разрешить имя приложения. Это штатное поведение для curl: (6) Could not resolve host: qbittorrent, поскольку подключённый контейнер не присоединился ни к одной сети и не зарегистрировал имя. Используйте gluetun и порт.
Второй подключённый контейнер не запускается. Два процесса в одном сетевом пространстве имён не могут занять один и тот же порт. Проигравший процесс сообщает, что адрес уже используется. Измените внутренний порт приложения или запустите второй gluetun.
После изменения gluetun у приложения нет сети. Перезапуск или пересоздание gluetun разрывает соединение у всех подключённых к нему контейнеров. Перезапустите эти контейнеры.
Небольшие страницы загружаются, а большие зависают. Причина — MTU (maximum transmission unit). Туннель добавляет служебные данные, и один из узлов на пути отбрасывает слишком большие пакеты, не отправляя сообщение об ошибке. Уменьшите WIREGUARD_MTU, попробуйте 1400, затем 1320.
Gluetun не переходит в состояние healthy. Проверка при запуске указывает на первые возможные причины: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. Проверьте, не истёк ли срок действия ключа, затем проверьте актуальность списка серверов и убедитесь, что firewall на хосте не блокирует исходящий UDP.
FAQ
Почему опубликованные порты контейнера перестали работать за Gluetun?
Потому что network_mode: "service:gluetun" помещает контейнер в сетевое пространство имён gluetun, а у пространства имён один IP-адрес и один набор прослушиваемых портов. Приложение продолжает принимать подключения, но правило публикации должно находиться у контейнера, которому принадлежит это пространство имён. Перенесите список ports: в сервис gluetun. Если оставить его у подключённого сервиса, Docker даже не создаст его: Error response from daemon: conflicting options: port publishing and the container type network mode.
Как обратиться к контейнеру внутри VPN-туннеля из контейнера вне этого туннеля?
Используйте имя сервиса gluetun и порт, на котором приложение принимает подключения, например gluetun:8080. Подключённый контейнер не подключён к собственной Docker network, поэтому его имя не разрешается. Для трафика между контейнерами публикация портов не требуется. В обратном направлении контейнер внутри пространства имён обращается к внешнему контейнеру по имени его сервиса, например postgres:5432, в Gluetun v3.41 и более новых версиях. Подключение из другой подсети, например с ноутбука в вашей LAN, gluetun блокирует своим firewall, пока вы не добавите эту подсеть в FIREWALL_OUTBOUND_SUBNETS.
Работает ли Gluetun как kill switch при обрыве VPN?
Да, и сразу по двум причинам. У подключённого контейнера нет маршрута, кроме маршрута в общем пространстве имён, поэтому при отказе туннеля у него не остаётся пути за пределы сервера. Firewall Gluetun также разрешает исходящий трафик только через туннель и к endpoint VPN-сервера. Затем Gluetun перезапускает VPN внутри себя и записывает в журнал WARN [vpn] restarting VPN because it failed to pass the healthcheck, а не завершает работу, поскольку при перезапуске самого gluetun все подключённые контейнеры теряют сеть.
Tailscale и Gluetun в одном стеке: какой из них передаёт исходящий трафик?
Gluetun — во всех конфигурациях, кроме одной. По умолчанию Tailscale маршрутизирует только трафик между устройствами в вашей tailnet и не изменяет маршрутизацию публичного трафика. В режиме userspace, используемом по умолчанию в образе контейнера, он вообще не создаёт интерфейс, поэтому не может влиять на маршрутизацию. При использовании TS_USERSPACE=false он устанавливает маршруты только для 100.64.0.0/10 и анонсируемых вами подсетей. Исключение — exit node: sudo tailscale set --exit-node=<exit-node-ip> делает Tailscale маршрутом по умолчанию, и тогда приоритет получает он. Выберите один продукт, который будет управлять маршрутом по умолчанию, вместо одновременного использования обоих.