Docker Compose networking: network, DNS và port
Hiểu network mặc định, DNS bằng service name, khi nên dùng host mode, cách dùng chung network giữa project và vì sao port publish có thể bỏ qua UFW.
Docker Compose tạo gì trước khi ứng dụng khởi động
Mạng Docker Compose bắt đầu từ một quy tắc: docker compose up tạo một private network cho project, kết nối mọi service vào đó và cho phép các service này truy cập lẫn nhau bằng service name. Bạn không cần viết một dòng networks: nào để có được điều đó. Phần lớn nhầm lẫn về networking trong Compose xuất phát từ việc không biết rằng mạng mặc định đã được tạo sẵn.
Đây là một file nhỏ. Lưu file này thành compose.yaml trong một thư mục có tên shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleKhởi động project và xem Docker đã tạo gì:
docker compose up -d
docker network lsDanh sách hiện có một network tên là shop_default. Compose đặt tên cho network là <project>_default, còn project name mặc định là tên thư mục viết thường. Bạn có thể ghi đè bằng docker compose -p myproject up -d hoặc bằng name: myproject ở cấp cao nhất trong file. Driver của network là bridge, tức một virtual switch bên trong host. Mỗi container nhận một địa chỉ trên private subnet, còn traffic đi ra ngoài được chuyển đổi sang địa chỉ của host.
docker compose down xóa network đó. Đây là lý do một container cũ từ project trước có thể giữ network chưa bị xóa: Docker từ chối thao tác với error while removing network: network shop_default has active endpoints, và cách xử lý là stop hoặc remove container vẫn đang kết nối với network đó.
Nếu bạn chưa quen với Compose, hãy đọc trước bố cục file Compose và các lệnh quản lý vòng đời, vì các phần bên dưới giả định rằng bạn đã biết cách start và stop một project.
DNS theo tên service là phần người mới thường bỏ sót
Trên mọi network do người dùng định nghĩa, Docker chạy một DNS server tích hợp mà mỗi container có thể truy cập tại 127.0.0.11. DNS này phân giải tên service thành địa chỉ hiện tại của container. Vì vậy, web có thể kết nối đến database bằng hostname db trên port 5432 mà không cần cấu hình gì.
docker compose exec web getent hosts dbLệnh này in ra một dòng như 172.18.0.2 db. Nếu không in gì, hai service không nằm trên cùng một network.
Lỗi mà gần như ai cũng mắc một lần là dùng localhost trong cấu hình ứng dụng. Bên trong container, localhost là chính container đó, không phải host và cũng không phải service khác. Postgres client báo lỗi này khá rõ:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?Chuỗi kết nối phải là postgresql://postgres:example@db:5432/postgres. Phần host là tên service.
Có 2 chi tiết giúp tiết kiệm thời gian về sau. Tên được phân giải thành những gì đang chạy tại thời điểm hiện tại, vì vậy docker compose up -d --scale web=3 trả về một tên với 3 địa chỉ. Client cache DNS vĩnh viễn sẽ tự cố định vào một container đã chết. Ngoài ra, network legacy bridge được plain docker run sử dụng khi không có --network hoàn toàn không có khả năng phân giải tên. Vì vậy, các hướng dẫn về container link từ năm 2016 không khớp với những gì bạn thấy hiện nay.
Bạn không cần ports: để kết nối hai service
ports: publish một cổng của container trên host. Nó dành cho traffic đi từ bên ngoài Docker vào. Nó không liên quan đến traffic giữa các service; traffic này đã hoạt động trên toàn bộ dải cổng trong project network.
Vì vậy, ports: - "5432:5432" mà nhiều người thêm vào service database không mang lại lợi ích nào mà còn gây hại thực sự: nó expose Postgres trên interface public của server. Hãy xóa nó. Nếu cần truy cập database từ laptop để thực hiện migration, hãy bind cổng đó vào loopback bằng "127.0.0.1:5432:5432" rồi truy cập qua SSH tunnel. Sự khác nhau giữa listening socket, published port và firewall rule được trình bày trong cách port và service đang listen hoạt động trên Linux.
expose: chỉ có tác dụng ghi chú trong Compose. Nó không mở cổng nào, vì giữa các container trên cùng network không có gì bị đóng.
Khi nào nên dùng network_mode: host và chi phí phải trả
Host mode loại bỏ network namespace riêng của container, cho phép tiến trình sử dụng trực tiếp các interface của host.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityCó những lý do thực tế để dùng chế độ này. Một tiến trình cần nhận traffic broadcast hoặc multicast trên mạng cục bộ, chẳng hạn chức năng phát hiện thiết bị của media server hoặc hub tự động hóa gia đình, sẽ không thấy được traffic đó khi nằm sau bridge vì bridge không chuyển loại traffic này đến container. Monitoring agent đọc các bộ đếm trên interface của host cũng cần dùng interface của host. Ngoài ra, bạn bỏ qua một bước NAT, điều này có ý nghĩa khi packet rate cao.
Chi phí có những điểm cụ thể.
ports: không còn hoạt động. Docker cảnh báo rằng các port đã publish sẽ bị bỏ qua khi dùng host network mode, còn container sẽ bind vào bất kỳ port nào mà tiến trình của nó bind. Hai container dùng host mode cùng muốn dùng port 8080 sẽ xung đột, và container thứ hai sẽ dừng với lỗi bind: address already in use.
Phân giải tên bằng service name sẽ không còn hoạt động theo cả hai chiều. Container không nằm trên project network nên không phân giải được db, và các service khác cũng không phân giải được tên của nó. Container chỉ có thể kết nối đến các service đó thông qua những port đã publish trên host, thường là tại 127.0.0.1.
Tính cô lập không còn. Một tiến trình bind vào 0.0.0.0 bên trong container dùng host mode sẽ lắng nghe trên mọi interface của server, kể cả interface public, giống hệt một package được cài bằng apt. Điểm có lợi là traffic này đi qua luồng xử lý input thông thường, nên các rule của UFW vẫn áp dụng cho nó. Điều này không đúng với các port đã publish.
Host mode là tính năng của Linux Docker Engine. Docker Desktop chỉ hỗ trợ tính năng này từ version 4.34 trở lên và chỉ sau khi bạn bật nó. Ngoài ra, container không thể bind vào các địa chỉ IP của host và chỉ TCP và UDP được xử lý. Nếu một nửa team dùng Linux server còn nửa kia dùng Docker Desktop, hãy dự kiến cùng một file sẽ hoạt động khác nhau.
Hãy dùng host mode khi bạn cần interface của host. Đừng dùng nó để sửa một lỗi kết nối, vì cách này thường thay một vấn đề bằng một vấn đề khó xử lý hơn.
Kết nối hai project Compose bằng một network bên ngoài
Network do một project tạo ra không hiển thị với project khác. Vì vậy reverse proxy trong proxy/compose.yaml không thể thấy app trong app/compose.yaml, dù chúng chạy trên cùng một server. Cách khắc phục là dùng một network không thuộc quyền sở hữu của project nào.
Tạo network một lần bằng tay:
docker network create edgeSau đó khai báo network này là external trong từng project. Phía proxy:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: truePhía ứng dụng:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true cho Compose biết phải gắn vào network đã tồn tại thay vì tạo network mới, đồng thời giữ nguyên network đó trên docker compose down. Key name: riêng biệt quan trọng hơn bạn nghĩ: nếu không có key này, Compose sẽ tìm network có tên chính xác là edge. Khi có key này, bạn có thể đặt một tên cho network trong file và tên khác cho network trên host.
Nếu network không tồn tại, Compose sẽ từ chối khởi động và báo rằng network được khai báo là external nhưng không tìm thấy. Hãy tạo network trước.
Lưu ý cách file ứng dụng xử lý internal. Database chỉ nằm trên network riêng của project đó, nên proxy không thể truy cập database; chỉ app mới có thể truy cập. Thêm internal: true dưới một network sẽ hạn chế thêm và loại bỏ hoàn toàn route của network đó ra Internet. Đây là thiết lập mặc định phù hợp cho database, nhưng cần biết một điểm bất lợi trước khi áp dụng: container trên internal network không thể tải xuống bất kỳ thứ gì, vì vậy entrypoint chạy apt-get update hoặc pip install khi khởi động sẽ bị treo rồi fail do timeout.
Để xem cấu hình hoàn chỉnh có routing rule và certificate, hãy xem chạy nhiều app phía sau một instance Traefik.
Cổng đã publish bypass UFW
Đây là phần trong mạng Compose có thể dẫn đến sự cố bảo mật. Bạn publish một cổng, kiểm tra thấy UFW đang active và deny mọi thứ ngoài SSH, nhưng service vẫn truy cập được từ Internet.
sudo ufw status
curl http://203.0.113.10:8080UFW báo cổng đã bị block. curl vẫn trả về trang. Không có gì bị hỏng. Docker ghi trực tiếp các rule chuyển đổi địa chỉ và forwarding riêng vào iptables. Traffic đến cổng của container đã publish được forward đến container thay vì chuyển đến host, nên không đi qua chain mà UFW quản lý cho traffic gửi đến máy cục bộ. Các rule của Docker cũng được match trước rule của UFW.
Cách xử lý ngắn gọn là chỉ publish ở nơi cần thiết:
ports:
- "127.0.0.1:8080:80"Cấu hình này bind phía host vào loopback. Vì vậy, cổng chỉ truy cập được từ chính server và qua SSH tunnel, không truy cập được từ nơi khác. Hãy đặt entry point public phía sau reverse proxy, rồi cố ý publish các cổng 80 và 443. Phần giải thích đầy đủ, bao gồm chain DOCKER-USER cho trường hợp bắt buộc phải filter một cổng đã publish, có trong vì sao Docker publish cổng vượt qua UFW và cách xử lý.
Cách debug bằng 4 lệnh
Trước tiên, kiểm tra mỗi container thực sự đang ở network nào:
docker network inspect shop_defaultKhối Containers liệt kê mọi container đã được gắn vào network cùng với địa chỉ của chúng. Service không xuất hiện trong danh sách này đang ở network khác, đang dùng host mode hoặc chưa chạy.
Kiểm tra phân giải tên từ một container tạm thời được gắn vào cùng network. Bạn không cần cài thêm công cụ trong các image của mình:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432nslookup thất bại cho thấy có vấn đề với phân giải tên hoặc việc tham gia network. nslookup thành công nhưng nc thất bại nghĩa là service đang chạy nhưng không listening trên cổng đó, hoặc đang listening trên 127.0.0.1 bên trong container thay vì 0.0.0.0. Trường hợp sau thường gặp ở các development server. Cách sửa nằm ở bind address của ứng dụng, không phải trong Docker.
Có một lỗi khác trông giống như lỗi của Docker. Nếu các container có thể kết nối với nhau nhưng không thể truy cập một máy trong network văn phòng hoặc VPN, subnet của Docker có thể đang bị overlap với network đó. Docker mặc định cấp phát từ 172.17.0.0/16 trở lên. Hãy chuyển pool trong /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Sau đó chạy sudo systemctl restart docker và tạo lại các network bị ảnh hưởng, vì network hiện có vẫn giữ subnet được gán khi nó được tạo.
FAQ
Vì sao các container không thể kết nối với nhau bằng tên service?
Chúng không nằm trên cùng một network. Compose tự động đặt mọi service vào <project>_default, nhưng ngay khi bạn thêm danh sách networks: vào một service, danh sách đó trở thành toàn bộ network của service và network mặc định không còn được ngầm định. Chạy docker network inspect <network> và kiểm tra xem cả hai container có xuất hiện trong khối Containers hay không. Đồng thời kiểm tra để chắc chắn không service nào dùng network_mode: host, vì container chạy ở host mode không nằm trên Docker network nào và không thể phân giải tên service.
Có cần publish port để một service kết nối đến service khác không?
Không. Trên một Compose network, mọi port của mọi container đều có thể được các container khác trên network đó truy cập. ports: chỉ dùng để expose container cho traffic từ bên ngoài Docker, còn expose: chỉ mang tính tài liệu. Publish port của database là một thói quen phổ biến nhưng gây rủi ro và chi phí không cần thiết, vì nó đưa database ra interface public của server.
Bridge và host networking khác nhau như thế nào?
Bridge cấp cho container một network namespace và một địa chỉ riêng trên virtual switch, kèm khả năng phân giải tên tự động giữa các container và cơ chế chuyển đổi traffic đi ra ngoài. Host cho container dùng trực tiếp network stack của host: không có địa chỉ riêng, không phân giải được bằng tên service, không cần publish port và không có isolation với các listener khác trên host. Bridge là mặc định và là lựa chọn phù hợp, trừ khi process cần dùng các interface của host.
Làm cách nào để kết nối các container từ hai file Compose khác nhau?
Tạo một network dùng chung bằng docker network create edge, sau đó khai báo network đó trong cả hai file bằng external: true và attach các service cần kết nối vào network. Compose sẽ không tạo hoặc xóa network này. Nếu bỏ qua bước tạo, Compose sẽ từ chối khởi động và báo network được khai báo là external nhưng không tìm thấy.
Vì sao container vẫn có thể truy cập từ Internet khi UFW chặn port?
Vì port đã publish được xử lý bằng các rule forwarding mà Docker thêm vào iptables. Các rule đó được match trước rule của UFW, và forwarded traffic cũng không đi qua chain mà UFW lọc. Hãy bind phía host vào loopback bằng "127.0.0.1:8080:80" và đặt mọi dịch vụ public phía sau reverse proxy trên các port 80 và 443.