SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-28

Docker-контейнери через VPN: чому зникають порти

Gluetun і network_mode: service змінюють мережевий простір контейнера. Дізнайтеся, чому порти зникають і як налаштувати робочий 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, оскільки створює тунельний інтерфейс. Підключений контейнер не успадковує їх.
  • 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 прив’язується всередині namespace gluetun, а правило публікації передає трафік із хоста на порт 8080 у цьому namespace. Якщо змінити лише одне з цих чисел, порт не відповідатиме. 127.0.0.1:8080:8080 залишає вебінтерфейс доступним лише через loopback-адресу хоста. Окремий 8080:8080 публікує порт на всіх інтерфейсах і додає власне правило firewall. Саме так опубліковані порти Docker обходять ufw.

Запустіть стек, а потім перевірте його в такому порядку:

docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30

docker compose ps має показати gluetun зі статусом healthy, а qbittorrent — зі статусом running. Потім перевірте адресу виходу зсередини namespace. Саме ця перевірка визначає результат усіх наступних:

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. Це network gluetun, оскільки підключений контейнер не має власної мережі. У матеріалі Як налаштовані Docker Compose networks описано стандартні параметри.

Для підключення ззовні до контейнера використовуйте ім’я 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 на хості їм не допоможе. Потрібен forwarded port від вашого провайдера. Цей порт потрібно вказати в FIREWALL_VPN_INPUT_PORTS, що дозволяє порти з боку VPN-сервера. Саме цю частину найчастіше залишають непрацюючою media stacks, створені за допомогою Docker Compose.

Перемикач блокування: що відбувається, коли тунель розривається

Ця схема виправдовує свою складність у разі збою. Підключений контейнер не має іншого маршруту. Єдиний шлях назовні для нього проходить через спільний network namespace, тому після падіння тунелю запасного маршруту немає. Firewall Gluetun забезпечує це саме правило з іншого боку: вихідний network traffic проходить через тунель або до 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. Якщо ці перевірки не проходять, Gluetun перезапускає 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, який запитує 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 не зупиняє і не перезапускає застосунок. У цьому випадку працює внутрішній механізм автоматичного відновлення gluetun. Саме тому він перезапускає процес VPN, а не контейнер.

Перевірте витік DNS, перш ніж довіряти конфігурації

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

Проблему спричиняє параметр 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 proxy, тому не може змінити маршрутизацію. Gluetun і далі передає весь трафік.
  • Те саме з TS_USERSPACE=false: tailscaled створює tunnel device і встановлює маршрути, але лише для діапазону tailnet 100.64.0.0/10 та будь-яких subnet routes, оголошених через TS_ROUTES. Трафік до публічного інтернету й далі виходить через gluetun.
  • Будь-який із наведених варіантів із вибраним exit node, sudo tailscale set --exit-node=<exit-node-ip>: Tailscale призначає маршрут за замовчуванням і отримує пріоритет. Не поєднуйте це з gluetun. Один маршрут за замовчуванням має належати одному компоненту.

Якщо потрібні саме оголошені маршрути і весь приватний network за цим сервером має бути доступним, а не лише сам сервер, розділ про запуск Tailscale subnet router на VPS описує погодження маршрутів, IP forwarding і прапорець на стороні клієнта, який TS_ROUTES самостійно не налаштовує.

Якщо Tailscale потрібен для адміністративного URL, а не для маршрутизації, tailscale serve і tailscale funnel розміщують HTTPS перед gluetun:8080 для вашого tailnet. Лише funnel відкриває цей доступ до публічного інтернету.

Коли Tailscale працює всередині тунелю, це має один помітний наслідок. Його peers бачать адресу VPN provider, тому він частіше переходить на relays. У разі такого переходу 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 і порт.

Другий підключений контейнер не запускається. Два процеси в одному namespace не можуть прив’язати той самий порт, тому процес, який програв, повідомляє, що адреса вже використовується. Змініть внутрішній порт застосунку або запустіть другий 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. Перевірте, чи не минув строк дії ключа, потім — чи не застарів список серверів, а далі — чи не блокує host 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 і новіших версіях. Клієнт з іншої підмережі, наприклад laptop у вашій 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 маршрутом за замовчуванням, тому саме він отримує пріоритет. Виберіть один продукт, який керуватиме маршрутом за замовчуванням, замість одночасного використання обох.