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.
Vì sao các port biến mất khi định tuyến Docker container qua VPN
Để định tuyến Docker container qua VPN, bạn cho một container sử dụng tunnel, 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 khó hiểu. Container được gắn không còn network riêng, nên các port đã publish và service name của Docker cũng biến mất theo. Hãy publish các port trên VPN container. Các container khác sẽ truy cập app bằng 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 qua WireGuard hoặc OpenVPN và có firewall riêng. Bản phát hành v3.41.3 là bản hiện tại tính đến tháng 8 năm 2026. Các ví dụ sử 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 kết thúc tunnel trên phần cứng do mình sở hữu, tự chạy WireGuard server 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 với container bên trong tunnel
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, cụ thể là network của gluetun, vì container được gắn vào đó không có network riêng. Cách kết nối các Docker network trong Docker Compose giải thích các giá trị mặc định.
Kết nối từ bên ngoài vào bên trong thì 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 cổng của host.
Kết nối từ bên trong ra bên ngoài thì dùng service name của container còn lại, ví dụ postgres:5432. Từ v3.41, Gluetun đã phân giải được tên của các container khác bên trong namespace của nó. Vì vậy, hãy cố định phiên bản này hoặc phiên bản mới hơn nếu tên không phân giải đượ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. Một client ở subnet khác, chẳng hạn laptop trong LAN hoặc container trên một bridge network riêng, sẽ bị drop cho đến khi bạn khai báo subnet đó:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24Ý nghĩa được tài liệu hóa là chính xác: danh sách 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.
Các 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 cổng 6881 trên host không có tác dụng với chúng. Bạn cần một port forwarding từ provider và phải khai báo cổng đó trong FIREWALL_VPN_INPUT_PORTS. Thiết lập này cho phép các cổng đến từ phía VPN server. Đây là phần mà hầu hết media stack được dựng bằng Docker Compose vẫn để cấu hình lỗi.
Cơ chế kill switch: điều gì xảy ra khi tunnel bị mất
Mẫu triển khai này phát huy giá trị khi xảy ra lỗi. Container được gắn vào không có route thứ hai. Đường duy nhất từ container ra ngoài máy là namespace mà nó dùng chung. Vì vậy, khi tunnel ngừng hoạt động, không có đường dự phòng. Firewall của Gluetun thực thi quy tắc tương tự từ phía còn lại: lưu lượng outbound phải đi qua tunnel hoặc đến endpoint của VPN server; mọi lưu lượng khác đều bị drop. Không có khoảng thời gian nào để packet rò ra interface 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. 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 kiểm tra này thất bại, nó restart 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 vào theo thứ tự đó. 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ói 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 sai nguyên nhân.
HEALTH_RESTART_VPN=on được bật mặc định và nên giữ nguyên. 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: không cho stack khởi động trước khi tunnel sẵn sàng
Image có sẵn Docker healthcheck:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckLệnh đó chạy một bản sao gluetun tạm thờ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à container được đánh dấu là không khỏe sau một lần kiểm tra thất bại.
condition: service_healthy là điều kiện chờ việc đó. depends_on: [gluetun] thông thường chỉ chờ container khởi động, việc này xảy ra sớm hơn vài giây so với khi handshake hoàn tất. Vì vậy 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 timing.
Có một giới hạn dễ gây nhầm lẫn. Compose chỉ đánh giá điều kiện đó một lần, khi tạo container. Compose không dừng hoặc restart app sau đó nếu 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 này. Vì vậy gluetun restart tiến trình VPN thay vì restart container.
Kiểm tra DNS leak trước khi tin tưởng cấu hình
DNS (domain name system) là loại leak vẫn tồn tại dù tunnel đã được cấu hình đúng. Gluetun chạy resolver riêng trong namespace và mặc định chuyển tiếp query qua DoT (DNS over TLS) đến Cloudflare: DNS_UPSTREAM_RESOLVER_TYPE=dot và DNS_UPSTREAM_RESOLVERS=cloudflare. Giữ nguyên cả hai thiết lập này để các lookup được mã hóa và đi qua tunnel.
Thiết lập làm hỏng điều này 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 nêu rõ cái giá phải trả: toàn bộ DNS traffic sẽ không đi qua VPN tunnel mà leak ra ngoài. Traffic của bạn vẫn private. Danh sách hostname bạn truy vấn thì không. Lỗi tương tự trong phiên bản WireGuard được đề cập tại DNS ngừng resolve qua WireGuard tunnel.
Để kiểm tra, đặt HTTPPROXY=on trên gluetun và publish 8888:8888/tcp, sau đó trỏ browser đến proxy đó rồi mở một DNS leak test. Kết quả phải hiển thị nhà cung cấp của bạn hoặc Cloudflare, không được là home router. Tài liệu Gluetun cũng cảnh báo rằng một số leak test có thể trả về kết quả bất thường, vì resolver bên trong namespace là một bộ trung gian caching local chứ không phải server thực sự trả lời cuối cùng. Hãy xem quốc gia không đúng hoặc resolver của chính ISP là dấu hiệu thực sự.
Thêm Tailscale cạnh VPN sidecar và dịch vụ nào sẽ được ưu tiên
Tailscale là một overlay network xây dựng trên WireGuard để truy cập các máy của bạn. Nhiều người chạy Tailscale cùng VPN của nhà cung cấp để giữ một đường quản trị vào stack. Hai thành phần này hiếm khi xung đột vì có một nguyên nhân cần hiểu rõ. Tài liệu của Tailscale nêu mặc định như sau: Tailscale hoạt động như một overlay network, 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 đó. Tailscale truy cập app tại
gluetun:8080, giống như mọi container bên ngoài khác. - Tailscale dùng chung namespace của gluetun với
network_mode: "service:gluetun": Tailscale cầncap_addcủanet_adminvànet_rawriêng, vì namespace không tự cấp capabilities. Trong chế độ userspace networking mặc định,TS_USERSPACEđược bật. tailscaled không tạo interface nào và hoạt động như một SOCKS5 hoặc HTTP proxy, nên không thể thay đổi routing. Gluetun vẫn xử lý toàn bộ lưu lượng. - Cấu hình tương tự nhưng có
TS_USERSPACE=false: tailscaled tạo tunnel device và cài đặt routes, nhưng chỉ áp dụng cho dải tailnet100.64.0.0/10cùng các subnet route bạn quảng bá bằngTS_ROUTES. Lưu lượng công khai vẫn đi ra qua gluetun. - Bất kỳ cấu hình nào ở trên khi chọn exit node,
sudo tailscale set --exit-node=<exit-node-ip>: Tailscale nhận default route và được ưu tiên. Không kết hợp cấu hình này với gluetun. Chỉ có một default route và một thành phần được quyền quản lý nó.
Nếu các route được quảng bá là mục đích chính, và bạn muốn truy cập toàn bộ private network phía sau máy thay vì chỉ truy cập chính máy đó, chạy Tailscale subnet router trên VPS sẽ hướng dẫn cách phê duyệt route, bật IP forwarding và dùng flag phía client 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 sẽ đặt HTTPS phía trước gluetun:8080 cho tailnet của bạn. Chỉ funnel mới mở URL đó ra Internet công khai.
Một tác động phụ sẽ thấy rõ khi Tailscale chạy bên trong tunnel. Các peer của Tailscale sẽ thấy địa chỉ của nhà cung cấp VPN, vì vậy Tailscale có thể chuyển sang relay thường xuyên hơn. Khi điều đó xảy ra, tailscale status hiển thị relay "..." cạnh một peer thay vì direct. 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 network, 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 gì sẽ lỗi và thông báo bạn sẽ thấy
Docker từ chối tạo app container. Error response from daemon: conflicting options: port publishing and the container type network mode nghĩa là một khối ports: vẫn đang nằm trên service được attach. Chuyển nó 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 app. curl: (6) Could not resolve host: qbittorrent là hành vi đúng, vì container được attach không tham gia network nào và không đăng ký tên nào. Dùng gluetun và port.
Container được attach thứ hai không khởi động. Hai process trong cùng một namespace không thể bind cùng một port. Process không bind được sẽ báo địa chỉ đang được sử dụng. Đổi internal port của app hoặc chạy thêm một gluetun.
App không có network sau khi bạn thao tác với gluetun. Restart hoặc recreate gluetun sẽ làm mất kết nối của mọi container được attach vào đó. Restart các container này.
Trang nhỏ tải được nhưng trang lớn bị treo. Đây là vấn đề MTU (maximum transmission unit). Tunnel thêm overhead, và 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. Giảm WIREGUARD_MTU, thử 1400, rồi 1320.
Gluetun không bao giờ chuyển sang trạng thái healthy. Kiểm tra startup nêu các nguyên nhân đầu tiên cần kiểm tra: 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 server list có lỗi thời không, rồi kiểm tra firewall trên host có chặn outbound UDP không.
FAQ
Vì sao các cổng đã publish của container ngừng hoạt động khi chạy sau Gluetun?
Vì network_mode: "service:gluetun" đặt container vào network namespace của gluetun, mà mỗi namespace chỉ có một địa chỉ IP và một tập cổng 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 cách nào để truy cập container bên trong VPN tunnel từ một container bên ngoài?
Dùng service name của gluetun và cổng mà app đang listening, ví dụ gluetun:8080. Container được gắn vào không có Docker network riêng, nên name của chính nó sẽ không resolve được. Không cần publish cổng cho lưu lượng giữa các container. Theo chiều ngược lại, trên Gluetun v3.41 trở lên, container bên trong namespace có thể truy cập container bên ngoài bằng service name, chẳng hạn postgres:5432. Client ở subnet khác, như laptop trong LAN, sẽ bị firewall của gluetun drop cho đến khi bạn thêm subnet đó vào FIREWALL_OUTBOUND_SUBNETS.
Gluetun có hoạt động như kill switch khi VPN bị ngắt không?
Có, vì 2 lý do cùng lúc. Container được gắn vào không có route nào ngoài route trong namespace dùng chung, nên khi tunnel bị ngắt, nó không còn đường ra khỏi máy. Firewall của Gluetun cũng chỉ cho phép outbound traffic đi qua tunnel và đến endpoint của VPN server. Gluetun sau đó tự khởi động lại VPN bên trong, 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ý outbound traffic?
Gluetun, trong mọi cấu hình trừ một trường hợp. Theo mặc định, Tailscale chỉ định tuyến traffic giữa các thiết bị trong tailnet và để nguyên public traffic. Ở 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, nó chỉ cài route cho 100.64.0.0/10 và các subnet được advertise. Ngoại lệ là exit node: sudo tailscale set --exit-node=<exit-node-ip> khiến Tailscale trở thành default route, nên Tailscale sẽ được ưu tiên. Chọn một sản phẩm để quản lý default route, thay vì xếp chồng cả hai.