Container cần gọi ngược về một dịch vụ chạy trên máy chủ — CSDL cài trực tiếp, một API đang chạy trong IDE, một tiến trình debug. Cái tên host.docker.internal sinh ra cho việc đó, và nó hành xử khác nhau tuỳ nền tảng theo cách dễ làm bạn mất buổi chiều.

Tôi dựng hai dịch vụ trên máy chủ, một bind 0.0.0.0, một bind 127.0.0.1, mỗi máy trả về một chuỗi tự nhận dạng để không nhầm lẫn được.

Docker Desktop trên macOS

host.docker.internal    -> 192.168.65.254
gateway.docker.internal -> 192.168.65.1
gateway mac dinh cua container -> 172.17.0.1
/etc/hosts: (khong co dong nao)

Cái tên có sẵn, không cần cờ gì, và nó đến từ DNS nhúng chứ không phải /etc/hosts.

Gọi tới Kết quả
host.docker.internal:18190 (bind 0.0.0.0) TOI-LA-MACOS [200]
host.docker.internal:18191 (bind 127.0.0.1) TOI-LA-MACOS [200]
192.168.65.254:18190 TOI-LA-MACOS [200]
172.17.0.1:18190 (gateway mặc định) [000] không kết nối được

Hai dòng đáng chú ý. Thứ nhất: dịch vụ bind 127.0.0.1 vẫn gọi được. Docker Desktop chạy trong máy ảo và có một lớp chuyển tiếp về loopback của macOS, nên "chỉ nghe localhost" không còn là hàng rào. Thứ hai: gateway mặc định của container (172.17.0.1) không dùng được — đó là địa chỉ trong máy ảo Linux, không phải máy Mac của bạn.

--add-host=x:host-gateway trên Desktop cũng trỏ về địa chỉ đặc biệt đó chứ không phải docker0:

192.168.65.254        x
fdc4:f303:9324::254   x

Daemon Linux

Trên Linux, Docker không tự cung cấp cái tên này. Bạn phải khai:

docker run --add-host=host.docker.internal:host-gateway ...

Khi đó nó trỏ vào chính gateway của cầu Docker:

host.docker.internal -> 172.18.0.1
gateway mac dinh     -> 172.18.0.1     (cung mot dia chi)
/etc/hosts: 172.18.0.1  host.docker.internal

Khác hẳn Desktop: ở đây nó /etc/hosts, và nó gateway.

Gọi tới Kết quả
host.docker.internal:18190 (bind 0.0.0.0) TOI-LA-MAY-LINUX [200]
host.docker.internal:18191 (bind 127.0.0.1) [000] không kết nối được
172.18.0.1:18190 (gọi thẳng gateway) TOI-LA-MAY-LINUX [200]

Đây là khác biệt quan trọng nhất của cả bài. Dịch vụ bind 127.0.0.1 trên Linux thì container không với tới được, vì gói tin đến từ mạng cầu 172.18.0.0/16 chứ không phải từ loopback.

Nghĩa là chuỗi sự kiện quen thuộc: bạn viết code trên macOS, mọi thứ chạy ngon; đẩy lên máy chủ Linux, container báo "connection refused" tới CSDL — vì trên máy chủ, PostgreSQL nghe listen_addresses = localhost. Cách sửa là cho nó nghe cả địa chỉ cầu Docker, và chỉ địa chỉ đó, đừng mở ra 0.0.0.0 (xem phần 35 về việc tường lửa không cứu bạn ở chỗ này).

Trên Linux, gọi thẳng 172.18.0.1 cũng chạy. Nhưng địa chỉ đó đổi theo từng mạng Docker, nên dùng tên vẫn tốt hơn — chỉ cần nhớ luôn kèm cờ.

Windows

Tôi không có máy Windows nên không đo được. Tài liệu nói Docker Desktop trên Windows hành xử như trên macOS, nhưng đó là điều tôi đọc chứ không phải điều tôi đo, nên tôi để nguyên như vậy.

Trường hợp nó gọi nhầm sang máy khác

Đây là phần thú vị nhất, và tôi vấp phải nó ngay trong lúc dựng phép đo.

Phòng thí nghiệm Linux của tôi là một daemon Docker chạy lồng trong Docker Desktop. Khi tôi chạy container không kèm cờ gì rồi gọi host.docker.internal:18190, nó trả về:

TOI-LA-MACOS [ma 200]

phân giải được, kết nối được, trả về 200 — và đi tới hoàn toàn sai máy. Nếu cả hai máy cùng chạy một dịch vụ giống nhau, tôi sẽ chẳng bao giờ phát hiện ra.

Nguyên nhân: container lồng bên trong thừa hưởng resolver 192.168.65.7 của Docker Desktop, và resolver đó vui vẻ trả lời host.docker.internal bằng địa chỉ của máy Mac. Cái tên đúng, câu trả lời sai ngữ cảnh.

Dấu hiệu để nhận ra: /etc/hosts không có dòng nào. Trên Linux đúng chuẩn, cái tên này chỉ tồn tại nhờ --add-host, tức là nhờ /etc/hosts. Nếu nó phân giải được mà /etc/hosts trống, câu trả lời đang đến từ một nơi bạn không kiểm soát.

docker run --rm alpine sh -c \
  'grep docker.internal /etc/hosts || echo "khong co trong /etc/hosts"'

Bài học chung: hai điểm cuối trả lời giống nhau không có nghĩa là bạn đang nói chuyện với đúng điểm cuối. Cho mỗi máy chủ trả về một chuỗi tự nhận dạng là mẹo rẻ nhất tôi biết để bắt lỗi loại này.

Viết sao cho chạy được ở cả hai nơi

Khai cờ trong Compose. Trên Linux nó tạo bản ghi cần thiết; trên Desktop nó thừa nhưng vô hại:

services:
  app:
    image: vi-du:1.0
    extra_hosts:
      - "host.docker.internal:host-gateway"

Và ba điều cần nhớ:

  • Dịch vụ trên máy chủ phải nghe địa chỉ cầu Docker, không chỉ 127.0.0.1 — nếu không thì chạy trên macOS mà hỏng trên Linux.
  • Đừng dùng cách này cho việc thật. Nếu hai thứ cần nói chuyện lâu dài, cho cả hai vào chung một mạng Docker và gọi theo tên dịch vụ (phần 34). host.docker.internal là công cụ cho môi trường phát triển.
  • Nó là một lỗ thủng trong cách ly. Container gọi được vào máy chủ nghĩa là container gọi được vào mọi thứ máy chủ đang nghe.

Thử ba mươi giây

python3 -m http.server 18190 --bind 127.0.0.1 &
docker run --rm --add-host=host.docker.internal:host-gateway \
  curlimages/curl:8.10.1 -s -o /dev/null -w '%{http_code}\n' \
  --max-time 3 http://host.docker.internal:18190/

Ra 200 là bạn đang ở Docker Desktop. Ra 000 là bạn ở Linux — và đó là hành vi đúng, đổi --bind 127.0.0.1 thành địa chỉ cầu Docker là chạy.

Phần sau dựng nhiều mạng cùng lúc và đo bảng "cái gì gọi được cái gì".