Từ sự cố “Connection Refused” đến việc làm chủ Docker Network
Mình còn nhớ như in cái ngày đầu deploy một hệ thống microservices lên server Staging. Dưới local thì mọi thứ chạy hoàn hảo với docker-compose up. Frontend (React) gọi backend (Node.js) vèo vèo, backend lại nói chuyện được với Database (Postgres) và Redis như người nhà. Nhưng khi lên server, một cơn ác mộng đã xảy ra: frontend không tài nào kết nối được tới backend. Lỗi Connection Refused hiện lên đỏ rực màn hình.
Mình đã mất gần một buổi chỉ để loay hoay tìm lỗi. Ping từ server vào IP của container thì được, nhưng container này gọi container kia bằng tên service (ví dụ: http://backend-api:3000) thì thất bại hoàn toàn. Vấn đề nằm ở đâu?
Gốc rễ vấn đề: Mỗi container là một “ốc đảo” mạng
Sau một hồi đào sâu vào tài liệu và các forum, mình nhận ra một nguyên tắc cốt lõi: mặc định, mỗi container Docker là một môi trường biệt lập hoàn toàn, bao gồm cả networking.
Khi bạn chạy docker run mà không chỉ định network, Docker tự động gán container vào một network mặc định tên là bridge. Hai container chạy riêng lẻ như vậy tuy cùng nằm trên network bridge này, nhưng lại không thể “gọi tên” nhau để giao tiếp. Chúng chỉ có thể nói chuyện qua địa chỉ IP nội bộ mà Docker cấp, vốn có thể thay đổi mỗi khi container khởi động lại, gây ra sự thiếu ổn định.
Ngược lại, docker-compose lại tỏ ra “khôn ngoan” hơn. Nó tự tạo một network bridge riêng cho tất cả các service định nghĩa trong file docker-compose.yml. Nhờ vậy, các container trong cùng một “gia đình” compose có thể dễ dàng gọi nhau bằng tên service. Sự cố của mình trên Staging chính là do đã chạy các container riêng lẻ thay vì dùng compose, khiến chúng bị “lạc” nhau.
Việc nắm rõ cơ chế này không chỉ giúp mình sửa lỗi tức thời. Nó còn là nền tảng để tự tin thiết kế các hệ thống phức tạp hơn. Trong thế giới Docker, có 3 network driver chính mà chúng ta cần làm chủ: Bridge, Host, và Overlay.
Các giải pháp networking trong Docker
1. Bridge Network: Lựa chọn an toàn cho đa số trường hợp
Có thể xem Bridge là loại network “quốc dân” của Docker. Nó vừa là mặc định, vừa đủ linh hoạt cho hầu hết các kịch bản. Khi sử dụng, Docker sẽ tạo ra một gateway và subnet ảo trên máy host, hoạt động như một “cây cầu” cho các container.
- Cách hoạt động: Mỗi container được cấp một IP riêng trong một mạng con ảo (ví dụ: 172.17.0.0/16). Docker sẽ xử lý việc định tuyến và NAT (Network Address Translation). Điều này cho phép các container giao tiếp với nhau và kết nối ra internet thông qua IP của máy host.
- DNS nội bộ: “Vũ khí bí mật” khi bạn dùng user-defined bridge network (mạng bridge tự tạo) chính là hệ thống DNS tích hợp. Các container cùng mạng có thể tìm thấy và giao tiếp với nhau chỉ bằng tên container (ví dụ:
postgres,redis). Đây chính là phép màu đằng sau sự tiện lợi củadocker-compose.
Ví dụ thực tế: Thay vì để Docker dùng bridge mặc định, hãy luôn tạo mạng của riêng mình.
# 1. Tạo một mạng bridge tùy chỉnh
docker network create my-app-net
# 2. Chạy container database trên mạng đó
docker run -d --name my-postgres --network my-app-net -e POSTGRES_PASSWORD=mysecretpassword postgres
# 3. Chạy container ứng dụng trên cùng mạng đó
# Nó có thể kết nối tới DB bằng hostname "my-postgres"
docker run -d --name my-app --network my-app-net -e DATABASE_HOST=my-postgres my-app-image
Kinh nghiệm cá nhân: Với các dự án chạy trên một server duy nhất, 99% trường hợp mình chỉ dùng user-defined bridge network. Nó đủ an toàn (cô lập tốt) và tiện lợi (DNS resolution). À, nhân tiện, mình đã chuyển từ docker-compose v1 sang v2 cho toàn bộ stack và quá trình khá smooth, networking vẫn hoạt động y hệt, chỉ là cú pháp trong file YAML gọn gàng hơn thôi.
2. Host Network: Phá bỏ giới hạn để đạt tốc độ tối đa
Khi hiệu năng mạng là ưu tiên tuyệt đối, Host network cho phép bạn “dỡ bỏ” lớp ảo hóa. Hãy hình dung container không còn card mạng riêng nữa. Thay vào đó, nó chia sẻ trực tiếp card mạng của máy chủ, như thể là một ứng dụng chạy thẳng trên host.
- Cách hoạt động: Container không có IP riêng mà sử dụng network stack của host. Một ứng dụng chạy ở port 8080 bên trong container sẽ chiếm dụng trực tiếp port 8080 trên máy host.
- Ưu điểm: Hiệu năng đỉnh cao. Do không phải đi qua lớp NAT trung gian, tốc độ mạng gần như tương đương với ứng dụng chạy native trên host.
- Nhược điểm: Đánh đổi lớn về bảo mật và quản lý. Vấn đề nhãn tiền là xung đột port. Bạn không thể chạy hai container cùng chiếm một port. Quan trọng hơn, nó phá vỡ nguyên tắc cô lập – một trong những giá trị cốt lõi nhất của Docker.
Ví dụ:
# Container này sẽ chiếm thẳng port 80 của host
# Nếu port 80 đã bị dùng, lệnh này sẽ báo lỗi
docker run -d --name nginx-host --network host nginx
Khi nào nên dùng? Trong thực tế, mình cực kỳ hiếm khi dùng host network. Nó chỉ phù hợp cho các tác vụ đặc thù, ví dụ như các agent giám sát hệ thống (Prometheus Node Exporter, Datadog Agent) cần truy cập trực tiếp vào network interface của host để thu thập metrics. Hoặc trong một số kịch bản yêu cầu xử lý luồng dữ liệu mạng cực lớn với độ trễ thấp nhất có thể. Hãy luôn cân nhắc kỹ lưỡng trước khi lựa chọn.
3. Overlay Network: Sức mạnh cho hệ thống phân tán
Rồi app của bạn chạy ngon trên một server. Sếp vỗ vai bảo “Scale lên 3 server chạy cho ổn định em ơi!”. Lúc này, Bridge network không còn đủ nữa. Container ở server A làm sao nói chuyện được với container ở server B?
Overlay network chính là câu trả lời. Nó là công nghệ nền tảng cho phép giao tiếp giữa các container trên nhiều host, đóng vai trò then chốt trong các hệ thống điều phối như Docker Swarm hay Kubernetes.
- Cách hoạt động: Overlay network tạo ra một mạng layer 2 ảo, trải dài trên nhiều Docker host. Nó giống như một tấm “lưới” vô hình phủ lên trên mạng vật lý của các server. Các container kết nối vào “lưới” này có thể giao tiếp với nhau bằng tên, bất kể chúng đang chạy ở host nào trong cụm (cluster).
- Yêu cầu: Để tạo và quản lý mạng overlay, bạn cần một công cụ điều phối container. Với Docker, công cụ tích hợp sẵn chính là Docker Swarm.
Ví dụ với Docker Swarm:
# (Trên node manager)
# 1. Khởi tạo Docker Swarm
docker swarm init
# 2. Tạo một mạng overlay
docker network create --driver overlay my-distributed-net
# 3. Deploy một service với 3 bản sao, trên mạng overlay đó
# Docker Swarm sẽ tự động phân tán 3 container này trên các node trong cụm
docker service create --name my-api --network my-distributed-net --replicas 3 my-api-image
Lúc này, 3 container của service my-api có thể đang nằm trên 3 server vật lý khác nhau. Tuy nhiên, chúng vẫn “thấy” và gọi được nhau một cách liền mạch thông qua mạng my-distributed-net.
Bảng tóm tắt: Chọn network nào cho kịch bản nào?
Không có lựa chọn “tốt nhất” cho mọi trường hợp. Thay vào đó, hãy chọn loại network “phù hợp nhất” với kiến trúc và yêu cầu của bạn.
- User-defined Bridge Network: Lựa chọn mặc định cho các ứng dụng chạy trên một host duy nhất. Nó cân bằng hoàn hảo giữa tính an toàn, sự tiện lợi (nhờ DNS nội bộ), và hiệu năng tốt.
- Host Network: Dành cho trường hợp đặc biệt khi cần hiệu năng mạng tối đa và bạn chấp nhận đánh đổi về bảo mật và xung đột port. Sử dụng một cách có chủ đích và cẩn trọng.
- Overlay Network: Giải pháp bắt buộc cho các ứng dụng phân tán trên nhiều host. Đây là nền tảng của các hệ thống có tính sẵn sàng cao sử dụng Docker Swarm hoặc Kubernetes.
Việc nắm vững ba loại network này đã thay đổi hoàn toàn cách mình thiết kế kiến trúc hệ thống. Từ một ứng dụng đơn giản trên một VPS đến một hệ thống microservices phức tạp, tất cả đều quy về việc lựa chọn network phù hợp. Hy vọng những kinh nghiệm thực tế này sẽ giúp bạn tự tin hơn khi làm việc với Docker.
