Phần 34 đo DNS nội bộ theo chiều container gọi container. Phần 35 đo địa chỉ theo chiều ngược lại: máy ngoài gọi vào container. Bài này đóng nốt chiều thứ ba: container gọi ra máy chủ đang chạy Docker — tình huống rất thật khi ứng dụng trong container cần nối tới một database, một mock server hay một IDE debugger đang chạy ngay trên máy dev, ngoài container. host.docker.internal là cái tên hay được mách, nhưng đo thử mới thấy nó không đơn giản như tưởng: nó không phải là gateway của mạng bridge, và một cách "giả lập Linux" tưởng chừng hợp lý lại cho kết quả sai.
Môi trường đo
Docker Engine 29.7.2 trên Docker Desktop 4.87.0 (macOS 26.6), daemon chạy trong VM linuxkit. Máy chủ giả lập là một python3 -m http.server-kiểu handler nghe ở cổng 8765 ngay trên máy host, container gọi vào bằng alpine:3.20.
$ docker version --format '{{.Server.Version}} {{.Server.Os}}/{{.Server.Arch}}'
29.7.2 linux/arm64
Mặc định đã chạy được, không cần cấu hình gì thêm
$ docker run --rm alpine:3.20 sh -c "wget -qO- http://host.docker.internal:8765/"
pong-from-host
Không --add-host, không sửa /etc/hosts, không quan tâm container đang ở mạng bridge mặc định hay một mạng tự định nghĩa nào — host.docker.internal phân giải được ngay từ container đầu tiên. Đây là hành vi đặc thù của Docker Desktop, khác hẳn Docker Engine chạy trần trên Linux (xem phần cuối bài).
Địa chỉ thật đứng sau cái tên đó không phải là gateway của bridge
docker network inspect bridge cho gateway của mạng bridge mặc định:
$ docker network inspect bridge --format '{{(index .IPAM.Config 0).Gateway}}'
172.17.0.1
172.17.0.1 là địa chỉ hay được mách trong các hướng dẫn viết cho Linux thuần — "cứ gọi vào gateway của bridge là ra host". Thử đúng cách đó trên Docker Desktop:
$ docker run --rm alpine:3.20 sh -c "wget -qO- --timeout=3 http://172.17.0.1:8765/"
wget: can't connect to remote host (172.17.0.1): Connection refused
Bị từ chối thẳng. Hỏi DNS xem host.docker.internal thật ra là địa chỉ nào:
$ docker run --rm alpine:3.20 nslookup host.docker.internal
Server: 192.168.65.7
Address: 192.168.65.7#53
Non-authoritative answer:
Name: host.docker.internal
Address: 192.168.65.254
Name: host.docker.internal
Address: fdc4:f303:9324::254
resolv.conf bên trong container trỏ nameserver về 192.168.65.7 — đúng là DNS proxy nội bộ của Docker Desktop đã gặp ở phần 34, nằm trong VM linuxkit chứ không phải DNS thật của mạng. Proxy này trả về cả bản ghi A (192.168.65.254) lẫn AAAA (fdc4:f303:9324::254) cho host.docker.internal, và cả hai đều không nằm trong dải subnet của bridge (172.17.0.0/16). Đây là một IP ảo do tầng mạng của Docker Desktop tự cấp phát và định tuyến ra máy thật, hoàn toàn tách biệt khỏi khái niệm gateway của một Docker network cụ thể — kể cả đổi sang mạng tự định nghĩa khác (gateway 172.19.0.1 thay vì 172.17.0.1), host.docker.internal vẫn trỏ về đúng một địa chỉ đó, không đổi theo mạng.
Một chi tiết đo được nhưng không lý giải nổi: getent hosts host.docker.internal (chạy trên musl libc của Alpine) chỉ hiện đúng dòng AAAA, bỏ qua dòng A dù cùng resolver:
$ docker run --rm alpine:3.20 getent hosts host.docker.internal
fdc4:f303:9324::254 host.docker.internal host.docker.internal
Nhưng wget -S (verbose) lại cho thấy kết nối HTTP thật sự đi qua IPv4:
$ docker run --rm alpine:3.20 sh -c "wget -S -O- http://host.docker.internal:8765/ 2>&1 | head -3"
Connecting to host.docker.internal:8765 (192.168.65.254:8765)
HTTP/1.0 200 OK
Nói cách khác: hai công cụ đọc cùng một resolver ra hai kết quả hiển thị khác nhau, còn hành vi kết nối thật thì ổn định trên IPv4. Không đoán thêm ở đây — ghi lại đúng những gì đo được.
--add-host=host.docker.internal:host-gateway không đổi gì trên Desktop
Cờ host-gateway là cách chính thức để khai host.docker.internal một cách tường minh (bắt buộc trên Linux, xem phần dưới). Thử trên Desktop xem nó trỏ vào đâu:
$ docker run --rm --add-host=host.docker.internal:host-gateway alpine:3.20 cat /etc/hosts
...
192.168.65.254 host.docker.internal
fdc4:f303:9324::254 host.docker.internal
Đúng hai địa chỉ ảo đã thấy ở trên, không phải gateway thật của mạng container đang chạy. Trên Docker Desktop, từ khoá host-gateway được diễn giải lại thành "địa chỉ proxy nội bộ của Desktop" chứ không phải "gateway của Docker network hiện tại" — khác với ý nghĩa gốc của nó trên Linux.
Số đo sai đầu tiên: tưởng docker-in-docker giả lập được Linux thuần
Không có máy Linux server thật (không chạy Desktop) trong tay, ý tưởng ban đầu là dùng docker:27-dind — chạy hẳn một dockerd gốc bên trong container, không dính gì tới lớp Desktop — để xem container lồng bên trong có mất host.docker.internal mặc định hay không, giống hành vi Linux được tài liệu mô tả.
$ docker run -d --privileged --name dind-test docker:27-dind
$ docker exec dind-test docker version --format '{{.Server.Version}} {{.Server.Os}}/{{.Server.Arch}}'
27.5.1 linux/arm64
dockerd bên trong đúng là bản gốc, không phải Docker Desktop. Nhưng chạy thử:
$ docker exec dind-test docker run --rm alpine:3.20 getent hosts host.docker.internal
fdc4:f303:9324::254 host.docker.internal host.docker.internal
Vẫn ra y hệt IP ảo ở trên — kết quả không như kỳ vọng. Kiểm tra thẳng resolv.conf của chính container dind-test (không phải container lồng bên trong nó) thì lộ nguyên nhân:
$ docker exec dind-test cat /etc/resolv.conf
nameserver 192.168.65.7
dind-test tự nó cũng chỉ là một container chạy trên VM Desktop, nên nó nhận đúng resolv.conf đó từ daemon cha. dockerd bên trong nó, khi tạo container mới, kế thừa cấu hình DNS từ chính môi trường nó đang chạy — tức là vẫn dính Desktop, dù bản thân dockerd là bản Linux gốc. Nói cách khác: docker-in-docker cô lập được process và daemon, nhưng không cô lập được tầng mạng/DNS khỏi container cha. Cách này không dùng được để mô phỏng Linux Engine thuần — phần "trên Linux thật" ở dưới vì vậy dựa theo tài liệu chính thức của Docker (docs.docker.com), không phải số đo tự tay.
Chi phí phân giải tên: không đáng kể
$ docker run --rm alpine:3.20 sh -c '
i=0; while [ $i -lt 20 ]; do curl -s -o /dev/null -w "%{time_total}\n" http://host.docker.internal:8765/; i=$((i+1)); done \
| awk "{s+=\$1;n++} END{print \"trung binh:\", s/n*1000, \"ms\"}"'
trung binh: 0.99185 ms
$ docker run --rm alpine:3.20 sh -c '
i=0; while [ $i -lt 20 ]; do curl -s -o /dev/null -w "%{time_total}\n" http://192.168.65.254:8765/; i=$((i+1)); done \
| awk "{s+=\$1;n++} END{print \"trung binh:\", s/n*1000, \"ms\"}"'
trung binh: 0.6204 ms
Gọi qua tên chậm hơn gọi thẳng IP khoảng 0,37 ms mỗi request, trung bình trên 20 lần — chi phí phân giải DNS nội bộ, không đáng để tối ưu trong phần lớn ứng dụng thực tế.
Trên Linux thật và Windows: phần này trích tài liệu, không phải số đo
Không có máy Linux server (không phải Desktop) hay máy Windows trong tầm tay lúc viết bài này, và mục trên đã cho thấy dùng docker-in-docker để giả lập không đáng tin. Phần dưới đây lấy từ tài liệu chính thức tại docs.docker.com, ghi rõ để không lẫn với phần đã tự đo ở trên.
Theo tài liệu, Docker Engine chạy trên Linux không tự thêm host.docker.internal — đây vốn là tính năng riêng của Docker Desktop (macOS/Windows). Từ Docker Engine 20.10, có thể khai tường minh bằng đúng cờ đã thử ở trên, nhưng trên Linux nó mang nghĩa khác hẳn: host-gateway phân giải thành gateway thật của Docker network (kiểu 172.17.0.1 đã bị từ chối trên Desktop) — vì trên Linux không có lớp VM trung gian, gateway đó chính là địa chỉ của host trên interface docker0.
docker run --add-host=host.docker.internal:host-gateway ...
# docker-compose.yml, cần trên Linux — không cần trên Desktop
services:
app:
extra_hosts:
- "host.docker.internal:host-gateway"
Với Windows chạy Docker Desktop ở chế độ Linux containers (WSL2), theo tài liệu cùng một cơ chế Desktop như macOS nên host.docker.internal cũng có sẵn mặc định — nhưng đây cũng là điều chưa tự kiểm chứng, ghi lại đúng mức độ chắc chắn thật của thông tin. Windows containers mode (không dùng WSL2) có cơ chế mạng khác hẳn và nằm ngoài phạm vi bài này.
Khi nào dùng, khi nào không
host.docker.internal sinh ra cho máy phát triển: gọi ngược vào database, mock server hay debugger đang chạy ngoài container trên chính máy đó. Đừng đưa nó vào cấu hình chạy production — trên server Linux thật nó không tự có, phải khai extra_hosts cho từng service, và ý nghĩa của tên đó (gateway thật, có thể chạm tới các service khác trên cùng máy) khác hẳn với ý nghĩa "IP ảo riêng của Desktop" đã đo ở trên. Hai môi trường, hai hành vi — cấu hình chạy tốt trên máy dev không chắc chạy đúng khi lên Linux server nếu thiếu dòng extra_hosts.
Tổng kết
| Nền tảng | host.docker.internal mặc định |
host-gateway trỏ vào đâu |
Nguồn |
|---|---|---|---|
| macOS (Docker Desktop) | Có sẵn | IP ảo 192.168.65.254/fdc4:f303:9324::254, không đổi theo mạng |
Đo trực tiếp |
| Linux (Docker Engine, không Desktop) | Không có, phải khai --add-host/extra_hosts |
Gateway thật của Docker network (172.x.0.1) |
Tài liệu Docker |
| Windows (Docker Desktop, WSL2) | Có sẵn (cùng cơ chế Desktop) | Chưa kiểm chứng | Tài liệu Docker |
Thử ba mươi giây
Xem chính máy bạn đang trỏ host.docker.internal vào địa chỉ nào và so với gateway của mạng đang dùng — hai giá trị giống nhau là dấu hiệu bạn đang ở Linux Engine thuần (đã khai host-gateway), khác nhau là đang ở Docker Desktop:
docker run --rm alpine getent hosts host.docker.internal
docker network inspect bridge --format '{{(index .IPAM.Config 0).Gateway}}'
Bài viết liên quan
- DNS nội bộ Docker: container tìm nhau bằng tên thế nào, và cái bẫy IP cũ sau khi tái tạo
- 0.0.0.0 bị Docker Desktop khoá về loopback, nhưng trên Linux thật thì mở toang: đo publish, expose và bind địa chỉ
- host nhanh hơn bridge 27%, overlay chậm nhất và retransmit gấp 48 lần: đo bốn loại mạng Docker bằng iperf3