SSD Nodes Learn 8GB RAM — $66/năm
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-01

Hướng dẫn cấu hình Docker Compose networking chi tiết

Tìm hiểu cơ chế bridge mặc định, DNS theo tên service và cách chia sẻ network giữa các dự án. Giải thích lý do port publish bỏ qua UFW và khi nào nên dùng host mode.

Những gì Compose xây dựng trước khi ứng dụng của bạn khởi chạy

Mạng Docker Compose bắt đầu với một quy tắc: docker compose up tạo ra một mạng riêng cho dự án, gắn mọi service vào đó và cho phép các service này kết nối với nhau thông qua tên service. Bạn không cần viết một dòng networks: nào để có được điều đó. Phần lớn sự nhầm lẫn về mạng trong Compose xuất phát từ việc không biết rằng mặc định đó đã tồn tại sẵn.

Dưới đây là một tệp nhỏ. Hãy lưu nó dưới tên 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: example

Khởi chạy nó và xem những gì Docker đã tạo ra:

docker compose up -d
docker network ls

Danh sách hiện chứa một mạng có tên shop_default. Compose đặt tên nó là <project>_default, và tên dự án mặc định là tên thư mục viết thường. Bạn có thể ghi đè nó bằng docker compose -p myproject up -d hoặc bằng một mục name: myproject ở cấp cao nhất trong tệp. Driver của nó là bridge, đây là một switch ảo bên trong host. Mỗi container nhận một địa chỉ trên một subnet riêng, và traffic đi ra ngoài được chuyển đổi sang địa chỉ của host khi rời khỏi mạng.

docker compose down sẽ xóa mạng đó. Đây là lý do tại sao một container cũ từ một dự án cũ có thể giữ mạng đó mở: Docker sẽ từ chối với lỗi error while removing network: network shop_default has active endpoints, và cách khắc phục là dừng hoặc xóa container vẫn còn đang gắn với mạng đó.

Nếu bạn mới làm quen với Compose, bố cục tệp Compose và các lệnh vòng đời là tài liệu đáng đọc trước tiên, vì mọi nội dung bên dưới đều giả định rằng bạn đã biết cách khởi động và dừng một dự án.

DNS theo tên dịch vụ là phần người mới bắt đầu thường bỏ lỡ

Trên bất kỳ mạng do người dùng định nghĩa nào, Docker đều chạy một máy chủ DNS nhúng mà mỗi container có thể thấy tại 127.0.0.11. Nó phân giải tên dịch vụ thành địa chỉ của container hiện tại. Vì vậy, web kết nối tới cơ sở dữ liệu tại hostname db, trên cổng 5432, mà không cần bất kỳ cấu hình nào.

docker compose exec web getent hosts db

Lệnh đó in ra một dòng như 172.18.0.2 db. Nếu nó không in ra gì cả, hai dịch vụ đó không nằm trên cùng một mạng.

Sai lầm mà hầu như ai cũng mắc phải một lần là sử dụng localhost trong cấu hình ứng dụng. Bên trong một container, localhost chính là container đó, không phải là host và cũng không phải là dịch vụ còn lại. Các client Postgres báo lỗi này rất rõ ràng:

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 chính là tên dịch vụ.

Hai chi tiết giúp tiết kiệm thời gian về sau. Tên được phân giải thành bất cứ thứ gì đang chạy tại thời điểm đó, vì vậy docker compose up -d --scale web=3 cung cấp một tên với ba địa chỉ, và một client lưu cache DNS vĩnh viễn sẽ tự ghim chính nó vào một container đã chết. Và mạng bridge cũ được sử dụng bởi một lệnh docker run đơn thuần không có --network hoàn toàn không có khả năng phân giải tên, đó là lý do tại sao các lời khuyên về container links 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 dịch vụ

ports: xuất bản một port của container ra host. Nó dành cho lưu lượng truy cập từ bên ngoài Docker. Nó không liên quan gì đến lưu lượng giữa các dịch vụ với nhau, vốn đã hoạt động trên toàn bộ dải port trong mạng của dự án.

Vì vậy, ports: - "5432:5432" mà nhiều người thêm vào dịch vụ cơ sở dữ liệu của họ không có tác dụng gì mà còn gây hại thực sự: nó làm lộ Postgres trên giao diện public của server. Hãy xóa nó đi. Nếu bạn muốn truy cập nó từ laptop để thực hiện migration, hãy bind nó vào loopback bằng "127.0.0.1:5432:5432" và truy cập thông qua SSH tunnel. Sự khác biệt giữa một listening socket, một published port và một firewall rule đã được đề cập trong cách thức hoạt động của các port và dịch vụ đang lắng nghe trên Linux.

expose: chỉ mang tính chất tài liệu trong Compose. Nó không mở bất cứ thứ gì, vì không có gì bị chặn giữa các container nằm trên cùng một mạng.

Khi nào network_mode host là lựa chọn phù hợp và cái giá phải trả

Chế độ host loại bỏ network namespace riêng của container và 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 infinity

Có những lý do thực tế để cần đến cấu hình này. Một tiến trình cần nhìn thấy lưu lượng broadcast hoặc multicast trên mạng nội bộ, ví dụ như tính năng phát hiện thiết bị cho media server hoặc hub nhà thông minh, sẽ không thể thấy được nếu nằm sau bridge, vì bridge không chuyển tiếp lưu lượng đó vào container. Một monitoring agent đọc các bộ đếm interface của host cũng cần truy cập trực tiếp vào các interface của host. Ngoài ra, bạn còn bỏ qua được bước chuyển đổi địa chỉ, điều này rất quan trọng khi lưu lượng gói tin ở mức cao.

Các cái giá phải trả rất cụ thể.

ports: sẽ ngừng hoạt động. Docker cảnh báo rằng các port được publish sẽ bị loại bỏ khi sử dụng network mode host, và container sẽ bind vào bất cứ port nào mà tiến trình của nó bind. Hai container chạy ở chế độ host cùng muốn sử dụng port 8080 sẽ xung đột, và container thứ hai sẽ chết với lỗi bind: address already in use.

Khả năng phân giải tên theo service name sẽ mất đi ở cả hai chiều. Container không nằm trên project network, nên nó không thể phân giải db, và các service khác cũng không thể phân giải nó. Nó chỉ có thể kết nối tới các service khác thông qua các port được publish trên host, thường là tại 127.0.0.1.

Tính cô lập không còn nữa. Một tiến trình bind vào 0.0.0.0 bên trong một container chạy chế độ host sẽ lắng nghe trên mọi interface của server, bao gồm cả interface công cộng, giống hệt như một gói phần mềm được cài đặt bằng apt. Có một điểm lợi ở đây: lưu lượng này tuân theo đường dẫn input thông thường, nên các quy tắc UFW sẽ áp dụng cho nó, điều này không đúng với các port được publish.

Chế độ host là một tính năng của Linux Docker Engine. Docker Desktop chỉ hỗ trợ tính năng này từ phiên bản 4.34 trở đi và chỉ sau khi bạn kích hoạt nó, với các giới hạn bổ sung là container không thể bind vào các địa chỉ IP của host và chỉ hỗ trợ TCP và UDP. Nếu một nửa đội ngũ của bạn dùng server Linux và nửa còn lại dùng Docker Desktop, hãy chuẩn bị tinh thần cho việc cùng một file cấu hình sẽ hoạt động khác nhau.

Hãy sử dụng chế độ host khi bạn thực sự cần các interface của host. Đừng sử dụng nó để sửa lỗi kết nối, vì nó thường thay thế một vấn đề bằng một vấn đề khác khó giải quyết hơn.

Kết nối hai dự án Compose bằng một network bên ngoài

Một network được tạo bởi một dự án sẽ không hiển thị với dự án khác. Đó là lý do tại sao reverse proxy trong proxy/compose.yaml không thể nhìn thấy ứng dụng trong app/compose.yaml, ngay cả khi chúng nằm trên cùng một server. Giải pháp là sử dụng một network không thuộc sở hữu của dự án nào cả.

Tạo network đó một lần bằng tay:

docker network create edge

Sau đó khai báo nó là external trong mỗi dự án. 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: true

Phí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 yêu cầu Compose kết nối vào một network đã tồn tại thay vì tạo mới, và giữ nguyên nó khi chạy docker compose down. Khóa name: riêng biệt quan trọng hơn vẻ ngoài của nó: nếu không có nó, Compose sẽ tìm một network có tên chính xác là edge, còn với nó, bạn có thể đặt tên network trong file một kiểu và tên trên host một kiểu khác.

Nếu network không tồn tại, Compose sẽ từ chối khởi động và báo lỗi rằng network được khai báo là external nhưng không tìm thấy. Hãy tạo nó trước.

Lưu ý những gì file ứng dụng thực hiện với internal. Database chỉ nằm trên network cục bộ của dự án đó, vì vậy proxy không thể truy cập nó và chỉ có app mới làm được. Thêm internal: true vào dưới một network sẽ tiến xa hơn và loại bỏ hoàn toàn tuyến đường của nó ra thế giới bên ngoài. Đây là cấu hình mặc định tốt cho database, với một lưu ý cần biết trước khi thiết lập: container trên một network nội bộ không thể tải xuống bất cứ thứ gì, vì vậy entrypoint chạy apt-get update hoặc pip install khi khởi động sẽ bị treo và sau đó thất bại do timeout.

Để xem thiết lập hoàn chỉnh với các quy tắc định tuyến và chứng chỉ, hãy xem chạy nhiều ứng dụng phía sau một instance Traefik.

Các port được publish bỏ qua UFW

Đây là phần trong mạng của Compose dễ dẫn đến sự cố bảo mật. Bạn publish một port, bạn kiểm tra thấy UFW đang hoạt động và từ chối mọi kết nối ngoại trừ SSH, nhưng dịch vụ vẫn có thể truy cập được từ internet.

sudo ufw status
curl http://203.0.113.10:8080

UFW báo rằng port đã bị chặn. curl vẫn trả về trang web như thường. Không có gì bị hỏng cả. Docker tự ghi các quy tắc chuyển đổi địa chỉ và chuyển tiếp trực tiếp vào iptables, và lưu lượng truy cập đến port container đã publish được chuyển tiếp thẳng đến container thay vì gửi đến host, vì vậy nó không bao giờ đi qua chuỗi (chain) mà UFW quản lý cho lưu lượng truy cập đến máy chủ. Các quy tắc của Docker cũng được khớp trước các quy tắc của UFW.

Cách khắc phục nhanh là chỉ publish ở nơi bạn cần:

    ports:
      - "127.0.0.1:8080:80"

Lệnh đó gắn phía host vào loopback, vì vậy port chỉ có thể truy cập được từ chính máy chủ, thông qua SSH tunnel, và không thể truy cập từ bất kỳ đâu khác. Hãy đặt điểm truy cập công khai phía sau một reverse proxy, nơi publish port 80 và 443 một cách có chủ đích. Giải thích đầy đủ, bao gồm cả chuỗi DOCKER-USER cho các trường hợp bạn bắt buộc phải lọc một port đã publish, nằm tại tại sao Docker publish bỏ qua UFW và cách khắc phục.

Cách debug trong bốn lệnh

Bắt đầu bằng việc kiểm tra xem mỗi container thực sự đang nằm trên network nào:

docker network inspect shop_default

Khối Containers liệt kê mọi container được gắn kèm cùng địa chỉ của chúng. Một service không có trong danh sách này đang nằm trên một network khác, ở chế độ host, hoặc không chạy.

Kiểm tra phân giải tên miền từ một container dùng một lần được gắn vào cùng network, để bạn không cần cài thêm công cụ vào image của chính mình:

docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432

nslookup thất bại cho thấy lỗi phân giải tên miền hoặc lỗi thành viên network. nslookup thành công trong khi nc thất bại nghĩa là service đang chạy nhưng không lắng nghe trên port đó, hoặc đang lắng nghe trên 127.0.0.1 bên trong container thay vì 0.0.0.0. Trường hợp cuối rất phổ biến với các development server, và cách sửa nằm ở bind address của ứng dụng, không phải ở Docker.

Một lỗi khác trông giống như lỗi của Docker. Nếu các container có thể giao tiếp với nhau nhưng không thể kết nối tới một máy trong văn phòng hoặc network VPN của bạn, có khả năng subnet của Docker bị trùng với network đó. Theo mặc định, Docker cấp phát từ 172.17.0.0/16 trở lên. Hãy di 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ì một network hiện có sẽ giữ nguyên subnet mà nó được tạo ra ban đầu.

FAQ

Tại sao các container của tôi không thể kết nối với nhau thông qua tên dịch vụ?

Chúng không nằm trên cùng một network. Compose tự động đưa 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 đó sẽ trở thành tập hợp network duy nhất cho service đó và network mặc định sẽ không còn được áp dụng. Hãy 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. Ngoài ra, hãy kiểm tra xem không có service nào sử dụng network_mode: host, vì container ở chế độ host không nằm trên bất kỳ Docker network nào và không thể phân giải tên dịch vụ.

Tôi có cần publish port để một service kết nối với service khác không?

Không. Trên một network của Compose, mọi port của mọi container đều có thể truy cập được bởi các container khác trên cùng network đó. ports: chỉ tồn tại để cho phép container nhận traffic từ bên ngoài Docker, và expose: chỉ mang tính chất tài liệu. Việc publish port của database là một thói quen phổ biến nhưng gây tốn kém, vì nó đưa database lên giao diện công cộng của server.

Sự khác biệt giữa bridge và host networking là gì?

Bridge cung cấp cho container một network namespace và địa chỉ riêng trên một switch ảo, với khả năng phân giải tên tự động giữa các container và chuyển đổi traffic outbound. Host cung cấp cho container quyền truy cập trực tiếp vào network stack của host: không có địa chỉ riêng, không phân giải theo tên dịch vụ, không cần publish port và không có sự cô lập với các tiến trình đang lắng nghe khác trên host. Bridge là mặc định và là lựa chọn phù hợp trừ khi tiến trình cần sử dụng các interface của host.

Làm thế 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 nó trong cả hai file với external: true và gắn các service cần giao tiếp vào đó. Compose sẽ không tự tạo hoặc xóa network này. Nếu bạn bỏ qua bước tạo, Compose sẽ từ chối khởi động và báo lỗi rằng network được khai báo là external nhưng không tìm thấy.

Tại sao container của tôi có thể truy cập được từ internet trong khi UFW đã chặn port đó?

Vì một port được publish sẽ được xử lý bởi các quy tắc forwarding mà Docker thêm vào iptables. Các quy tắc đó được khớp trước các quy tắc của UFW, và traffic được forward dù sao cũng không đi qua chuỗi lọc của UFW. Hãy bind phía host vào loopback bằng "127.0.0.1:8080:80" và đặt bất kỳ dịch vụ công cộng nào phía sau một reverse proxy trên port 80 và 443.