Docker-контейнери через VPN: чому зникають порти
Після підключення контейнера до Gluetun через network_mode: service:gluetun порти зникають. Дізнайтеся чому та перегляньте робочий compose-файл.
Чому порти зникають, коли 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-провайдера через WireGuard або OpenVPN і має власний firewall. Станом на August 2026 актуальним є реліз v3.41.3. У прикладах використовується Mullvad із WireGuard, тому потрібні обліковий запис і ключ від вашого провайдера. Якщо ви хочете завершувати тунель на власному обладнанні, розгортання власного WireGuard-сервера на VPS створює інший кінець тунелю, а wg-easy у Docker додає до нього вебінтерфейс.
Що насправді робить network_mode: "service:gluetun"
Зазвичай кожен Docker-контейнер отримує власний мережевий простір імен: власні інтерфейси, таблицю маршрутизації, правила firewall і сокети, що прослуховують порти. Режим service: пропускає цей крок і запускає контейнер у просторі імен gluetun. Один простір імен означає одну IP-адресу, і це змінює шість речей.
- Застосунок не має власної адреси. Його адреса — адреса gluetun.
- Застосунок не підключений до жодної Docker network, тому його ім’я сервісу не реєструється і не визначається. Інші контейнери мають використовувати
gluetun. - Контейнери всередині простору імен взаємодіють один з одним через
localhost. - Два контейнери в одному просторі імен не можуть прослуховувати один і той самий порт. У документації Gluetun це зазначено прямо: обхідного рішення немає.
- Capabilities належать контейнеру, а не простору імен. Gluetun має
NET_ADMINі/dev/net/tun, оскільки створює tunnel interface. Підключений контейнер не успадковує їх. - 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 -30docker 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 network — мережі gluetun, оскільки підключений контейнер не має власної мережі. У розділі Як налаштовано мережі Docker Compose описано типові параметри.
Для доступу ззовні всередину використовуйте ім’я gluetun і порт, на якому прослуховується застосунок. Контейнер reverse proxy звертається до вебінтерфейсу qBittorrent за адресою gluetun:8080. Запис ports: для цього не потрібен, оскільки трафік між контейнерами залишається в Docker network і не проходить через порт хоста.
Для доступу зсередини назовні використовуйте ім’я сервісу іншого контейнера, наприклад postgres:5432. Починаючи з v3.41, Gluetun визначає імена інших контейнерів зі свого namespace, тому використовуйте цю або новішу версію, якщо ім’я не визначається.
Firewall Gluetun визначає, хто може відкривати з’єднання з ним. Трафік із власної Docker network gluetun дозволений. Клієнт з іншої підмережі, ноутбук у вашій LAN або контейнер в окремій bridge network блокуються, доки ви не вкажете цю підмережу:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24Задокументоване значення параметра точне: підмережі, розділені комами, яким Gluetun і контейнери зі спільним мережевим стеком дозволено надавати доступ.
Вхідні з’єднання з інтернету — окрема проблема. Піри torrent-клієнта підключаються через VPN, тому публікація порту 6881 на хості нічого для них не змінює. Потрібен port forwarding від вашого провайдера, а цей порт потрібно вказати в FIREWALL_VPN_INPUT_PORTS. Параметр дозволяє порти з боку VPN-сервера. Саме цю частину найчастіше не налаштовано в медіастеках, створених за допомогою Docker Compose.
Аварійний блокувач: що відбувається, коли тунель розривається
Ця схема виправдовує свою складність саме під час відмови. Підключений контейнер не має іншого маршруту. Єдиний шлях із машини проходить через спільний із ним network namespace, тому після розриву тунелю резервного маршруту немає. Firewall Gluetun забезпечує те саме правило з іншого боку: вихідний трафік проходить через тунель або до endpoint 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 увімкнено за замовчуванням, і його слід залишати увімкненим. Вимикайте його лише під час діагностики конкретної проблеми, оскільки після вимкнення непрацюючий тунель залишатиметься непрацюючим.
Порядок запуску: не дозволяйте стеку запускатися до встановлення тунелю
Образ містить healthcheck Docker:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckЦя команда запускає другу короткоживучу копію gluetun, яка опитує health server запущеного екземпляра на http://127.0.0.1:9999/. Працюючий тунель повертає 200 OK. Непрацюючий тунель повертає 500 Internal server error із рядком помилки, після чого контейнер позначається як unhealthy вже після першої помилки.
Саме condition: service_healthy очікує на цей стан. Звичайний depends_on: [gluetun] очікує лише запуску контейнера. Це відбувається на кілька секунд раніше, ніж завершується handshake, тому застосунок запускається без робочої мережі й часто припиняє роботу після першої невдалої спроби підключення. У розділі Healthchecks у Docker Compose описано синтаксис і поля, що визначають час очікування.
Є обмеження, про яке часто забувають. Compose перевіряє цю умову один раз під час створення контейнера. Якщо gluetun згодом переходить у стан unhealthy, Compose не зупиняє та не перезапускає застосунок. У цьому випадку працює внутрішній механізм auto-healing gluetun. Саме тому він перезапускає процес VPN, а не контейнер.
Перевірте витік DNS, перш ніж довіряти налаштуванню
DNS (система доменних імен) — це витік, який зберігається навіть за правильно налаштованого тунелю. Gluetun запускає власний резолвер у namespace і за замовчуванням пересилає запити через DoT (DNS over TLS) до Cloudflare: DNS_UPSTREAM_RESOLVER_TYPE=dot і DNS_UPSTREAM_RESOLVERS=cloudflare. Не змінюйте ці параметри, і ваші запити залишатимуться зашифрованими та проходитимуть через тунель.
Цю схему порушує параметр DNS_UPSTREAM_PLAIN_ADDRESSES. До нього звертаються, коли ім’я не резолвиться і потрібно, щоб замість цього відповідав маршрутизатор або DNS-резолвер провайдера. У документації Gluetun прямо описано наслідок: увесь DNS-трафік не проходитиме через VPN-тунель і витікатиме за його межі. Ваш трафік залишається приватним. Перелік запитуваних імен хостів — ні. Така сама помилка у WireGuard описана в розділі DNS, який перестає резолвити імена через тунель WireGuard.
Щоб перевірити це, задайте HTTPPROXY=on для gluetun і опублікуйте 8888:8888/tcp, потім відкрийте в браузері цей проксі та завантажте тест на витік DNS. У результаті має бути вказано вашого провайдера або Cloudflare, але не домашній маршрутизатор. Власна документація Gluetun попереджає, що деякі тести на витік можуть показувати нетипові результати, оскільки резолвер усередині namespace є локальним посередником із кешуванням, а не сервером, який зрештою надає відповідь. Неправильна країна або резолвер вашого ISP є справжньою ознакою проблеми.
Додавання Tailscale поруч із VPN sidecar і визначення пріоритетного маршруту
Tailscale — це overlay network на основі WireGuard для доступу до власних машин. Його часто запускають поруч із provider VPN, щоб зберегти адміністративний доступ до стека. Ці два компоненти рідко конфліктують. Причину цього важливо розуміти. У документації Tailscale зазначено поведінку за замовчуванням: він працює як overlay network, маршрутизує трафік лише між пристроями, на яких запущено Tailscale, і не змінює трафік до публічного інтернету.
Відповідь залежить від одного параметра.
- Tailscale у власному контейнері, конфігурація за замовчуванням: він не бачить вихідний трафік застосунку. Увесь цей трафік передає Gluetun. Tailscale підключається до застосунку через
gluetun:8080, так само як будь-який інший зовнішній контейнер. - Tailscale, підключений до namespace gluetun через
network_mode: "service:gluetun": йому потрібні власніcap_addдляnet_adminіnet_raw, оскільки capabilities не передаються разом із namespace. У режимі userspace networking за замовчуванням параметрTS_USERSPACEувімкнено. tailscaled не створює інтерфейс і працює як SOCKS5- або HTTP-проксі, тому не може змінити маршрутизацію. Увесь трафік і далі передає Gluetun. - Те саме з
TS_USERSPACE=false: tailscaled створює tunnel device і встановлює маршрути, але лише для діапазону tailnet100.64.0.0/10та будь-яких subnet routes, оголошених черезTS_ROUTES. Публічний трафік і далі виходить через gluetun. - Будь-який із наведених варіантів із вибраним exit node,
sudo tailscale set --exit-node=<exit-node-ip>: Tailscale встановлює default route і стає її власником. Не поєднуйте це з gluetun. Має бути один default route і один власник.
Побічний ефект помітний, коли Tailscale працює всередині тунелю. Його вузли бачать адресу VPN provider, тому частіше використовуються relay. Якщо це сталося, 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, спробуйте 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. Перевірте, чи не завершився термін дії ключа, потім перевірте актуальність списку серверів і переконайтеся, що firewall хоста не блокує вихідний UDP-трафік.
FAQ
Чому опубліковані порти мого контейнера перестали працювати за Gluetun?
Тому що network_mode: "service:gluetun" розміщує контейнер у мережевому просторі імен Gluetun, а один простір імен має одну IP-адресу та один набір портів, на яких приймаються з’єднання. Застосунок продовжує приймати з’єднання, але правило публікації має бути вказане для контейнера, якому належить цей простір імен. Перенесіть список ports: до сервісу Gluetun. Якщо залишити його в підключеному сервісі, Docker взагалі не створить це правило: Error response from daemon: conflicting options: port publishing and the container type network mode.
Як підключитися з контейнера поза VPN-тунелем до контейнера всередині нього?
Використовуйте ім’я сервісу Gluetun і порт, на якому застосунок приймає з’єднання, наприклад gluetun:8080. Підключений контейнер не має власної Docker network, тому його власне ім’я не визначається. Для трафіку між контейнерами публікація портів не потрібна. У зворотному напрямку контейнер усередині простору імен може підключатися до зовнішнього контейнера за його ім’ям сервісу, наприклад postgres:5432, у Gluetun v3.41 і новіших версіях. Клієнт з іншої підмережі, наприклад ноутбук у вашій LAN, Gluetun блокує на рівні firewall, доки ви не додасте цю підмережу до FIREWALL_OUTBOUND_SUBNETS.
Чи працює Gluetun як kill switch у разі розриву VPN-з’єднання?
Так, і одразу з двох причин. Підключений контейнер не має іншого маршруту, крім маршруту у спільному просторі імен, тому після розриву тунелю він втрачає шлях за межі машини. Firewall Gluetun також дозволяє вихідний трафік лише через тунель і до endpoint VPN-сервера. Після цього Gluetun перезапускає VPN внутрішньо та записує в журнал WARN [vpn] restarting VPN because it failed to pass the healthcheck, а не завершує роботу, оскільки під час перезапуску самого Gluetun усі підключені контейнери втрачають мережеве підключення.
Tailscale і Gluetun в одному stack: який із них передає вихідний трафік?
Gluetun — у всіх конфігураціях, крім однієї. Типово Tailscale маршрутизує лише трафік між пристроями у вашому tailnet, а публічний трафік не змінює. У типовому userspace mode образу контейнера він взагалі не створює інтерфейс, тому не може впливати на маршрутизацію. З параметром TS_USERSPACE=false він встановлює маршрути лише для 100.64.0.0/10 і анонсованих вами підмереж. Виняток — exit node: sudo tailscale set --exit-node=<exit-node-ip> робить Tailscale маршрутом за замовчуванням, і тоді саме він використовується. Виберіть один продукт, який володітиме маршрутом за замовчуванням, замість одночасного використання обох.