SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-26

Как маршрутизировать 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-сети будут отбрасываться, пока вы не укажете эту подсеть:

FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24

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

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

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

Эта схема оправдывает свою сложность в случае сбоя. У подключенного контейнера нет второго маршрута. Его единственный путь за пределы машины — это пространство имен, которое он разделяет с VPN-клиентом, поэтому при падении туннеля переключаться не на что. Межсетевой экран Gluetun применяет то же правило с другой стороны: исходящий трафик идет либо через туннель, либо к конечной точке 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. При неудаче он перезапускает 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):

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

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

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

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

Проверка на утечку DNS перед тем, как доверять настройке

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

Настройка, которая нарушает это поведение — DNS_UPSTREAM_PLAIN_ADDRESSES. Пользователи включают её, когда имя не разрешается, и они хотят, чтобы вместо этого ответил их роутер или резолвер провайдера. Документация Gluetun прямо указывает на последствия: весь DNS-трафик не пойдет через VPN-туннель и будет утекать за его пределы. Ваш трафик останется приватным, но список имен хостов — нет. Аналогичная ошибка в версии WireGuard описана в DNS, который перестает разрешаться через туннель WireGuard.

Чтобы проверить это, установите HTTPPROXY=on в gluetun и опубликуйте 8888:8888/tcp, затем направьте браузер на этот прокси и запустите тест на утечку DNS. Результат должен указывать на вашего провайдера или Cloudflare, но ни в коем случае не на ваш домашний роутер. Собственная документация Gluetun предупреждает, что некоторые тесты на утечки могут выдавать странные результаты, так как резолвер внутри пространства имен является локальным кэширующим посредником, а не тем сервером, который дает окончательный ответ. Считайте неверную страну или резолвер вашего ISP реальным признаком проблемы.

Добавление Tailscale вместе с VPN-sidecar: кто побеждает

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

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

  • Tailscale в собственном контейнере, конфигурация по умолчанию: он не видит исходящий трафик приложения. Весь трафик проходит через gluetun. Tailscale обращается к приложению по gluetun:8080, точно так же, как любой другой внешний контейнер.
  • Tailscale подключен к пространству имен gluetun с помощью network_mode: "service:gluetun": ему требуются собственные cap_add из net_admin и net_raw, так как права доступа не наследуются вместе с пространством имен. В режиме сетевого взаимодействия в пространстве пользователя по умолчанию, TS_USERSPACE включен, tailscaled не создает сетевой интерфейс и работает как SOCKS5 или HTTP-прокси, поэтому он не может изменять маршрутизацию. Весь трафик по-прежнему проходит через gluetun.
  • То же самое, но с TS_USERSPACE=false: tailscaled создает туннельное устройство и добавляет маршруты, но только для диапазона tailnet 100.64.0.0/10 и любых подсетей, которые вы анонсируете через TS_ROUTES. Публичный трафик по-прежнему уходит через gluetun.
  • Любой из вышеперечисленных вариантов с выбранным exit node, sudo tailscale set --exit-node=<exit-node-ip>: Tailscale захватывает маршрут по умолчанию и побеждает. Не используйте это вместе с gluetun. Маршрут по умолчанию может иметь только одного владельца.

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

Если Tailscale нужен для доступа к административному URL, а не для маршрутизации, tailscale serve и tailscale funnel позволяют настроить HTTPS перед gluetun:8080 для вашей tailnet, при этом только функция funnel открывает доступ из публичного интернета.

Один побочный эффект заметен, когда Tailscale работает внутри туннеля. Его пиры видят адрес VPN-провайдера, поэтому ожидайте более частого использования ретрансляторов (relays). tailscale status показывает relay "..." рядом с пиром вместо direct, когда это происходит. Соединение работает, но оно медленнее. Если оверлейная сеть — это единственное, что вам действительно нужно, разница между обычным 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. Проверьте, не истёк ли срок действия ключа, не устарел ли список серверов, а также не блокирует ли ваш хостовый фаервол исходящий 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 v3.41 и новее. Трафик от клиента из другой подсети, например ноутбука в вашей локальной сети, будет отброшен межсетевым экраном gluetun, пока вы не добавите эту подсеть в FIREWALL_OUTBOUND_SUBNETS.

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

Да, и по двум причинам одновременно. У присоединенного контейнера нет маршрутов, кроме тех, что находятся в общем пространстве имен, поэтому при падении туннеля у него не остается пути для выхода с машины. Межсетевой экран Gluetun также разрешает исходящий трафик только через туннель и к конечной точке 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 маршрутом по умолчанию, и тогда он берет управление на себя. Выберите один продукт для управления маршрутом по умолчанию, вместо того чтобы объединять оба.