SSD Nodes Learn 🎉 VPS từ $4.99/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

Docker qua VPN: vì sao cổng bị mất với Gluetun

Gắn container vào Gluetun bằng network_mode: service làm mất port publish và service name. Xem lỗi Docker cụ thể và file Compose đúng để sửa.

Vì sao các cổng biến mất khi định tuyến container Docker qua VPN

Để định tuyến container Docker 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 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 bằng tên của VPN container.

Nếu để lại 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 mode

Công cụ được dùng ở đây là Gluetun. Đây là container kết nối đến nhà cung cấp VPN (virtual private network) thương mại qua WireGuard hoặc OpenVPN và tự quản lý firewall. Bản release v3.41.3 là bản hiện tại tính đến tháng 8 năm 2026. Các ví dụ dùng Mullvad với WireGuard, nên bạn cần tài khoản và key từ nhà cung cấp. Nếu muốn kết thúc tunnel trên phần cứng do bạn sở hữu, chạy WireGuard server riêng trên một VPS sẽ dựng đầu còn lại, còn wg-easy trong Docker cung cấp web interface cho phần đó.

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 app không bao giờ được đăng ký và không thể resolve. Các container khác phải dùng gluetun.
  • Các container trong cùng namespace kết nối 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 khắc phục.
  • Capability thuộc về container, không thuộc về namespace. Gluetun giữ NET_ADMIN/dev/net/tun vì nó tạo tunnel interface. Container được gắn vào không kế thừa các capability này.
  • Compose từ chối mọi file trong đó một service đồng thời đặt cả network_modenetworks. 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à đó 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ó.

File 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-stopped

Tag :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ủa nhánh master, tức phiên bản đang phát triển, vì vậy hãy ghim :v3 trên máy mà bạn không muốn phải debug vào một ngày bất kỳ.

WEBUI_PORT=8080 phải khớp với cổng được publish, vì qBittorrent bind bên trong namespace của gluetun và rule publish chuyển 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 trả lời. 127.0.0.1:8080:8080 giữ web interface chỉ trên địa chỉ loopback của host. Một 8080:8080 không có địa chỉ cụ thể sẽ publish trên mọi interface và tự thêm rule vào firewall. Đây là lý do các cổng được Docker publish có thể đi thẳng qua ufw.

Khởi động stack, sau đó kiểm tra theo thứ tự sau:

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

docker compose ps phải hiển thị gluetun ở trạng thái healthy và qbittorrent ở trạng thái running. Tiếp theo, xác nhận địa chỉ exit 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à mọi phần bên dưới sẽ không hoạt động như mô tả.

Không để key trong 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/32

Cả hai giá trị đều lấy từ file cấu hình WireGuard mà bạn tạo trong khu vực tài khoản của nhà cung cấp. Đặt file về mode 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 toàn bộ biến môi trường cho bất kỳ ai có thể truy cập Docker socket. File môi trường và secrets 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 chiều đều hoạt động và mỗi chiều 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 Docker Compose kết nối các network 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, 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 khai báo ports:, vì traffic giữa các container ở lại 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, dùng service name của container còn lại, ví dụ postgres:5432. Gluetun đã có thể phân giải 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 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. Client trên subnet khác, laptop trong LAN của bạn hoặc container trên 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 ghi trong tài liệu là chính xác: đây là danh sách 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 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ừ nhà cung cấp VPN, sau đó khai báo cổng đó trong FIREWALL_VPN_INPUT_PORTS. Thiết lập này cho phép các cổng 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 chưa cấu hình đúng.

Cơ chế kill switch: điều gì xảy ra khi tunnel bị ngắt

Mẫu này cho thấy sự phức tạp của nó khi có lỗi. Container được gắn vào không có route thứ hai. Đường duy nhất để đi ra khỏ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 áp dụng cùng quy tắc từ phía ngược lại: lưu lượng đi ra 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ò rỉ qua interface thường trong lúc client kết nối lại.

Gluetun tự theo dõi kết nối của mình. 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 kết nối này fail, 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 timeout

Hãy đọc log của container được gắn vào theo thứ tự đó. Các dòng như connection refused, operation not permittedi/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 chỉ thấy 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ự: ngăn stack khởi động trước khi tunnel sẵn sàng

Image có sẵn một Docker healthcheck:

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

Lệnh đó chạy một bản gluetun thứ hai trong thời gian ngắn. Bản này truy vấn health server của bản đ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 cùng 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 dùng để chờ trạng thái đó. 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 lúc handshake hoàn tất. Vì vậy app có thể 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 chỉ đánh giá điều kiện đó một lần, khi tạo container. Nó không dừng hoặc khởi động lại app về 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 khởi động lại tiến trình VPN thay vì container.

Kiểm tra rò rỉ DNS trước khi tin tưởng 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=dotDNS_UPSTREAM_RESOLVERS=cloudflare. Giữ nguyên cả hai tùy chọn này để các truy vấn được mã hóa và đi qua tunnel.

Thiết lập gây ra lỗi này là DNS_UPSTREAM_PLAIN_ADDRESSES. Người dùng thường bật tùy chọn này khi một hostname không resolve được và muốn router hoặc resolver của nhà cung cấp dịch vụ trả lời thay. Tài liệu Gluetun nêu rõ hậu quả: toàn bộ traffic DNS sẽ không đi qua VPN tunnel mà bị rò rỉ ra ngoài. Traffic của bạn vẫn riêng tư. Danh sách hostname bạn truy vấn thì không. Phiên bản WireGuard của cùng lỗi này được đề cập trong DNS ngừng resolve qua tunnel WireGuard.

Để kiểm tra, hãy đặt HTTPPROXY=on trên gluetun và publish 8888:8888/tcp, sau đó trỏ trình duyệt đến proxy đó rồi mở một trang kiểm tra rò rỉ DNS. Kết quả phải hiển thị nhà cung cấp dịch vụ của bạn hoặc Cloudflare, không bao giờ là router tại 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ể hiển thị kết quả bất thường, vì resolver bên trong namespace là một intermediary caching cục bộ chứ không phải server trả lời cuối cùng. Hãy xem quốc gia bị nhận diện sai hoặc resolver của chính ISP của bạn là dấu hiệu rò rỉ 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 mạng overlay 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 với VPN của nhà cung cấp để giữ một đường truy cập 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 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 cộng.

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ần cap_add riêng của net_adminnet_raw, vì các capability không được chia sẻ cùng namespace. Trong chế độ userspace networking mặc định, TS_USERSPACE được bật, tailscaled không tạo interface nào và hoạt động dưới dạng SOCKS5 hoặc HTTP proxy, nên không thể thay đổi định tuyến. 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 route, nhưng chỉ áp dụng cho dải tailnet 100.64.0.0/10 cùng các subnet route bạn quảng bá bằng TS_ROUTES. Lưu lượng công cộng vẫn đi 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 giành 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ó.

Một tác động phụ xuất hiện 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. tailscale status hiển thị relay "..." cạnh một peer thay vì direct khi tình huống 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 gì sẽ hỏng 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 có nghĩa là một block ports: vẫn đang gắn với 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_modenetworks. Đặ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 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 được gắn thứ hai không khởi động. 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ỉ đã được sử dụng. Đổi port nội bộ của app hoặc chạy thêm một gluetun.

App không còn network sau khi bạn thay đổi gluetun. Việc restart hoặc recreate gluetun sẽ 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 thêm overhead, khiế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 về. Giảm WIREGUARD_MTU, thử 1400, sau đó thử 1320.

Gluetun không bao giờ chuyển sang trạng thái healthy. Kiểm tra lúc khởi động chỉ ra 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 xem key đã hết hạn chưa, sau đó kiểm tra danh sách server có quá cũ 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 không còn hoạt động khi chạy phía sau Gluetun?

network_mode: "service:gluetun" đặt container vào network namespace của gluetun. Một namespace chỉ có một địa chỉ IP và một bộ cổng listening. Ứng dụng 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 attach, 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 một container bên trong VPN tunnel từ container bên ngoài tunnel?

Dùng service name của gluetun và cổng mà ứng dụng đang listening, ví dụ gluetun:8080. Container được attach không có Docker network riêng, nên name của chính nó sẽ không bao giờ resolve được. Không cần publish cổng cho traffic 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 thuộc 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 attach không có route nào ngoài route trong namespace dùng chung. Vì vậy, khi tunnel bị ngắt, container 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 sẽ 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 attach 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à không ảnh hưởng đế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, Tailscale 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. Hãy chọn một product làm chủ default route thay vì xếp chồng cả hai.