SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-28

Docker przez VPN: dlaczego porty przestają działać

Dowiedz się, dlaczego po podłączeniu kontenera do sieci Gluetun przez network_mode service tracisz dostęp do portów. Poznaj rozwiązanie oparte na poprawnym pliku docker-compose.

Dlaczego porty znikają podczas kierowania kontenerów Docker przez VPN

Aby skierować kontenery Docker przez VPN, należy przypisać tunel do jednego kontenera, a następnie podłączyć pozostałe do jego przestrzeni nazw sieci za pomocą network_mode: "service:gluetun". To właśnie to podłączenie jest dla wielu osób zaskakujące. Podłączony kontener przestaje posiadać własną sieć, więc jego opublikowane porty oraz nazwa usługi Docker przestają być dostępne. Należy opublikować porty w kontenerze VPN, a pozostałe kontenery będą uzyskiwać dostęp do aplikacji poprzez nazwę kontenera VPN.

Pozostawienie bloku ports: w podłączonym kontenerze spowoduje, że Docker odmówi jego utworzenia:

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

Narzędziem wykorzystywanym w tym celu jest Gluetun, kontener łączący się z komercyjnym dostawcą VPN (virtual private network) za pośrednictwem WireGuard lub OpenVPN, wyposażony we własny firewall. Wydanie v3.41.3 jest aktualne na sierpień 2026. Przykłady wykorzystują Mullvad z WireGuard, dlatego wymagane jest posiadanie konta oraz klucza od dostawcy. Jeśli tunel ma być terminowany na własnym sprzęcie, uruchomienie własnego serwera WireGuard na VPS pozwala zbudować drugą stronę połączenia, a wg-easy w Docker opakowuje to rozwiązanie w interfejs WWW.

Działanie network_mode: "service:gluetun" w praktyce

Każdy kontener Docker domyślnie otrzymuje własną przestrzeń nazw sieci (network namespace): własne interfejsy, tablicę routingu, reguły firewall oraz gniazda nasłuchujące. Tryb service: pomija ten krok i uruchamia kontener wewnątrz przestrzeni nazw gluetun. Jedna przestrzeń nazw oznacza jeden adres IP, co zmienia sześć kwestii.

  • Aplikacja nie posiada własnego adresu. Jej adresem jest adres gluetun.
  • Aplikacja nie jest podłączona do żadnej sieci Docker, więc jej nazwa usługi nie jest rejestrowana i nie podlega rozwiązaniu (DNS). Inne kontenery muszą używać gluetun.
  • Kontenery wewnątrz tej samej przestrzeni nazw komunikują się przez localhost.
  • Dwa kontenery w jednej przestrzeni nazw nie mogą nasłuchiwać na tym samym porcie. Dokumentacja gluetun jasno wskazuje: nie ma na to obejścia.
  • Uprawnienia (capabilities) są przypisane do kontenera, a nie do przestrzeni nazw. Gluetun posiada NET_ADMIN oraz /dev/net/tun, ponieważ tworzy interfejs tunelu. Podłączony kontener ich nie dziedziczy.
  • Docker Compose odrzuca pliki, w których jedna usługa ma zdefiniowane zarówno network_mode, jak i networks. Podłącz gluetun do swoich sieci, a aplikacja będzie współdzielić to połączenie.

Restart gluetun powoduje rozłączenie wszystkich podłączonych do niego kontenerów. Jest to udokumentowane zachowanie i stanowi powód, dla którego gluetun restartuje proces VPN wewnątrz kontenera zamiast kończyć działanie w przypadku awarii połączenia. Po samodzielnym restarcie lub odtworzeniu gluetun należy zrestartować podłączone do niego kontenery.

Działający plik 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

Tag :v3 to najnowsze stabilne wydanie z serii v3. Tag :latest wskazuje na ostatni commit w gałęzi master, czyli wersję rozwojową, dlatego należy przypiąć :v3 na maszynie, której nie chcemy debugować we wtorki.

Wartość WEBUI_PORT=8080 musi być zgodna z opublikowanym portem, ponieważ qBittorrent wiąże się wewnątrz przestrzeni nazw gluetun, a reguła publikacji kieruje ruch z hosta na port 8080. Zmiana jednej liczby bez drugiej spowoduje, że port przestanie odpowiadać. Parametr 127.0.0.1:8080:8080 utrzymuje interfejs webowy na adresie pętli zwrotnej hosta. Zwykłe 8080:8080 publikuje usługę na każdym interfejsie i tworzy własną regułę firewalla, co jest przyczyną, dla której opublikowane porty Docker omijają ufw.

Uruchom kontener, a następnie sprawdź go w tej kolejności:

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

Polecenie docker compose ps powinno wykazać gluetun jako healthy oraz qbittorrent jako running. Następnie potwierdź adres wyjściowy z wnętrza przestrzeni nazw; jest to test, który decyduje o wszystkim innym:

docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"

Pole ip w tym pliku JSON powinno zawierać adres Twojego dostawcy VPN. Jeśli jest to własny adres serwera, aplikacja nie znajduje się w tunelu i nic poniżej nie będzie działać zgodnie z opisem.

Przechowywanie kluczy poza plikiem compose

gluetun.env przechowuje dane uwierzytelniające i nie jest dodawany do git:

WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32

Obie wartości pochodzą z pliku konfiguracyjnego WireGuard, wygenerowanego w panelu dostawcy. Należy ustawić uprawnienia pliku na 600. Należy mieć świadomość ograniczeń tego rozwiązania: klucz nie trafia do repozytorium, jednak docker inspect gluetun nadal wyświetla wszystkie zmienne środowiskowe każdemu, kto ma dostęp do Docker socket. Pliki środowiskowe i sekrety w Docker Compose omawia bezpieczniejsze alternatywy.

Komunikacja kontenera spoza tunelu z kontenerem wewnątrz

Oba kierunki działają, a każdy z nich wykorzystuje inną nazwę. Kontenery wymagają wspólnej sieci Docker, co w tym przypadku oznacza sieć gluetun, ponieważ podłączony kontener nie posiada własnej. Sposób konfiguracji sieci w Docker Compose opisuje ustawienia domyślne.

Aby połączyć się z zewnątrz do wewnątrz, należy użyć nazwy gluetun oraz portu, na którym nasłuchuje aplikacja. Kontener z reverse proxy uzyskuje dostęp do interfejsu WWW qBittorrent pod adresem gluetun:8080. Wpis ports: nie jest do tego wymagany, ponieważ ruch między kontenerami pozostaje wewnątrz sieci Docker i nie korzysta z portów hosta.

Aby połączyć się z wewnątrz na zewnątrz, należy użyć nazwy usługi drugiego kontenera, na przykład postgres:5432. Od wersji v3.41 gluetun poprawnie rozpoznaje nazwy innych kontenerów wewnątrz swojej przestrzeni nazw, więc w przypadku problemów z rozpoznawaniem nazw należy przypiąć tę lub nowszą wersję.

Firewall gluetun decyduje o tym, kto może nawiązać z nim połączenie. Ruch z własnej sieci Docker gluetun jest dozwolony. Klient z innej podsieci, laptop w sieci lokalnej lub kontener w oddzielnej sieci typu bridge, zostanie odrzucony, dopóki nie zostanie zdefiniowana odpowiednia podsieć:

FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24

Znaczenie tego parametru jest precyzyjne: to rozdzielona przecinkami lista podsieci, do których gluetun oraz kontenery współdzielące jego stos sieciowy mają dostęp.

Połączenia przychodzące z Internetu stanowią odrębny problem. Uczestnicy wymiany (peers) klienta torrent łączą się od strony VPN, więc publikacja portu 6881 na hoście nie przyniesie efektu. Wymagane jest przekierowanie portu od dostawcy VPN oraz umieszczenie tego portu w FIREWALL_VPN_INPUT_PORTS, co zezwala na ruch na portach od strony serwera VPN. Jest to element, który większość stosów multimedialnych budowanych za pomocą Docker Compose pozostawia nieskonfigurowany.

Wyłącznik bezpieczeństwa: co się dzieje, gdy tunel zostaje przerwany

Ten schemat zyskuje na złożoności w przypadku awarii. Podłączony kontener nie posiada drugiej trasy. Jego jedyną drogą poza maszynę jest przestrzeń nazw, którą współdzieli, więc gdy tunel nie działa, nie ma żadnego mechanizmu zapasowego. Firewall Gluetun wymusza tę samą zasadę z drugiej strony: ruch wychodzący przechodzi przez tunel lub do punktu końcowego serwera VPN, a wszystko inne jest odrzucane. Nie istnieje okno czasowe, w którym pakiety mogłyby wyciekać przez zwykły interfejs podczas ponownego łączenia klienta.

Gluetun monitoruje własne połączenie. Co minutę wysyła żądanie ICMP echo (ping) na adresy w HEALTH_ICMP_TARGET_IPS, które domyślnie ustawione są na 1.1.1.1,8.8.8.8. Co pięć minut wykonuje pełne nawiązanie połączenia TCP i TLS (transport layer security) do HEALTH_TARGET_ADDRESSES, domyślnie cloudflare.com:443,github.com:443. Gdy te próby kończą się niepowodzeniem, restartuje VPN wewnątrz kontenera i odnotowuje to w dzienniku:

WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout

Dzienniki podłączonego kontenera należy analizować z uwzględnieniem tej kolejności. Linie takie jak connection refused, operation not permitted oraz i/o timeout wewnątrz aplikacji są skutkiem martwego tunelu, a nie jego przyczyną. Dokumentacja Gluetun mówi o tym wprost, ponieważ użytkownicy często zgłaszają skutki i tracą godziny na ich diagnozowanie.

HEALTH_RESTART_VPN=on jest ustawieniem domyślnym i powinno pozostać włączone. Należy je wyłączać tylko podczas debugowania konkretnej awarii, ponieważ po jego wyłączeniu martwy tunel pozostaje nieaktywny.

Kolejność: zatrzymanie uruchamiania stosu przed nawiązaniem połączenia tunelu

Obraz zawiera wbudowany mechanizm healthcheck dla Docker:

HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck

To polecenie uruchamia drugą, krótkotrwałą kopię gluetun, która odpytuje serwer stanu działającej instancji pod adresem http://127.0.0.1:9999/. Działający tunel zwraca 200 OK. Uszkodzony tunel zwraca 500 Internal server error wraz z komunikatem błędu, a kontener zostaje oznaczony jako niezdrowy po pojedynczej awarii.

condition: service_healthy odpowiada za oczekiwanie na ten stan. Zwykłe depends_on: [gluetun] czeka jedynie na uruchomienie kontenera, co następuje kilka sekund przed zakończeniem procesu uzgadniania połączenia. W rezultacie aplikacja uruchamia się w martwej sieci i często przerywa działanie przy pierwszej próbie połączenia. Healthchecks w Docker Compose szczegółowo omawia składnię oraz pola dotyczące czasu oczekiwania.

Istnieje jedno ograniczenie, które często sprawia trudności. Compose ocenia ten warunek tylko raz, w momencie tworzenia kontenera. Nie zatrzymuje ani nie restartuje aplikacji później, jeśli gluetun przejdzie w stan niezdrowy. Wewnętrzny mechanizm samonaprawy gluetun obsługuje ten przypadek, dlatego restartuje on proces VPN, a nie cały kontener.

Sprawdź wyciek DNS przed zaufaniem konfiguracji

DNS (Domain Name System) to wyciek, który może wystąpić nawet przy poprawnie działającym tunelu. Gluetun uruchamia własny resolver wewnątrz przestrzeni nazw i domyślnie przesyła zapytania przez DoT (DNS over TLS) do Cloudflare: DNS_UPSTREAM_RESOLVER_TYPE=dot oraz DNS_UPSTREAM_RESOLVERS=cloudflare. Pozostawienie tych ustawień bez zmian zapewnia szyfrowanie zapytań i ich przesyłanie przez tunel.

Ustawieniem, które powoduje wyciek, jest DNS_UPSTREAM_PLAIN_ADDRESSES. Użytkownicy zmieniają je, gdy nazwa nie zostaje rozwiązana i chcą, aby odpowiedzi udzielał router lub resolver dostawcy usług internetowych. Dokumentacja Gluetun jasno określa konsekwencje: cały ruch DNS nie będzie przechodził przez tunel VPN i wycieknie na zewnątrz. Ruch pozostaje prywatny, ale lista odwiedzanych nazw hostów – nie. Wersja tego błędu dotycząca WireGuard została opisana w DNS przestaje działać przez tunel WireGuard.

Aby przeprowadzić test, ustaw HTTPPROXY=on w gluetun i opublikuj 8888:8888/tcp, a następnie skieruj przeglądarkę na ten proxy i uruchom test wycieku DNS. Wynik powinien wskazywać dostawcę VPN lub Cloudflare, nigdy router domowy. Dokumentacja Gluetun ostrzega, że niektóre testy wycieków mogą raportować nietypowe wyniki, ponieważ resolver wewnątrz przestrzeni nazw jest lokalnym pośrednikiem buforującym, a nie serwerem, który udziela ostatecznej odpowiedzi. Wskazanie błędnego kraju lub resolvera własnego ISP należy traktować jako realny sygnał wycieku.

Dodawanie Tailscale obok sidecara VPN i kwestia pierwszeństwa

Tailscale to sieć nakładkowa oparta na WireGuard, służąca do łączenia własnych maszyn. Użytkownicy uruchamiają ją obok VPN dostawcy, aby zachować ścieżkę administracyjną do stosu usług. Te dwa rozwiązania rzadko wchodzą w konflikt, co wynika z konkretnych założeń technicznych. Dokumentacja Tailscale określa domyślne zachowanie: działa jako sieć nakładkowa, trasuje ruch wyłącznie między urządzeniami z uruchomionym Tailscale i nie ingeruje w publiczny ruch internetowy.

Odpowiedź zależy zatem od jednego ustawienia.

  • Tailscale we własnym kontenerze, konfiguracja domyślna: nie widzi ruchu wychodzącego aplikacji. Cały ruch obsługuje gluetun. Tailscale łączy się z aplikacją przez gluetun:8080, dokładnie tak samo jak każdy inny zewnętrzny kontener.
  • Tailscale podpięte do przestrzeni nazw gluetun za pomocą network_mode: "service:gluetun": wymaga własnych cap_add w postaci net_admin oraz net_raw, ponieważ uprawnienia nie są dziedziczone wraz z przestrzenią nazw. W domyślnym trybie sieciowym userspace, TS_USERSPACE jest włączone, tailscaled nie tworzy żadnego interfejsu i działa jako proxy SOCKS5 lub HTTP, więc nie może modyfikować tablicy routingu. Gluetun nadal obsługuje cały ruch.
  • To samo, z użyciem TS_USERSPACE=false: tailscaled tworzy urządzenie tunelowe i dodaje trasy, ale tylko dla zakresu tailnet 100.64.0.0/10 oraz wszelkich tras podsieci anonsowanych za pomocą TS_ROUTES. Ruch publiczny nadal wychodzi przez gluetun.
  • Każdy z powyższych przypadków z wybranym węzłem wyjściowym (exit node), sudo tailscale set --exit-node=<exit-node-ip>: Tailscale przejmuje domyślną trasę i wygrywa. Nie należy łączyć tego z gluetun. Jedna trasa domyślna, jeden właściciel.

Jeśli celem są anonsowane trasy i użytkownik chce uzyskać dostęp do całej sieci prywatnej za serwerem, a nie tylko do samego serwera, uruchomienie routera podsieci Tailscale na VPS wyjaśnia zatwierdzanie tras, przekazywanie pakietów IP oraz flagę po stronie klienta, której TS_ROUTES samodzielnie nie konfiguruje.

Jeśli Tailscale ma służyć jedynie do udostępnienia adresu URL dla administratora, tailscale serve oraz tailscale funnel pozwalają umieścić HTTPS przed gluetun:8080 wewnątrz tailnetu, przy czym tylko funkcja funnel otwiera dostęp do publicznego Internetu.

Efektem ubocznym jest sytuacja, gdy Tailscale działa wewnątrz tunelu. Węzły sieci widzą adres dostawcy VPN, więc należy spodziewać się częstszego korzystania z przekaźników (relays). tailscale status wyświetla relay "..." obok węzła zamiast direct, gdy do tego dojdzie. Połączenie działa, ale jest wolniejsze. Jeśli sieć nakładkowa jest jedynym wymaganym elementem, różnica między czystym WireGuard a Tailscale stanowi lepszy punkt wyjścia.

Co ulega awarii i jaki komunikat zobaczysz

Docker odmawia utworzenia kontenera aplikacji. Error response from daemon: conflicting options: port publishing and the container type network mode oznacza, że blok ports: nadal znajduje się w podłączonej usłudze. Przenieś go do gluetun.

Compose odrzuca cały plik. Usługa nie może jednocześnie ustawić network_mode oraz networks. Umieść sieci w gluetun.

Inny kontener nie może rozwiązać nazwy aplikacji. curl: (6) Could not resolve host: qbittorrent to poprawne zachowanie, ponieważ podłączony kontener nie dołączył do żadnej sieci i nie zarejestrował żadnej nazwy. Użyj gluetun oraz portu.

Drugi podłączony kontener nie uruchamia się. Dwa procesy w tej samej przestrzeni nazw nie mogą powiązać tego samego portu, a proces, który przegrał, zgłasza, że adres jest już w użyciu. Zmień wewnętrzny port aplikacji lub uruchom drugą instancję gluetun.

Aplikacja nie ma sieci po modyfikacji gluetun. Restartowanie lub odtwarzanie gluetun przerywa łączność dla wszystkich podłączonych do niego elementów. Zrestartuj te kontenery.

Małe strony ładują się, a duże zawieszają. To kwestia MTU (maximum transmission unit). Tunel dodaje narzut, a któryś z elementów na ścieżce odrzuca zbyt duże pakiety bez wysyłania komunikatu o błędzie. Zmniejsz WIREGUARD_MTU, wypróbuj 1400, a następnie 1320.

Gluetun nigdy nie uzyskuje statusu healthy. Kontrola startowa wskazuje pierwszych podejrzanych: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. Sprawdź, czy klucz nie wygasł, czy lista serwerów nie jest przestarzała oraz czy firewall hosta nie blokuje ruchu wychodzącego UDP.

FAQ

Dlaczego opublikowane porty kontenera przestały działać za Gluetun?

Ponieważ network_mode: "service:gluetun" umieszcza kontener wewnątrz przestrzeni nazw sieciowej (network namespace) usługi gluetun, a przestrzeń nazw posiada jeden adres IP i jeden zestaw nasłuchujących portów. Aplikacja nadal nasłuchuje, ale reguła publikacji musi znajdować się w kontenerze, który jest właścicielem przestrzeni nazw. Przenieś listę ports: do usługi gluetun. Jeśli pozostawisz ją w podłączonej usłudze, Docker nie utworzy jej, zwracając błąd: Error response from daemon: conflicting options: port publishing and the container type network mode.

Jak uzyskać dostęp do kontenera wewnątrz tunelu VPN z kontenera znajdującego się poza nim?

Użyj nazwy usługi gluetun oraz portu, na którym nasłuchuje aplikacja, na przykład gluetun:8080. Podłączony kontener nie posiada własnej sieci Docker, więc jego nazwa nie zostanie rozpoznana. Ruch między kontenerami nie wymaga publikowania portów. W drugą stronę, kontener wewnątrz przestrzeni nazw łączy się z kontenerem zewnętrznym poprzez jego nazwę usługi, na przykład postgres:5432 (w wersji Gluetun 3.41 i nowszych). Klient z innej podsieci, na przykład laptop w sieci lokalnej, zostanie zablokowany przez zaporę sieciową gluetun, dopóki nie dodasz tej podsieci do FIREWALL_OUTBOUND_SUBNETS.

Czy Gluetun działa jako wyłącznik bezpieczeństwa (kill switch), gdy połączenie VPN zostanie zerwane?

Tak, z dwóch powodów jednocześnie. Podłączony kontener nie posiada innej trasy routingu niż ta w udostępnionej przestrzeni nazw, więc po wygaśnięciu tunelu traci dostęp do sieci zewnętrznej. Zapora sieciowa gluetun zezwala na ruch wychodzący wyłącznie przez tunel oraz do punktu końcowego serwera VPN. Gluetun restartuje połączenie VPN wewnętrznie, logując WARN [vpn] restarting VPN because it failed to pass the healthcheck, zamiast kończyć działanie, ponieważ każdy podłączony kontener straciłby sieć w momencie restartu samego gluetun.

Tailscale i Gluetun w tym samym stosie: który z nich obsługuje ruch wychodzący?

Gluetun, w każdej konfiguracji poza jedną. Tailscale domyślnie kieruje ruch tylko między urządzeniami wewnątrz tailnet i nie ingeruje w ruch publiczny. W domyślnym trybie userspace obrazu kontenera nie tworzy on żadnego interfejsu, więc nie wpływa na routing. Z flagą TS_USERSPACE=false instaluje trasy tylko dla 100.64.0.0/10 oraz reklamowanych podsieci. Wyjątkiem jest węzeł wyjściowy (exit node): sudo tailscale set --exit-node=<exit-node-ip> sprawia, że Tailscale staje się domyślną trasą i przejmuje kontrolę. Należy wybrać jedno rozwiązanie do obsługi domyślnej trasy, zamiast łączyć oba produkty.