SSD Nodes Learn 🎉 VPS от $4.99/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-07

Настройка доступа к сети для контейнеров через Gluetun

Узнайте, как организовать сетевое взаимодействие контейнеров за Gluetun. Разберем публикацию портов и настройку маршрутизации для доступа к локальным подсетям вне VPN туннеля.

Что происходит, когда контейнер подключается к сети 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 (трансляции сетевых адресов), которое перенаправляет порт хоста в сетевое пространство имен контейнера, а у данного контейнера его нет. Перенесите маппинг в сервис 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 в образе qBittorrent от LinuxServer, а затем опубликуйте новый номер в 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 заменяет на внутренний адрес самого хоста. В стандартной установке Docker на Linux это адрес моста docker0, обычно 172.17.0.1. Уточните свой адрес с помощью ip -4 addr show docker0 на VPS. Docker Desktop разрешает это имя самостоятельно, поэтому в руководствах, написанных для ноутбуков, часто пропускают строку extra_hosts, из-за чего тот же файл конфигурации не работает на сервере.

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

  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, а не через VPN-туннель. Также для них открывается межсетевой экран, так как в противном случае 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, зарезервированном для CGNAT. Для двух направлений трафика требуются разные подходы.

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

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

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

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

Исходящие соединения — это область, где вступает в силу FIREWALL_OUTBOUND_SUBNETS. Если контейнеру необходимо обратиться к узлу сети, добавьте адрес этого узла. Рекомендуется использовать /32 для каждого узла, а не весь диапазон /10. Имена 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, поэтому запуск остальных контейнеров не начнется, пока туннель не сообщит о готовности. Healthcheck в 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 управляет пространством имен (namespace), поэтому жизненный цикл 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 сообщает об ошибке "port publishing and the container type network mode"?

Потому что блок 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.