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

Как настроить сеть для контейнеров внутри Gluetun

Контейнер за Gluetun не имеет своих сетевых интерфейсов. Узнайте, как правильно публиковать порты через Gluetun и ограничивать доступ к подсетям для повышения безопасности.

Что происходит, когда контейнер подключается к сети gluetun

Контейнер, для которого задан параметр network_mode: service:gluetun, не имеет собственных сетевых интерфейсов. Он присоединяется к сетевому пространству имен (network namespace) gluetun, поэтому публикация портов и правила межсетевого экрана перестают быть свойствами этого контейнера и становятся свойствами сервиса gluetun. Все ответы ниже вытекают из этого факта.

Сетевое пространство имен — это изолированная копия сетевого стека в ядре: собственные интерфейсы, собственная таблица маршрутизации, собственные правила межсетевого экрана и собственные слушающие сокеты. Docker по умолчанию создает такое пространство для каждого контейнера. Когда вы указываете network_mode: service:gluetun, Docker пропускает этот шаг и помещает новый контейнер в пространство имен, которое уже принадлежит gluetun. Контейнер сохраняет свою файловую систему и свой файл /etc/hosts, и второй из них будет важен позже.

Вы можете увидеть это напрямую.

docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrent

Эта команда выводит container:, за которым следует ID контейнера gluetun, в то время как обычный контейнер вывел бы bridge. Это руководство продолжает тему, на которой остановилось описание маршрутизации трафика Docker через VPN с помощью gluetun: туннель работает, и теперь ничто не может связаться с контейнером.

Публикация порта в gluetun, а не в приложении

Оставьте блок ports: в сервисе, который задает network_mode, и Docker откажется создавать контейнер:

Error response from daemon: conflicting options: port publishing and the container type network mode

Причина проста. Публикация порта означает добавление правила NAT (network address translation), которое перенаправляет порт хоста в сетевое пространство имен контейнера, а у данного контейнера его нет. Перенесите маппинг в сервис gluetun. Номер порта не меняется, так как приложение по-прежнему слушает этот порт внутри общего пространства имен.

services:
  gluetun:
    ports:
      - "8080:8080/tcp"   # qBittorrent web UI

  qbittorrent:
    network_mode: "service:gluetun"
    # no ports: block here

Блок expose: в зависимом сервисе так же бесполезен, а блок networks: там является критической ошибкой: Compose сообщает, что сервис объявляет взаимоисключающие network_mode и networks, и полностью отказывается загружать файл.

Одно последствие проявляется позже. Каждый контейнер в пространстве имен использует общее адресное пространство портов, поэтому два приложения, которые по умолчанию используют 8080, конфликтуют, и второе запущенное приложение завершается с ошибкой address already in use. Измените порт одного из них в его собственной конфигурации, например, через переменную WEBUI_PORT в образе LinuxServer qBittorrent, а затем опубликуйте новый номер в gluetun.

Как контейнеры за gluetun взаимодействуют друг с другом?

Внутри пространства имён они уже используют общий loopback-интерфейс. Контейнер за gluetun обращается к соседнему контейнеру по адресу 127.0.0.1:<port> без участия сети Docker.

Извне пространства имён у контейнера нет имени. Встроенный DNS-сервер Docker разрешает имя сервиса в адрес этого сервиса в пользовательской сети, а у данного контейнера нет адреса ни в одной сети. Поэтому обычный контейнер, например Sonarr, не может обратиться к торрент-клиенту по адресу http://qbittorrent:8080. Он обращается к нему по адресу http://gluetun:8080, так как сокет прослушивается в пространстве имён gluetun, на адресе gluetun. Это удивляет тех, кто знает как работают сети и имена сервисов в Docker Compose и ожидает применения стандартных правил именования. Это также работает без публикации портов на хост, так как оба контейнера находятся в одной сети Compose.

Перед началом отладки проверьте DNS. Gluetun запускает собственный резолвер и перезаписывает /etc/resolv.conf в своём контейнере, но /etc/resolv.conf — это файл для каждого отдельного контейнера, поэтому файл, записанный gluetun, не является тем, который считывает ваше приложение.

docker exec qbittorrent cat /etc/resolv.conf

Как получить доступ к сервису, работающему на хосте Docker?

Используйте host.docker.internal. Для этого требуется изменить две настройки в двух разных местах, так как нарушены два разных механизма.

Сначала имя. /etc/hosts настраивается для каждого контейнера отдельно, поэтому запись extra_hosts должна находиться в конфигурации контейнера с приложением, а не в gluetun.

  prowlarr:
    network_mode: "service:gluetun"
    extra_hosts:
      - "host.docker.internal:host-gateway"

host-gateway — это специальное значение, которое Docker заменяет на внутренний IP-адрес самого хоста. В стандартной установке Docker на Linux это адрес моста docker0, обычно 172.17.0.1. Уточните свой адрес с помощью ip -4 addr show docker0 на VPS. Docker Desktop разрешает это имя самостоятельно, поэтому в руководствах, написанных для ноутбуков, часто пропускают строку extra_hosts, из-за чего тот же файл конфигурации не работает на сервере.

Затем маршрут. Добавление имени лишь указывает контейнеру, какой адрес использовать. Пакет всё равно уходит через маршрут по умолчанию в gluetun, то есть в туннель, где его отбрасывает межсетевой экран gluetun. Симптомом является соединение, которое зависает и завершается по тайм-ауту, а не сбрасывается. Сброс соединения означает, что пакет дошёл и получил отказ. Тайм-аут означает, что пакет не дошёл вовсе.

  gluetun:
    environment:
      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32

Затем убедитесь, что сервис на хосте действительно слушает этот адрес. PostgreSQL, привязанный только к 127.0.0.1, будет недоступен из любого контейнера, независимо от туннеля, так как 127.0.0.1 внутри пространства имён — это loopback самого контейнера. Вместо этого привяжите сервис к 172.17.0.1: он принимает соединения от контейнеров, оставаясь при этом недоступным из публичной сети. Проверьте результат с помощью ss -lntp | grep 5432 на хосте.

На что на самом деле влияет FIREWALL_OUTBOUND_SUBNETS

В документации gluetun этот параметр описывается как список подсетей, разделенных запятыми, к которым разрешен доступ для gluetun и контейнеров, использующих его сетевой стек. Также отмечается, что это влечет за собой изменения в настройках межсетевого экрана и маршрутизации. Важны обе составляющие. Gluetun добавляет маршрут для каждой указанной подсети через шлюз Docker bridge, поэтому пакеты для этих адресов уходят через eth0, а не через туннель. Кроме того, для них открывается межсетевой экран, так как в противном случае gluetun отбрасывает исходящий трафик, который не направлен на VPN-сервер.

Указывайте значение без пробелов после запятых.

      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32

Два свойства легко упустить из виду. Это настройка уровня пространства имен (namespace), поэтому она применяется к каждому контейнеру за gluetun, а не только к тому, который вы имели в виду. Кроме того, это касается только исходящего трафика: параметр управляет соединениями, которые инициирует контейнер. Соединения, поступающие на опубликованный порт, проходят по другому пути и не требуют внесения в этот список.

Доступ к веб-интерфейсу с узла Tailscale

Tailscale назначает каждой машине адрес из диапазона 100.64.0.0/10, зарезервированного для carrier-grade NAT. В личном tailnet это не требует оплаты. Однако ограничения бесплатного плана по пользователям и устройствам определяют, сохранится ли это условие, когда к тем же интерфейсам потребуется доступ другим людям. Поскольку оплата рассчитывается по числу пользователей, а не машин, реальная стоимость платного tailnet зависит от количества приглашённых пользователей, а не от числа контейнеров, к которым вы предоставляете доступ. Эти два направления требуют разной настройки.

Входящие соединения настроить проще. Публикация 8080:8080 в gluetun привязывает этот порт ко всем адресам хоста, и интерфейс tailscale0 является одним из них, поэтому узел открывает http://<machine-name>:8080 и получает доступ к контейнеру. Gluetun не участвует в этом пути, так как правило NAT в Docker находится на хосте, вне пространства имен.

Чтобы сделать веб-интерфейс доступным только через tailnet, привяжите опубликованный порт к адресу Tailscale на хосте, а не ко всем адресам сразу.

    ports:
      - "100.101.102.103:8080:8080/tcp"

Узнайте этот адрес с помощью tailscale ip -4 на хосте. Привязка здесь является более строгим методом контроля, чем правило брандмауэра, так как порт вообще не открывается на публичном интерфейсе. Если вы предпочитаете обращаться к интерфейсу по HTTPS-имени, а не по хосту и порту, tailscale serve может выступать фронтендом для этого опубликованного порта, хотя serve и funnel различаются тем, кто в итоге получает доступ, и только один из них оставляет интерфейс внутри tailnet. Это также позволяет обойти проблему, описанную в публикации портов Docker в обход ufw.

Исходящие соединения — это область, где возвращается FIREWALL_OUTBOUND_SUBNETS. Если контейнеру нужно обратиться к узлу, добавьте адрес этого узла и отдайте предпочтение /32 для каждого узла, а не всему диапазону /10. Если машина, к которой вы обращаетесь, находится в частной сети, доступной через VPS, анонсирующий эту подсеть в вашу tailnet, укажите анонсированный диапазон вместо собственного адреса 100.x маршрутизатора и убедитесь, что сам хост принял эти маршруты. Имена MagicDNS не будут разрешаться внутри контейнера, так как контейнер не использует резолвер хоста, поэтому используйте числовой адрес 100.x или закрепите его с помощью строки extra_hosts. То же самое относится к случаям, когда вы используете собственный сервер управления Tailscale на базе Headscale.

Полный файл compose для типичной конфигурации

Клиент для загрузки за VPN, два веб-интерфейса, доступных только в tailnet, и один контейнер, который считывает данные из базы данных PostgreSQL, запущенной на хосте.

services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      - "127.0.0.1:8000:8000/tcp"        # gluetun control server, host only
      - "100.101.102.103:8080:8080/tcp"  # qBittorrent UI, tailnet only
      - "100.101.102.103:9696:9696/tcp"  # Prowlarr UI, tailnet only
    volumes:
      - ./gluetun:/gluetun
    environment:
      - VPN_SERVICE_PROVIDER=mullvad
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
      - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
      - SERVER_CITIES=Amsterdam
      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
      - TZ=Europe/Amsterdam
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent:/config
      - /srv/downloads:/downloads
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    network_mode: "service:gluetun"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - PROWLARR__POSTGRES__HOST=host.docker.internal
      - PROWLARR__POSTGRES__PORT=5432
      - PROWLARR__POSTGRES__USER=prowlarr
      - PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
      - PROWLARR__POSTGRES__MAINDB=prowlarr-main
      - PROWLARR__POSTGRES__LOGDB=prowlarr-log
    volumes:
      - ./prowlarr:/config
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

Изучите этот файл, чтобы понять структуру, а не конкретные продукты. Оба веб-интерфейса опубликованы через gluetun и привязаны к адресу tailnet хоста, поэтому они отвечают только через Tailscale и нигде больше. Только Prowlarr содержит строку extra_hosts, так как именно этот контейнер разрешает host.docker.internal. FIREWALL_OUTBOUND_SUBNETS указывает два отдельных адреса: адрес Docker bridge хоста, чтобы Prowlarr мог установить соединение с базой данных, и адрес одного узла tailnet.

Сервер PostgreSQL намеренно отсутствует в этом файле. Он работает на VPS как обычный системный сервис, слушающий 172.17.0.1:5432. Это та же схема уровней, что и в стеке arr на Docker Compose, но с базой данных, вынесенной за пределы Docker.

Не храните закрытый ключ WireGuard в файле compose. ${WIREGUARD_PRIVATE_KEY} считывает его из файла .env, расположенного рядом, — этот шаблон описан в файлы env и секреты для Docker Compose. В блоке condition: service_healthy используется проверка работоспособности (healthcheck), которую предоставляет образ gluetun, поэтому ничего не запустится, пока туннель не сообщит о готовности. Проверки работоспособности в Compose объясняют общий принцип.

Публикация на всех адресах вместо только tailnet

Удалите префикс адреса и привязки портов в 0.0.0.0, что включает публичный IP-адрес VPS. Делайте это только при наличии настроенного вами брандмауэра и предварительно ознакомившись с примечанием по ufw выше.

    ports:
      - "8080:8080/tcp"

Проверка прохождения трафика через туннель

Выполните один и тот же запрос дважды: сначала изнутри пространства имен, а затем с хоста, после чего сравните результаты.

docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.org

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

Таблица маршрутизации показывает, какой трафик идет через туннель, а какой — в обход.

docker run --rm --network=container:gluetun alpine:3.22 ip route show

Маршрут по умолчанию должен указывать на интерфейс туннеля, tun0. Ниже вы должны увидеть по одному маршруту для каждой записи в FIREWALL_OUTBOUND_SUBNETS, указывающему на шлюз Docker bridge. Любой другой маршрут, выходящий через eth0, означает, что трафик идет в обход VPN.

Сервер управления Gluetun сообщает тот же публичный IP на порту 8000 по адресу /v1/publicip/ip. В последних версиях требуется настройка аутентификации для маршрутов сервера управления, поэтому настройте её, прежде чем полагаться на этот метод.

Утечка, вызванная неверной подсетью

FIREWALL_OUTBOUND_SUBNETS — это намеренно созданное отверстие в межсетевом экране, поэтому размер риска прямо пропорционален размеру этого отверстия. Четыре способа сделать его чрезмерно большим:

  • 0.0.0.0/0 отправляет весь трафик вне туннеля. Две проверки IP, описанные выше, выявят это при первом же запуске, так как они вернут одинаковый адрес.
  • Диапазон шире, чем целевой. Открытие 10.0.0.0/8 для доступа к одной машине по адресу 10.0.1.7 также открывает доступ ко всем адресам, которые может анонсировать участник торрент-раздачи в этом диапазоне. Указывайте 10.0.1.7/32.
  • Диапазон, перекрывающий собственные адреса туннеля. В документации gluetun предупреждается, что это заставляет gluetun отправлять VPN-трафик через мост, что нарушает перенаправление портов. Проверьте значение WIREGUARD_ADDRESSES перед открытием любого частного диапазона.
  • 100.64.0.0/10 для Tailscale. Это открывает доступ примерно к четырем миллионам адресов ради того, чтобы можно было связаться с одним узлом. Перечисляйте нужные узлы как записи /32.

Помните, что эта настройка применяется ко всему пространству имен. Открытие подсети для того, чтобы индексатор мог получить доступ к хост-сервису, открывает ту же подсеть и для торрент-клиента, использующего это же пространство имен. Повторно запускайте проверку публичного IP после каждого изменения этой переменной, так как это единственный тест, показывающий, привело ли изменение к ожидаемому результату.

Что ломается при перезапуске gluetun

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

Error response from daemon: cannot join network of a non running container

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

docker compose up -d --force-recreate

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

FAQ

Почему Docker выдает ошибку о публикации порта и сетевом режиме контейнера?

Потому что блок ports: все еще присутствует в сервисе, для которого также задан параметр network_mode: service:gluetun. Публикация порта добавляет правило NAT, которое перенаправляет порт хоста в собственное сетевое пространство имен контейнера, а у контейнера в этом режиме его нет. Удалите блок ports: из этого сервиса и добавьте такое же сопоставление в сервис gluetun. Номер порта остается прежним, так как приложение по-прежнему ожидает соединений на нем внутри общего пространства имен.

Как другие контейнеры могут получить доступ к сервису, который находится за gluetun?

Контейнеры внутри одного пространства имен взаимодействуют друг с другом через 127.0.0.1. Контейнеры вне этого пространства используют имя сервиса gluetun, поэтому http://gluetun:8080 работает там, где http://qbittorrent:8080 не работает. Контейнер приложения не имеет адреса ни в одной сети Docker, поэтому встроенному DNS-серверу нечего разрешать для его имени. Публикация портов для этого не требуется, если оба контейнера находятся в одной сети Compose.

Что нужно указать в FIREWALL_OUTBOUND_SUBNETS?

Только те адреса, к которым контейнер за gluetun должен инициировать соединение, указанные максимально узко. Одиночный хост — это /32. Две типичные записи — это хост Docker по адресу 172.17.0.1/32 и один /32 для каждого узла Tailscale, к которому вы обращаетесь. Никогда не добавляйте 0.0.0.0/0 и не указывайте диапазон, который пересекается с адресами вашего собственного VPN-туннеля. Входящие соединения на опубликованный порт здесь не требуют записей.

Почему контейнер не может разрешить мои имена Tailscale MagicDNS?

MagicDNS работает путем перенаправления резолвера хоста на DNS-сервер Tailscale, а контейнер не использует резолвер хоста. Он использует то, что указано в его собственном файле /etc/resolv.conf, который за gluetun настроен на DNS-сервер gluetun. Проверьте это с помощью docker exec <container> cat /etc/resolv.conf. Используйте числовой адрес 100.x узла или закрепите имя с помощью записи extra_hosts в этом контейнере.

Как убедиться, что трафик по-прежнему идет через VPN?

Выполните один запрос изнутри пространства имен и такой же запрос с хоста, затем сравните ответы. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org должен вернуть выходной адрес вашего VPN-провайдера, в то время как curl -s https://api.ipify.org на VPS вернет адрес самого VPS. Если ответы совпадают, значит, туннель не пропускает трафик контейнера. Повторяйте эту проверку после каждого изменения FIREWALL_OUTBOUND_SUBNETS.