Gluetun: як дістатися хоста й інших контейнерів
Контейнер за Gluetun не має власних інтерфейсів: публікуйте його порти на Gluetun і відкривайте лише потрібні підмережі поза VPN-тунелем.
Що відбувається, коли контейнер приєднується до мережі gluetun
Контейнер, у якому задано network_mode: service:gluetun, не має власних мережевих інтерфейсів. Він приєднується до мережевого простору імен gluetun, тому публікація портів і правила firewall перестають бути властивостями цього контейнера та стають властивостями сервісу gluetun. Усі наведені нижче відповіді випливають саме з цього факту.
Мережевий простір імен — це приватна копія мережевого стека ядра: його власні інтерфейси, таблиця маршрутизації, правила firewall і сокети, що прослуховують порти. За замовчуванням 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, конфліктують, а другий застосунок не запускається через помилку, що адреса вже використовується. Змініть порт одного з них у його власній конфігурації, наприклад змініть змінну WEBUI_PORT в образі LinuxServer qBittorrent, а потім опублікуйте новий номер на gluetun.
Як контейнери за gluetun взаємодіють один з одним?
У межах namespace вони вже спільно використовують loopback-інтерфейс. Контейнер за gluetun звертається до сусіднього контейнера за адресою 127.0.0.1:<port> без використання Docker network.
Ззовні namespace контейнер не має власного імені. Вбудований DNS Docker перетворює ім’я сервісу на адресу цього сервісу в user-defined network, а цей контейнер не має адреси в жодній мережі. Тому звичайний контейнер, наприклад Sonarr, не звертається до torrent-клієнта за адресою http://qbittorrent:8080. Він звертається до нього за адресою http://gluetun:8080, оскільки socket прослуховується в namespace gluetun за адресою gluetun. Це може здивувати користувачів, які знають як працюють мережі Docker Compose та імена сервісів і очікують стандартної схеми іменування. Публікувати порт на host не потрібно, оскільки обидва контейнери підключені до тієї самої Compose network.
Перед будь-яким іншим налагодженням перевірте DNS. Gluetun використовує власний resolver і перезаписує /etc/resolv.conf у власному контейнері, але /etc/resolv.conf є файлом окремого контейнера, тому файл, який записав gluetun, не є тим файлом, який читає ваш застосунок.
docker exec qbittorrent cat /etc/resolv.confЯк підключитися до сервісу, що працює на Docker host?
Використайте host.docker.internal. Для цього потрібні два параметри у двох різних місцях, оскільки несправні дві різні частини конфігурації.
Спочатку потрібно додати ім’я. /etc/hosts задається для кожного контейнера окремо, тому запис extra_hosts має належати контейнеру застосунку, а не gluetun.
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway — це спеціальне значення, яке Docker замінює на внутрішню адресу самого host. У звичайній інсталяції Docker у Linux це адреса bridge docker0, зазвичай 172.17.0.1. Перевірте свою адресу за допомогою ip -4 addr show docker0 на VPS. Docker Desktop визначає це ім’я самостійно. Саме тому в інструкціях для ноутбуків часто пропущено рядок extra_hosts, а той самий файл потім не працює на сервері.
Потім потрібно додати маршрут. Додавання імені лише повідомляє контейнеру, яку адресу використовувати. Пакет все одно виходить через маршрут за замовчуванням gluetun — через tunnel, а firewall gluetun його відкидає. Симптомом є з’єднання, яке зависає, а потім завершується тайм-аутом, а не відмовою в підключенні. Відмова означає, що пакет надійшов і певний компонент відповів відмовою. Тайм-аут означає, що пакет не надійшов.
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32Потім перевірте, чи сервіс на host справді прослуховує цю адресу. Сервер PostgreSQL, прив’язаний лише до 127.0.0.1, недоступний з будь-якого контейнера незалежно від використання tunnel, оскільки 127.0.0.1 усередині namespace — це loopback самого namespace. Натомість прив’яжіть його до 172.17.0.1: так він прийматиме підключення з контейнерів, залишаючись недоступним через публічний інтерфейс. Перевірте це за допомогою ss -lntp | grep 5432 на host.
Що саме змінює FIREWALL_OUTBOUND_SUBNETS
Документація gluetun описує цей параметр як перелік підмереж, розділених комами, до яких gluetun і контейнери зі спільним мережевим стеком дозволено підключатися. Також зазначено, що параметр змінює правила firewall і маршрутизацію. Важливі обидві частини. Gluetun додає маршрут для кожної вказаної підмережі через шлюз Docker bridge, тому пакети до цих адрес виходять через eth0, а не через тунель. Також gluetun відкриває для них доступ у firewall, оскільки інакше він відкидає вихідний трафік, не призначений для 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. Для двох напрямків потрібні різні налаштування.
Вхідний доступ налаштувати просто. Публікація 8080:8080 у gluetun прив’язує цей порт до всіх адрес хоста. Інтерфейс tailscale0 хоста є однією з них, тому вузол мережі відкриває http://<machine-name>:8080 і отримує доступ до контейнера. Gluetun не бере участі в цьому шляху, оскільки правило NAT Docker працює на хості, поза мережевим простором імен.
Щоб вебінтерфейс був доступний лише через tailnet, прив’яжіть опублікований порт до адреси Tailscale хоста, а не до всіх адрес.
ports:
- "100.101.102.103:8080:8080/tcp"Знайдіть цю адресу на хості за допомогою tailscale ip -4. У цьому випадку прив’язка є надійнішим засобом контролю, ніж правило firewall, оскільки порт взагалі не відкривається на публічному інтерфейсі. Це також усуває проблему, описану в публікації портів 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 і більше ніде. Рядок extra_hosts є лише в Prowlarr, оскільки саме цей контейнер розв’язує імена 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 поруч із compose-файлом. Цю схему описано в розділі env-файли та секрети для Docker Compose. Директива condition: service_healthy використовує healthcheck, який уже постачається з образом gluetun, тому нічого не запускається, доки тунель не повідомить про готовність. Загальний формат описано в розділі healthcheck у Compose.
:::detailsПублікація на всіх адресах, а не лише в tailnet
Приберіть префікс адреси та прив’язки портів у 0.0.0.0. Це також відкриє доступ через публічну IP-адресу VPS. Робіть це лише за наявності firewall, яким ви керуєте, і спочатку прочитайте наведену вище примітку щодо 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Перший запит має вивести адресу виходу вашого VPN-провайдера. Другий має вивести адресу VPS. Якщо вони збігаються, трафік контейнера не проходить через тунель. Поки це не виправлено, решта інструкцій у цьому посібнику не має сенсу.
Таблиця маршрутизації показує, який трафік проходить поза тунелем, а який — ні.
docker run --rm --network=container:gluetun alpine:3.22 ip route showМаршрут за замовчуванням має вказувати на інтерфейс тунелю, tun0. Нижче має бути по одному маршруту для кожного запису в FIREWALL_OUTBOUND_SUBNETS, який вказує на шлюз Docker bridge. Будь-який інший маршрут через eth0 передає трафік в обхід VPN.
Control server Gluetun повідомляє ту саму публічну IP-адресу на порту 8000 за адресою /v1/publicip/ip. В останніх версіях потрібно налаштувати автентифікацію для маршрутів control server, тому зробіть це до того, як покладатися на нього.
Підмережа з одним неправильним діапазоном створює витік
FIREWALL_OUTBOUND_SUBNETS — це навмисно відкритий у firewall прохід, тому розмір проходу визначає розмір ризику. Ось чотири способи зробити його надто широким:
0.0.0.0/0надсилає весь трафік за межі тунелю. Дві наведені вище перевірки IP виявляють це під час першого запуску, оскільки повертають ту саму адресу.- Діапазон ширший за потрібний. Відкриття
10.0.0.0/8для доступу до однієї машини за адресою10.0.1.7також відкриває всі адреси, які може оголошувати peer торента в цьому діапазоні. Запишіть10.0.1.7/32. - Діапазон перетинається з власними адресами тунелю. Документація gluetun попереджає, що в такому разі gluetun надсилає VPN-трафік через bridge замість тунелю, через що port forwarding перестає працювати. Перевірте значення
WIREGUARD_ADDRESSESперед відкриттям будь-якого приватного діапазону. 100.64.0.0/10для Tailscale. Це приблизно чотири мільйони відкритих адрес, щоб отримати доступ до одного peer. Додайте потрібні peer як записи/32.
Пам’ятайте, що цей параметр поширюється на весь namespace. Відкриття підмережі, щоб indexer міг звертатися до сервісу на хості, відкриває ту саму підмережу й для torrent client, який використовує цей namespace. Після кожної зміни цієї змінної повторно перевіряйте публічну 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 повідомляє "port publishing and the container type network mode"?
Це означає, що блок ports: усе ще заданий для сервісу, який також використовує network_mode: service:gluetun. Публікація порту додає правило NAT, що перенаправляє порт хоста до власного network namespace контейнера. У цьому режимі контейнер не має власного network namespace. Видаліть блок ports: із цього сервісу та додайте таке саме зіставлення до сервісу gluetun. Номер порту не змінюється, оскільки застосунок і далі слухає його у спільному namespace.
Як інші контейнери можуть отримати доступ до сервісу за gluetun?
Контейнери в тому самому namespace звертаються один до одного за адресою 127.0.0.1. Контейнери поза ним використовують ім’я сервісу gluetun, тому http://gluetun:8080 працює, а http://qbittorrent:8080 — ні. Контейнер застосунку не має адреси в жодній Docker network, тому вбудованому DNS-серверу нічого повертати для його імені. Публікація порту для цього не потрібна, якщо обидва контейнери підключені до однієї Compose network.
Що потрібно вказати у FIREWALL_OUTBOUND_SUBNETS?
Лише адреси, до яких контейнер за gluetun має встановлювати вихідні з’єднання. Записуйте їх якомога точніше. Окремий хост задається як /32. Два типові записи — це Docker host за адресою 172.17.0.1/32 і один /32 для кожного Tailscale peer, до якого ви підключаєтеся. Ніколи не додавайте 0.0.0.0/0 і не додавайте діапазон, що перетинається з власними tunnel addresses вашого VPN. Для вхідних з’єднань до опублікованого порту цей параметр не потрібен.
Чому контейнер не може визначити мої Tailscale MagicDNS names?
MagicDNS працює, коли resolver хоста спрямовує запити до DNS-сервера Tailscale. Контейнер не використовує resolver хоста. Він використовує значення власного /etc/resolv.conf, а за gluetun це DNS configuration gluetun. Перевірте це за допомогою docker exec <container> cat /etc/resolv.conf. Використовуйте числову адресу 100.x потрібного peer або закріпіть ім’я записом extra_hosts у цьому контейнері.
Як перевірити, що traffic і далі проходить через VPN?
Виконайте один запит усередині namespace і такий самий запит із хоста, а потім порівняйте відповіді. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org має повернути exit address вашого VPN provider, тоді як curl -s https://api.ipify.org на VPS має повернути адресу VPS. Однакові відповіді означають, що tunnel не передає traffic контейнера. Повторюйте цю перевірку після кожної зміни до FIREWALL_OUTBOUND_SUBNETS.