Docker qua VPN: port biến mất với network_mode
Dùng Gluetun làm VPN sidecar nhưng port biến mất? Tìm hiểu network_mode: service làm mất namespace riêng, port publish và service name, kèm compose đúng.
Tại sao các cổng biến mất khi định tuyến Docker container qua VPN
Để định tuyến Docker container qua VPN, bạn cấp tunnel cho một container, rồi gắn các container khác vào network namespace của nó bằng network_mode: "service:gluetun". Đây là phần thường gây bất ngờ. Container được gắn không còn network riêng, nên các cổng đã publish và Docker service name của nó cũng biến mất. Hãy publish các cổng trên VPN container, rồi để các container khác truy cập ứng dụng qua name của VPN container.
Nếu để một block ports: trên container được gắn, Docker sẽ từ chối tạo container đó:
Error response from daemon: conflicting options: port publishing and the container type network modeCông cụ được dùng ở đây là Gluetun, một container kết nối đến nhà cung cấp VPN thương mại (virtual private network) qua WireGuard hoặc OpenVPN và tự áp dụng firewall. Bản release v3.41.3 là bản hiện tại tính đến tháng 10 năm 2026. Các ví dụ dùng Mullvad với WireGuard, nên bạn cần account và key do nhà cung cấp cấp. Nếu muốn terminate tunnel trên phần cứng do bạn sở hữu, chạy WireGuard server riêng trên VPS sẽ tạo đầu bên kia, còn wg-easy trong Docker cung cấp web interface cho cấu hình đó.
network_mode: "service:gluetun" thực sự làm gì
Mỗi Docker container thường có một network namespace riêng: interface, routing table, firewall rule và listening socket riêng. Chế độ service: bỏ qua bước đó và khởi động container bên trong namespace của gluetun. Một namespace chỉ có một địa chỉ IP, và điều này làm thay đổi 6 điểm.
- App không có địa chỉ riêng. Địa chỉ của app là địa chỉ của gluetun.
- App không được gắn vào Docker network nào, nên service name của nó không được đăng ký và không thể resolve. Các container khác phải dùng
gluetun. - Các container bên trong namespace liên lạc với nhau qua
localhost. - Hai container trong cùng một namespace không thể listen trên cùng một port. Tài liệu Gluetun nói rõ: không có cách xử lý thay thế.
- Capability thuộc về container, không thuộc về namespace. Gluetun giữ
NET_ADMINvà/dev/net/tunvì nó tạo tunnel interface. Container được gắn vào không kế thừa các capability đó. - Compose từ chối mọi file trong đó một service đồng thời đặt cả
network_modevànetworks. Hãy gắn gluetun vào các network của bạn; app sẽ đi cùng namespace đó.
Khởi động lại gluetun sẽ ngắt kết nối mọi container được gắn vào nó. Đây là hành vi đã được ghi rõ trong tài liệu và cũng là lý do gluetun khởi động lại tiến trình VPN bên trong container thay vì thoát khi kết nối bị lỗi. Sau khi tự khởi động lại hoặc tạo lại gluetun, hãy khởi động lại các container được gắn vào nó.
Tệp compose hoạt động
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-stoppedTag :v3 là bản phát hành ổn định mới nhất trong dòng v3. Tag :latest trỏ đến commit cuối cùng của nhánh master, tức phiên bản đang phát triển. Vì vậy, hãy cố định :v3 trên máy mà bạn không muốn phải debug vào một ngày làm việc bất kỳ.
WEBUI_PORT=8080 phải khớp với cổng được publish, vì qBittorrent bind trong namespace của gluetun và rule publish chuyển network traffic từ host đến cổng 8080 trong namespace đó. Nếu thay đổi một trong hai số mà không thay đổi số còn lại, cổng sẽ không phản hồi. 127.0.0.1:8080:8080 giữ web interface trên địa chỉ loopback của host. Một 8080:8080 đứng riêng sẽ publish trên mọi interface và tự thêm rule vào firewall. Đây là lý do cổng được Docker publish có thể vượt qua ufw.
Khởi động các container, rồi kiểm tra theo thứ tự sau:
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30docker compose ps phải hiển thị gluetun ở trạng thái healthy và qbittorrent ở trạng thái running. Sau đó, xác nhận địa chỉ thoát từ bên trong namespace. Đây là bước kiểm tra quyết định mọi thứ còn lại:
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"Trường ip trong JSON đó phải là địa chỉ của VPN provider. Nếu đó là địa chỉ của chính server, app chưa nằm trong tunnel và các bước bên dưới sẽ không hoạt động như mô tả.
Để key bên ngoài file compose
gluetun.env chứa thông tin xác thực và không được đưa vào git:
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32Cả hai giá trị đều lấy từ file cấu hình WireGuard mà bạn tạo trong khu vực quản trị tài khoản của nhà cung cấp. Đặt quyền cho file là 600. Cần hiểu rõ lợi ích thực tế: key không nằm trong repository, nhưng docker inspect gluetun vẫn in mọi biến môi trường cho bất kỳ ai có thể truy cập Docker socket. File môi trường và secret trong Docker Compose trình bày các tùy chọn bảo mật mạnh hơn.
Cách một container bên ngoài tunnel kết nối đến container bên trong
Cả hai hướng đều hoạt động và mỗi hướng dùng một tên khác nhau. Hai container cần dùng chung một Docker network. Đó là network của gluetun, vì container được gắn vào không có network riêng. Cách Docker Compose kết nối các network giải thích các giá trị mặc định.
Từ bên ngoài vào bên trong, dùng tên của gluetun và cổng mà app đang listen. Một reverse proxy container có thể truy cập giao diện web của qBittorrent tại gluetun:8080. Không cần thêm mục ports:, vì traffic giữa các container vẫn nằm trên Docker network và không đi qua host port.
Từ bên trong ra bên ngoài, dùng service name của container còn lại, chẳng hạn postgres:5432. Gluetun đã có thể resolve tên của các container khác từ bên trong namespace của nó kể từ v3.41. Vì vậy, hãy ghim phiên bản đó hoặc mới hơn nếu tên không resolve được.
Firewall của Gluetun quyết định client nào được phép mở kết nối đến nó. Traffic từ Docker network riêng của gluetun được cho phép. Client ở subnet khác, laptop trong LAN hoặc container trên một bridge network riêng cần có return path được firewall và routing của gluetun cho phép:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24Ý nghĩa được tài liệu ghi rõ: đây là các subnet phân tách bằng dấu phẩy mà Gluetun và các container dùng chung network stack với nó được phép truy cập.
Kết nối inbound từ Internet là một vấn đề riêng. Peer của torrent client đi vào từ phía VPN, nên publish port 6881 trên host không giúp các kết nối này hoạt động. Bạn cần một port được provider forward và khai báo port đó trong FIREWALL_VPN_INPUT_PORTS; tùy chọn này cho phép các port từ phía VPN server. Mullvad, được dùng trong ví dụ này, không hỗ trợ port forwarding. Vì vậy, bạn cần một provider có hỗ trợ tính năng này. Đây là phần mà phần lớn media stack được xây dựng bằng Docker Compose bỏ sót và để hỏng.
Cơ chế ngắt kết nối: điều gì xảy ra khi tunnel bị ngắt
Mẫu triển khai này phát huy giá trị khi có lỗi. Container được gắn kết chia sẻ các interface và bảng định tuyến của Gluetun. Firewall của Gluetun chặn lưu lượng Internet thông thường đi ngoài tunnel, nhưng vẫn cho phép truy cập đến endpoint của VPN server, lưu lượng Docker nội bộ và các ngoại lệ đã cấu hình. Firewall này ngăn traffic chuyển sang interface thông thường khi tunnel bị ngắt. Không có khoảng thời gian nào để packet rò rỉ qua interface thông thường trong lúc client kết nối lại.
Gluetun tự theo dõi kết nối của nó. Mỗi phút, nó gửi một ICMP echo (ping) đến các địa chỉ trong HEALTH_ICMP_TARGET_IPS, mặc định là 1.1.1.1,8.8.8.8. Nếu ICMP không được cho phép, phép kiểm tra này sẽ chuyển sang truy vấn DNS UDP dạng plaintext. Mỗi 5 phút, nó thực hiện một kết nối TCP và TLS (transport layer security) đầy đủ đến HEALTH_TARGET_ADDRESSES, mặc định là cloudflare.com:443,github.com:443. Khi các phép kiểm tra này thất bại, nó khởi động lại VPN bên trong container và ghi log:
WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeoutHãy đọc log của container được gắn kết theo thứ tự này. Các dòng như connection refused, operation not permitted và i/o timeout bên trong app là hậu quả của tunnel đã ngừng hoạt động, không phải nguyên nhân. Tài liệu Gluetun nêu rõ điều này, vì nhiều người báo cáo hậu quả rồi mất hàng giờ để truy tìm nó.
HEALTH_RESTART_VPN=on là giá trị mặc định và nên được bật. Chỉ tắt nó khi debug một lỗi cụ thể, vì khi tắt, tunnel đã ngừng hoạt động sẽ tiếp tục ở trạng thái đó.
Thứ tự khởi động: chỉ khởi động stack sau khi tunnel đã hoạt động
Image có sẵn một Docker healthcheck:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheckLệnh đó chạy một bản sao gluetun thứ hai, tồn tại trong thời gian ngắn, để truy vấn health server của instance đang chạy tại http://127.0.0.1:9999/. Tunnel hoạt động sẽ trả về 200 OK. Tunnel bị lỗi sẽ trả về 500 Internal server error kèm chuỗi lỗi, và Docker đánh dấu container là không khỏe sau 3 lần kiểm tra liên tiếp thất bại.
condition: service_healthy là điều kiện chờ cho trường hợp này. depends_on: [gluetun] thông thường chỉ chờ container khởi động. Việc đó xảy ra sớm hơn vài giây so với khi handshake hoàn tất, nên app khởi động khi network chưa hoạt động và thường bỏ cuộc ngay ở lần thử kết nối đầu tiên. Healthcheck trong Docker Compose giải thích cú pháp và các trường thời gian.
Có một giới hạn dễ gây nhầm lẫn. Compose đánh giá điều kiện này khi khởi động stack. Nó không dừng hoặc khởi động lại app nếu sau đó gluetun chuyển sang trạng thái không khỏe. Cơ chế auto-healing nội bộ của gluetun xử lý trường hợp đó. Vì vậy, gluetun khởi động lại tiến trình VPN thay vì container.
Kiểm tra rò rỉ DNS trước khi tin cậy cấu hình
DNS (domain name system) là loại rò rỉ vẫn tồn tại dù tunnel đã được cấu hình đúng. Gluetun chạy resolver riêng bên trong namespace và mặc định chuyển tiếp truy vấn qua DoT (DNS over TLS) đến Cloudflare: DNS_UPSTREAM_RESOLVER_TYPE=dot và DNS_UPSTREAM_RESOLVERS=cloudflare. Giữ nguyên cả hai giá trị này để truy vấn được mã hóa và đi qua tunnel.
Thiết lập gây rò rỉ khi trỏ đến resolver cục bộ là DNS_UPSTREAM_PLAIN_ADDRESSES. Người dùng thường bật thiết lập này khi một hostname không resolve được và muốn router hoặc resolver của nhà cung cấp trả lời thay. Tài liệu Gluetun cảnh báo rằng truy vấn gửi đến resolver trên Docker bridge hoặc LAN sẽ đi ra ngoài VPN tunnel. Resolver public không mã hóa vẫn nhận truy vấn qua tunnel, nhưng VPN provider có thể đọc và sửa chúng. Lỗi tương tự với WireGuard được đề cập trong DNS ngừng resolve qua WireGuard tunnel.
Để kiểm tra, đặt SHADOWSOCKS=on và SHADOWSOCKS_PASSWORD trên gluetun, publish 8388:8388/tcp và 8388:8388/udp, sau đó kết nối bằng Shadowsocks client được cấu hình để tunnel DNS qua UDP và mở một trang kiểm tra rò rỉ DNS. Kết quả phải hiển thị nhà cung cấp của bạn hoặc Cloudflare, không bao giờ là router gia đình. Tài liệu Gluetun cũng cảnh báo rằng một số bài kiểm tra rò rỉ có thể trả về kết quả bất thường, vì resolver bên trong namespace là một bộ trung gian caching cục bộ chứ không phải server cuối cùng trả lời. Hãy xem quốc gia không đúng hoặc resolver của chính ISP là dấu hiệu rò rỉ thực sự.
Thêm Tailscale cạnh VPN sidecar và thành phần nào được ưu tiên
Tailscale là một mạng overlay xây dựng trên WireGuard, dùng để truy cập các máy của bạn. Nhiều người chạy Tailscale cạnh VPN của nhà cung cấp để duy trì đường quản trị vào stack. Dùng chung namespace cũng có nghĩa là dùng chung firewall rule, nên hai thành phần phải có cấu hình firewall và định tuyến tương thích. Tài liệu Tailscale nêu mặc định như sau: Tailscale hoạt động như một mạng overlay, chỉ định tuyến lưu lượng giữa các thiết bị đang chạy Tailscale và không can thiệp vào lưu lượng Internet công khai.
Vì vậy, câu trả lời phụ thuộc vào một thiết lập.
- Tailscale chạy trong container riêng, với cấu hình mặc định: Tailscale không bao giờ thấy lưu lượng outbound của app. Gluetun xử lý toàn bộ lưu lượng đó. Một process trong container Tailscale có thể truy cập app tại
gluetun:8080qua Docker network dùng chung; để client trong tailnet truy cập được kết nối này, bạn cần forwarding hoặc proxy. - Tailscale gắn vào namespace của gluetun với
network_mode: "service:gluetun": trong userspace networking mode mặc định,TS_USERSPACEđược bật, tailscaled không tạo interface nào và hoạt động như SOCKS5 hoặc HTTP proxy, nên không thể thay đổi định tuyến. Gluetun vẫn xử lý lưu lượng outbound thông thường, trừ khi app chủ động dùng proxy đó. - Cấu hình tương tự với
TS_USERSPACE=false: Tailscale cần thiết bị/dev/net/tunriêng và các capability tương ứng vì namespace không tự cung cấp chúng. Tailscaled tạo tunnel device và trên Linux thường cài các route IPv4 peer /32 riêng lẻ thay vì một route cho100.64.0.0/10. Chỉ đặtTS_USERSPACE=falsekhông bật aggregate route. Tailscale chỉ cài subnet route của node khác khi bạn truyền--accept-routesquaTS_EXTRA_ARGS.TS_ROUTEShoạt động theo hướng ngược lại: nó quảng bá các subnet mà container này có thể truy cập cho phần còn lại của tailnet, tương đương vớitailscale set --advertise-routes=, và không thêm route nào tại đây. Lưu lượng công khai vẫn đi ra qua gluetun. - Kernel networking trong namespace của gluetun khi đã chọn exit node,
sudo tailscale set --exit-node=<exit-node-ip>: Tailscale cài default route trong namespace đó, nhưng firewall của Gluetun có thể chặn đường này. Chọn exit node trong một container riêng không thay đổi route của app, còn userspace mode chỉ chuyển lưu lượng được gửi rõ ràng qua proxy của nó. Không kết hợp các default route cạnh tranh trong namespace của gluetun. Một default route, một thành phần quản lý.
Nếu mục tiêu là các route được quảng bá và bạn muốn truy cập toàn bộ private network phía sau máy này thay vì chỉ truy cập chính máy đó, chạy Tailscale subnet router trên VPS sẽ bao quát việc phê duyệt route, bật IP forwarding và đặt client-side flag mà TS_ROUTES không tự thực hiện.
Nếu dùng Tailscale để cung cấp admin URL thay vì cung cấp route, tailscale serve và tailscale funnel đặt HTTPS phía trước một HTTP service cục bộ; chỉ funnel mới mở service đó ra Internet công khai. Trong Tailscale v1.104.5, HTTP reverse proxy của chúng chấp nhận trực tiếp http://gluetun:8080 khi container Tailscale có thể truy cập địa chỉ đó qua Docker network dùng chung. Với target không phải localhost, hãy thêm scheme http://. Dùng chung network namespace của gluetun và target tới http://127.0.0.1:8080 cũng hoạt động.
Một tác động phụ có thể thấy khi Tailscale chạy bên trong tunnel. Peer của Tailscale sẽ thấy địa chỉ của nhà cung cấp VPN, nên Tailscale có thể chuyển sang relay thường xuyên hơn. tailscale status hiển thị relay "..." cạnh một peer thay vì direct khi điều này xảy ra. Kết nối vẫn hoạt động nhưng chậm hơn. Nếu bạn thực sự chỉ cần overlay, sự khác nhau giữa WireGuard thuần túy và Tailscale là điểm bắt đầu phù hợp hơn.
Những lỗi thường gặp và thông báo bạn sẽ thấy
Docker từ chối tạo container của ứng dụng. Error response from daemon: conflicting options: port publishing and the container type network mode nghĩa là một block ports: vẫn đang được gắn vào service. Chuyển block đó sang gluetun.
Compose từ chối toàn bộ file. Một service không thể đồng thời đặt network_mode và networks. Đặt các network trên gluetun.
Container khác không phân giải được ứng dụng. curl: (6) Could not resolve host: qbittorrent là hành vi đúng, vì container được gắn vào đó không tham gia network nào và không đăng ký tên nào. Dùng gluetun và port.
Container thứ hai được gắn vào không khởi động được. Hai tiến trình trong cùng một namespace không thể bind cùng một port. Tiến trình không bind được sẽ báo địa chỉ đang được sử dụng. Đổi port nội bộ của ứng dụng hoặc chạy thêm một gluetun.
Ứng dụng không còn network sau khi bạn thay đổi gluetun. Việc restart hoặc tạo lại gluetun làm mất kết nối của mọi container được gắn vào nó. Restart các container đó.
Trang nhỏ tải được nhưng trang lớn bị treo. Đây là vấn đề MTU (maximum transmission unit). Tunnel làm tăng overhead, còn một thành phần trên đường truyền loại bỏ các packet quá lớn mà không gửi lỗi trở lại. Kiểm tra WIREGUARD_MTU. Giá trị mặc định trong v3.41.3 là 1320. Hạ giá trị này nếu đường truyền yêu cầu packet nhỏ hơn. Development image sử dụng path MTU discovery khi không đặt giá trị này.
Gluetun không bao giờ chuyển sang trạng thái healthy. Kiểm tra startup nêu rõ các nguyên nhân cần kiểm tra đầu tiên: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. Kiểm tra key đã hết hạn chưa, sau đó kiểm tra danh sách server có cũ không, rồi kiểm tra firewall trên host có chặn lưu lượng UDP outbound không.
FAQ
Vì sao các port đã publish của container ngừng hoạt động khi chạy phía sau Gluetun?
Vì network_mode: "service:gluetun" đặt container vào network namespace của gluetun, còn một namespace chỉ có một địa chỉ IP và một tập port listening. App vẫn tiếp tục listening, nhưng publish rule phải nằm trên container sở hữu namespace đó. Chuyển danh sách ports: sang service gluetun. Nếu vẫn để danh sách này trên service được gắn vào, Docker thậm chí sẽ không tạo container: Error response from daemon: conflicting options: port publishing and the container type network mode.
Làm thế nào để truy cập một container bên trong VPN tunnel từ một container bên ngoài?
Dùng service name của gluetun và port mà app listening, ví dụ gluetun:8080. Container được gắn vào không nằm trên Docker network riêng, nên name của chính nó sẽ không resolve được. Không cần publish port cho traffic giữa các container. Theo chiều ngược lại, container bên trong namespace có thể truy cập container bên ngoài bằng service name của nó, chẳng hạn postgres:5432, trên Gluetun v3.41 trở lên. Client trên subnet khác, chẳng hạn laptop trong LAN, cần có return path được firewall và routing của gluetun cho phép; thêm subnet đó vào FIREWALL_OUTBOUND_SUBNETS.
Gluetun có hoạt động như kill switch khi VPN bị ngắt không?
Có, nếu firewall của Gluetun được bật. Container được gắn vào chia sẻ các interface và route của Gluetun, còn firewall sẽ chặn traffic Internet thông thường đi ngoài tunnel, đồng thời cho phép endpoint của VPN server, traffic Docker nội bộ và các ngoại lệ đã cấu hình. Gluetun sau đó restart VPN nội bộ và ghi log WARN [vpn] restarting VPN because it failed to pass the healthcheck, thay vì thoát, vì mọi container được gắn vào sẽ mất network khi chính gluetun restart.
Tailscale và Gluetun trong cùng một stack: thành phần nào xử lý traffic outbound?
Gluetun xử lý traffic outbound thông thường, trừ khi bạn chủ động gửi traffic qua Tailscale. Theo mặc định, Tailscale chỉ route traffic giữa các thiết bị trong tailnet và không xử lý traffic public. Ở userspace mode mặc định của container image, Tailscale không tạo interface nào nên không thể ảnh hưởng đến routing. Với TS_USERSPACE=false, Linux thường nhận các route IPv4 peer /32 riêng lẻ thay vì một route 100.64.0.0/10 tổng hợp, cùng với route đến subnet của các node khác chỉ khi bạn thêm --accept-routes thông qua TS_EXTRA_ARGS. TS_ROUTES quảng bá các subnet của container này đến tailnet và không thêm local route nào. Chọn exit node bằng sudo tailscale set --exit-node=<exit-node-ip> chỉ cài default route trong namespace đang dùng kernel networking của Tailscale; một container riêng không thay đổi route của app, còn userspace mode chỉ chuyển traffic được gửi qua proxy của nó. Hãy chọn một sản phẩm để quản lý default route, thay vì xếp chồng cả hai.