SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Как маршрутизировать Docker-контейнеры через VPN

При подключении контейнера к сети Gluetun опубликованные порты перестают работать из-за разделяемого пространства имен. Узнайте, как правильно настроить Docker Compose для VPN.

Почему порты исчезают при маршрутизации 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 (virtual private network) через WireGuard или OpenVPN и имеет собственный встроенный файрвол. Релиз v3.41.3 является актуальным по состоянию на октябрь 2026 года. В примерах используется Mullvad с протоколом WireGuard, поэтому вам потребуется учетная запись и ключ от вашего провайдера. Если вы предпочитаете терминировать туннель на собственном оборудовании, запуск собственного WireGuard-сервера на VPS позволит создать вторую сторону соединения, а wg-easy в Docker добавит к этому удобный веб-интерфейс.

Что на самом деле делает network_mode: "service:gluetun"

Каждый контейнер Docker обычно получает собственное сетевое пространство имен: свои интерфейсы, таблицу маршрутизации, правила межсетевого экрана и слушающие сокеты. Режим service: пропускает этот шаг и запускает контейнер внутри пространства имен gluetun. Одно пространство имен означает один IP-адрес, что меняет шесть аспектов.

  • У приложения нет собственного адреса. Его адрес — это адрес gluetun.
  • Приложение не подключено ни к одной сети Docker, поэтому его имя сервиса никогда не регистрируется и не разрешается. Другие контейнеры должны использовать gluetun.
  • Контейнеры внутри пространства имен взаимодействуют друг с другом через localhost.
  • Два контейнера в одном пространстве имен не могут слушать один и тот же порт. Документация Gluetun прямо говорит об этом: обходных путей не существует.
  • Возможности (capabilities) принадлежат контейнеру, а не пространству имен. 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 публикует сервис на всех интерфейсах и создает собственное правило файрвола, из-за чего опубликованные порты 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, а именно сеть gluetun, так как у присоединенного контейнера своей сети нет. В Как устроены сети Docker Compose описаны стандартные настройки.

Для доступа снаружи внутрь используйте имя gluetun и порт, на котором слушает приложение. Контейнер с reverse proxy обращается к веб-интерфейсу qBittorrent по адресу gluetun:8080. Запись ports: для этого не требуется, так как трафик между контейнерами остается внутри сети Docker и не затрагивает порты хоста.

Для доступа изнутри наружу используйте имя сервиса другого контейнера, например postgres:5432. Начиная с версии v3.41, gluetun разрешает имена других контейнеров из своего пространства имен, поэтому используйте эту или более новую версию, если имя не удается разрешить.

Межсетевой экран gluetun определяет, кто может устанавливать с ним соединение. Трафик из собственной сети Docker контейнера gluetun разрешен. Клиенту из другой подсети, ноутбуку в вашей локальной сети или контейнеру в отдельной bridge-сети требуется обратный путь, разрешенный межсетевым экраном и маршрутизацией gluetun:

FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24

Значение параметра в документации точно: это разделенные запятыми подсети, к которым разрешен доступ для gluetun и контейнеров, использующих его сетевой стек.

Входящие соединения из Интернета — это отдельная задача. Пиры торрент-клиента приходят со стороны VPN, поэтому публикация порта 6881 на хосте для них бесполезна. Вам нужен перенаправленный порт от вашего провайдера, и этот порт должен быть указан в FIREWALL_VPN_INPUT_PORTS, что разрешает порты со стороны VPN-сервера. Mullvad, используемый в этом примере, не поддерживает перенаправление портов, поэтому для этого требуется провайдер, предоставляющий такую возможность. Это тот элемент, который чаще всего остается нерабочим в медиа-стеках, развернутых с помощью Docker Compose.

Аварийный выключатель: что происходит при разрыве туннеля

Эта схема оправдывает свою сложность в случае сбоя. Подключенный контейнер использует сетевые интерфейсы и таблицу маршрутизации Gluetun. Межсетевой экран Gluetun блокирует обычный интернет-трафик вне туннеля, разрешая при этом трафик до конечной точки VPN-сервера, локальный трафик Docker и настроенные исключения. Именно этот межсетевой экран предотвращает переключение на обычный интерфейс при падении туннеля. Не существует временного окна, в котором пакеты могли бы уйти через обычный интерфейс во время переподключения клиента.

Gluetun отслеживает собственное соединение. Каждую минуту он отправляет ICMP echo (ping) на адреса в HEALTH_ICMP_TARGET_IPS, которые по умолчанию установлены в 1.1.1.1,8.8.8.8. Если ICMP запрещен, эта проверка переключается на обычный UDP DNS-запрос. Каждые пять минут он выполняет полное TCP и TLS (transport layer security) соединение с HEALTH_TARGET_ADDRESSES, по умолчанию cloudflare.com:443,github.com:443. При неудаче он перезапускает 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 включен по умолчанию, и его следует оставить в этом состоянии. Отключайте его только во время отладки конкретного сбоя, так как без него неработающий туннель останется в нерабочем состоянии.

Порядок запуска: предотвращение старта стека до поднятия туннеля

Образ поставляется с healthcheck для Docker:

HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck

Эта команда запускает вторую, кратковременную копию gluetun, которая опрашивает сервер проверки состояния запущенного экземпляра по адресу http://127.0.0.1:9999/. Работающий туннель отвечает 200 OK. Неисправный туннель отвечает 500 Internal server error со строкой ошибки, и Docker помечает контейнер как нездоровый после трех последовательных неудачных проверок.

condition: service_healthy — это то, что ожидает завершения данной проверки. Обычный depends_on: [gluetun] ожидает только запуска контейнера, который происходит за несколько секунд до завершения рукопожатия, поэтому приложение запускается в нерабочей сети и часто прекращает попытки соединения после первой же ошибки. В Healthcheck в Docker Compose подробно описаны синтаксис и поля настройки времени.

Одно ограничение часто вызывает трудности. Compose оценивает это условие в момент запуска стека. Он не останавливает и не перезапускает приложение позже, если состояние gluetun становится нездоровым. Вместо этого данный случай покрывает встроенная функция автоматического восстановления gluetun, поэтому она перезапускает процесс VPN, а не сам контейнер.

Проверьте утечку DNS, прежде чем доверять конфигурации

DNS (система доменных имён) — это утечка, которая сохраняется даже при правильно настроенном туннеле. Gluetun запускает собственный резолвер внутри namespace и по умолчанию пересылает запросы в Cloudflare по DoT (DNS over TLS): DNS_UPSTREAM_RESOLVER_TYPE=dot и DNS_UPSTREAM_RESOLVERS=cloudflare. Не изменяйте эти параметры. Тогда запросы DNS будут зашифрованы и пройдут через туннель.

Эта схема нарушается при указании локального резолвера в DNS_UPSTREAM_PLAIN_ADDRESSES. К этому параметру обращаются, когда имя не разрешается и требуется, чтобы ответил маршрутизатор или резолвер провайдера. Документация Gluetun предупреждает: запросы к резолверу в Docker bridge или локальной сети выходят за пределы VPN-туннеля. Общедоступный резолвер без шифрования всё равно получает запросы через туннель, но VPN-провайдер может читать и изменять их. Аналогичная ошибка в варианте с WireGuard описана в разделе DNS, который перестаёт разрешаться через туннель WireGuard.

Для проверки задайте SHADOWSOCKS=on и SHADOWSOCKS_PASSWORD в gluetun, опубликуйте 8388:8388/tcp и 8388:8388/udp, затем подключитесь с помощью клиента Shadowsocks, настроенного для передачи DNS по UDP через туннель, и откройте тест на утечку DNS. В результате должен быть указан ваш провайдер или Cloudflare, но не ваш домашний маршрутизатор. Собственная документация Gluetun предупреждает: некоторые тесты на утечку показывают необычные результаты, поскольку резолвер внутри namespace является локальным промежуточным кэширующим резолвером, а не сервером, который в итоге формирует ответ. Реальным признаком утечки считайте неправильную страну или резолвер вашего интернет-провайдера.

Добавление Tailscale рядом с VPN sidecar и определение приоритета

Tailscale — это overlay network на основе WireGuard для доступа к собственным машинам. Его часто запускают рядом с VPN провайдера, чтобы сохранить административный доступ к стеку. Общий namespace также означает общие правила firewall, поэтому настройки firewall и маршрутизации должны быть совместимыми. В документации Tailscale указано поведение по умолчанию: сервис работает как overlay network, маршрутизирует трафик только между устройствами с Tailscale и не изменяет трафик в публичный Интернет.

Ответ зависит от одной настройки.

  • Tailscale в отдельном контейнере с конфигурацией по умолчанию: он не видит исходящий трафик приложения. Весь этот трафик передаёт Gluetun. Процесс в контейнере Tailscale обращается к приложению по адресу gluetun:8080 через общую Docker network. Чтобы сделать это соединение доступным клиентам tailnet, нужна переадресация или proxy.
  • Tailscale подключён к namespace gluetun с помощью network_mode: "service:gluetun": в режиме userspace networking по умолчанию параметр TS_USERSPACE включён, tailscaled вообще не создаёт интерфейс и работает как SOCKS5- или HTTP-proxy, поэтому не может изменять маршрутизацию. Gluetun по-прежнему передаёт обычный исходящий трафик, если приложение явно не использует этот proxy.
  • То же подключение с TS_USERSPACE=false: Tailscale нужны собственное устройство /dev/net/tun и соответствующие capabilities, поскольку из namespace они не предоставляются. Tailscaled создаёт tunnel device и в Linux обычно устанавливает отдельные IPv4-маршруты /32 к узлам, а не один маршрут для 100.64.0.0/10. Одной настройки TS_USERSPACE=false недостаточно для включения агрегированного маршрута. Маршруты подсетей других узлов устанавливаются только при передаче --accept-routes через TS_EXTRA_ARGS. TS_ROUTES работает в обратном направлении: он объявляет остальным узлам tailnet подсети, доступные этому контейнеру, что эквивалентно tailscale set --advertise-routes=, и не добавляет маршруты в этом контейнере. Публичный трафик по-прежнему выходит через gluetun.
  • Kernel networking в namespace gluetun с выбранным exit node, sudo tailscale set --exit-node=<exit-node-ip>: Tailscale устанавливает default routes в этом namespace, но firewall Gluetun может заблокировать такой путь. Выбор exit node в отдельном контейнере не изменяет маршруты приложения, а userspace mode передаёт только трафик, явно направленный через его proxy. Не объединяйте конкурирующие default routes в namespace gluetun. Один default route — один владелец.

Если нужны именно объявленные маршруты и вы хотите обеспечить доступ ко всей частной сети за этим хостом, а не только к самому хосту, ознакомьтесь с материалом запуск subnet router Tailscale на VPS. В нём описаны подтверждение маршрута, IP forwarding и флаг на стороне клиента, которые одна настройка TS_ROUTES не выполняет.

Если Tailscale нужен для административного URL, а не для маршрута, tailscale serve и tailscale funnel размещают HTTPS перед локальным HTTP-сервисом. Только funnel открывает этот сервис для публичного Интернета. В Tailscale v1.104.5 их HTTP reverse proxy напрямую принимают http://gluetun:8080, если контейнер Tailscale может обратиться к нему через общую Docker network. Для target, отличного от localhost, укажите схему http://. Также работает общий network namespace gluetun с target http://127.0.0.1:8080.

Один побочный эффект заметен, когда Tailscale работает внутри tunnel. Его peers видят адрес VPN-провайдера, поэтому relays будут использоваться чаще. tailscale status показывает relay "..." рядом с peer вместо 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. Значение по умолчанию в v3.41.3 — 1320. Уменьшите его, если путь требует меньшего размера пакетов. При unset этого параметра development image использует path MTU discovery.

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" помещает контейнер в сетевое пространство имен (network namespace) Gluetun, а у пространства имен есть только один IP-адрес и один набор прослушиваемых портов. Приложение продолжает работать, но правило публикации должно находиться в контейнере, которому принадлежит это пространство имен. Перенесите список ports: в сервис gluetun. Если оставить его в подключенном сервисе, Docker даже не создаст его: Error response from daemon: conflicting options: port publishing and the container type network mode.

Как обратиться к контейнеру внутри VPN-туннеля из контейнера снаружи?

Используйте имя сервиса gluetun и порт, который прослушивает приложение, например gluetun:8080. Подключенный контейнер не находится ни в одной собственной сети Docker, поэтому его имя не разрешается. Для взаимодействия между контейнерами публикация портов не требуется. В обратном направлении контейнер внутри пространства имен обращается к внешнему контейнеру по его имени сервиса, например postgres:5432 (в Gluetun версии 3.41 и новее). Клиенту из другой подсети, например ноутбуку в вашей локальной сети, требуется обратный маршрут, разрешенный межсетевым экраном и таблицей маршрутизации gluetun; добавьте эту подсеть в FIREWALL_OUTBOUND_SUBNETS.

Работает ли Gluetun как kill switch при разрыве VPN-соединения?

Да, если включен его межсетевой экран. Подключенный контейнер использует интерфейсы и маршруты Gluetun, а межсетевой экран блокирует обычный интернет-трафик вне туннеля, разрешая при этом трафик до конечной точки VPN-сервера, локальный трафик Docker и настроенные исключения. Gluetun перезапускает VPN внутри себя, записывая в лог WARN [vpn] restarting VPN because it failed to pass the healthcheck, вместо завершения работы, так как при перезапуске самого gluetun все подключенные контейнеры теряют сеть.

Tailscale и Gluetun в одном стеке: какой из них передает исходящий трафик?

Gluetun передает обычный исходящий трафик, если вы явно не направите его через Tailscale. По умолчанию Tailscale маршрутизирует трафик только между устройствами в вашей tailnet и не затрагивает публичный трафик. В режиме userspace, используемом по умолчанию в образе контейнера, он вообще не создает интерфейс, поэтому не может влиять на маршрутизацию. При использовании TS_USERSPACE=false Linux обычно получает индивидуальные маршруты IPv4 peer /32 вместо агрегированного маршрута 100.64.0.0/10, а также маршруты подсетей других узлов, только если вы добавите --accept-routes через TS_EXTRA_ARGS. TS_ROUTES анонсирует подсети этого контейнера в вашу tailnet и не добавляет локальных маршрутов. Выбор exit node с помощью sudo tailscale set --exit-node=<exit-node-ip> устанавливает маршруты по умолчанию только в пространстве имен, использующем сетевой стек ядра Tailscale; отдельный контейнер не меняет маршруты приложения, а режим userspace передает только тот трафик, который направлен через его прокси. Выберите один продукт для управления маршрутом по умолчанию, вместо того чтобы объединять оба.

Была ли эта статья полезной?