Container 3 — Docker Compose, mạng & volume

13 thg 7, 2026 3 lượt xem
#docker
#networking
#devops
#docker-compose
#registry
#volume

Một container thì hiếm khi đủ

Ở bài Container là gì chúng ta đã hiểu container là một tiến trình bị cô lập, và ở bài Dockerfile & image chúng ta đã đóng gói ứng dụng thành image. Nhưng một ứng dụng thực tế hiếm khi chỉ là một container. Một hệ thống web tối thiểu thường gồm ít nhất ba mảnh: một web (frontend hoặc reverse proxy), một api (backend xử lý logic), và một db (cơ sở dữ liệu). Ba mảnh này phải:

  • Khởi động theo đúng thứ tự (db sẵn sàng trước khi api kết nối).
  • Tìm thấy nhau để gọi qua lại (api phải biết db ở đâu).
  • Chia sẻ cấu hình (mật khẩu, tên database) mà không hardcode.
  • Giữ được dữ liệu của db kể cả khi container bị xoá và tạo lại.

Chạy tay từng lệnh docker run cho cả ba, nhớ đúng cờ --network, -v, -e, -p theo đúng thứ tự, mỗi lần deploy lại gõ lại — đó là con đường dẫn tới lỗi. Bài này giới thiệu bốn công cụ giải quyết đúng bốn vấn đề trên: Docker Compose (khai báo cả stack trong một file), mạng container (cho container tìm và gọi nhau), volume (giữ dữ liệu bền), và registry (chia sẻ image giữa các máy).

Quy ước: các ví dụ trong bài dùng PostgreSQL và dịch vụ nhiều container. Môi trường viết bài không chạy được các stack này, nên mọi khối lệnh đều ghi (minh hoạ) — bạn hãy chạy trên máy có Docker.

Docker Compose: khai báo cả stack trong một file

Docker Compose là công cụ đọc một file YAML (mặc định compose.yaml, tên cũ docker-compose.yml) mô tả toàn bộ các service của ứng dụng, rồi dựng tất cả bằng một lệnh. Thay vì "gõ lệnh để tạo container", bạn mô tả trạng thái mong muốn rồi để Compose lo. Đây là bước đầu tiên chuyển từ tư duy mệnh lệnh (imperative) sang tư duy khai báo (declarative) — cùng triết lý mà Kubernetes đẩy lên quy mô cụm.

Một file Compose gồm các mục chính:

  • services: danh sách các dịch vụ (mỗi service là một loại container).
  • volumes: các named volume dùng chung.
  • networks: các mạng nội bộ.

Mỗi service có thể khai báo các khoá phổ biến sau:

  • image: dùng image có sẵn (ví dụ postgres:16), hoặc
  • build: chỉ thư mục chứa Dockerfile để Compose tự build image.
  • ports: publish cổng ra máy host theo dạng "HOST:CONTAINER".
  • environment / env_file: truyền biến môi trường.
  • volumes: gắn volume hoặc bind mount vào container.
  • depends_on: khai báo thứ tự và điều kiện khởi động.
  • healthcheck: cách Docker kiểm tra service đã "khoẻ" chưa.
  • networks: gắn service vào (các) mạng nào.

Vòng đời cơ bản chỉ có hai lệnh:

# (minh hoạ) dựng toàn bộ stack, -d = chạy nền
docker compose up -d

# (minh hoạ) xem trạng thái và log
docker compose ps
docker compose logs -f api

# (minh hoạ) dừng và xoá container + network (volume giữ lại)
docker compose down

Compose phù hợp nhất cho môi trường dev (dựng nguyên stack trên máy lập trình viên trong vài giây) và cho stack nhỏ chạy trên một máy. Nó không phải công cụ cho production nhiều máy — phần cuối bài sẽ nói vì sao.

Mạng container: cho các container tìm thấy nhau

Các network driver

Docker cung cấp vài loại mạng (driver):

  • bridge (mặc định): Docker tạo một mạng ảo riêng trên host; mỗi container được cấp một IP nội bộ trong mạng đó. Container trong cùng bridge nói chuyện được với nhau; ra ngoài internet thì đi qua NAT. Đây là loại dùng cho hầu hết trường hợp.
  • host: container dùng chung network stack với host, không có lớp cô lập mạng — nhanh hơn chút nhưng mất cách ly và dễ đụng cổng.
  • none: container không có mạng, hoàn toàn cô lập — dùng cho các job xử lý offline không cần kết nối.

DNS nội bộ: gọi nhau bằng TÊN SERVICE

Đây là điểm quan trọng nhất của phần mạng. Khi bạn tạo một user-defined bridge network (Compose tự tạo một mạng như vậy cho mỗi project), Docker chạy một DNS nội bộ. Nhờ đó, một container gọi container khác bằng tên service, không cần biết IP.

Ví dụ, nếu api và db cùng nằm trong một network do Compose tạo, thì trong code của api, chuỗi kết nối database chỉ cần trỏ tới hostname db:

postgresql://appuser:secret@db:5432/appdb

Docker DNS sẽ phân giải db thành IP hiện tại của container db. Điều này cực kỳ quan trọng vì IP của container thay đổi mỗi lần tạo lại — nếu hardcode IP thì hệ thống sẽ vỡ ngay khi restart. Tên service là địa chỉ ổn định. (Đây chính là ý tưởng "service discovery" mà Services trong K8s nâng lên tầm cụm nhiều máy.)

Lưu ý: DNS theo tên service chỉ hoạt động trên user-defined network, không hoạt động trên mạng bridge mặc định cũ (default bridge chỉ hỗ trợ liên kết qua IP). Vì Compose luôn tạo user-defined network riêng cho project, bạn được lợi này miễn phí.

Publish port: host so với nội bộ

Cần phân biệt rõ hai loại "cổng":

  • Cổng nội bộ giữa các container: không cần ports. api gọi db:5432 được ngay chỉ nhờ hai container cùng network. Không nên publish cổng db ra host.
  • Cổng publish ra host (ports: "8080:80"): mở cổng 8080 trên máy host, chuyển vào cổng 80 của container. Chỉ dùng cho những service cần truy cập từ bên ngoài (thường là web/reverse proxy). Cú pháp là HOST:CONTAINER.

Nguyên tắc: chỉ publish những gì buộc phải public. Database nên chỉ nằm trong mạng nội bộ, không có dòng ports nào — như vậy nó không thể bị truy cập trực tiếp từ ngoài host.

Mạng riêng để cô lập

Bạn có thể tạo nhiều network và gắn service vào các mạng khác nhau để cô lập tầng. Ví dụ:

  • Mạng frontend: chứa web và api.
  • Mạng backend: chứa api và db.

Khi đó web không cùng mạng với db, nên web không thể kết nối trực tiếp tới db kể cả khi biết tên — mọi truy cập dữ liệu buộc phải đi qua api. Đây là cách áp dụng nguyên tắc đặc quyền tối thiểu (least privilege) ở tầng mạng.

Volume: giữ dữ liệu khi container biến mất

Lớp ghi của container là ephemeral (tạm thời): khi container bị xoá, mọi thứ ghi vào filesystem của nó cũng mất theo. Với database, đó là thảm hoạ. Giải pháp là tách dữ liệu ra khỏi vòng đời container bằng volume. Có hai kiểu chính:

  • Named volume: Docker quản lý một vùng lưu trữ có tên (ví dụ db-data), nằm trong khu vực Docker quản lý trên host. Bạn không cần quan tâm đường dẫn thật. Đây là lựa chọn ưu tiên cho dữ liệu do container sinh ra và cần bền — điển hình là dữ liệu database. Named volume tồn tại độc lập, docker compose down không xoá nó (phải thêm cờ -v mới xoá).
  • Bind mount: gắn một thư mục cụ thể trên host vào container (ví dụ ./src:/app). Dùng cho dev khi muốn sửa code trên host và container thấy ngay, hoặc để đưa file cấu hình vào. Nhược điểm: phụ thuộc đường dẫn và layout của máy host, kém di động.

Quy tắc chọn:

Nhu cầuDùng
Dữ liệu DB, cần bền, do container ghiNamed volume
Sync code lúc dev (hot reload)Bind mount
File config đọc-only đưa vào containerBind mount (read-only)
Dữ liệu tạm không cần giữKhông cần volume

Với database, hãy luôn gắn một named volume vào thư mục dữ liệu của DB (với PostgreSQL là /var/lib/postgresql/data). Nhờ đó bạn có thể nâng cấp image, xoá và tạo lại container mà dữ liệu vẫn còn nguyên.

Sơ đồ stack

api gắn vào cả hai mạng nên vừa nhận request từ web (mạng frontend) vừa nói chuyện với db (mạng backend); db chỉ ở backend nên web không chạm tới được; dữ liệu db nằm ở named volume db-data nên không mất khi container tái tạo.

Ví dụ Compose đa dịch vụ

Dưới đây là một stack hoàn chỉnh gồm api + db, với volume và hai mạng cô lập. (minh hoạ) — không chạy trong môi trường viết bài.

# compose.yaml (minh hoạ)
services:
  api:
    build: ./api            # build từ Dockerfile trong ./api
    ports:
      - "8080:3000"         # publish: host 8080 -> container 3000
    environment:
      # trỏ tới db BẰNG TÊN SERVICE, không dùng IP
      DATABASE_URL: postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
    env_file:
      - .env                # nạp thêm biến từ file .env (không commit)
    depends_on:
      db:
        condition: service_healthy   # chỉ khởi động khi db đã "khoẻ"
    networks:
      - frontend
      - backend
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: appdb
    volumes:
      - db-data:/var/lib/postgresql/data   # named volume -> dữ liệu bền
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - backend             # db KHÔNG có ports -> không lộ ra host
    restart: unless-stopped

volumes:
  db-data:                  # Docker quản lý, sống độc lập với container

networks:
  frontend:
  backend:

Đọc kỹ ví dụ này, ta thấy đủ các nguyên tắc: api trỏ tới db bằng tên service; db không publish cổng nên chỉ truy cập được qua mạng nội bộ; mật khẩu lấy từ biến ${DB_PASSWORD} chứ không viết thẳng; db có named volume để giữ dữ liệu; và depends_on ... condition: service_healthy kết hợp với healthcheck bảo đảm api chỉ chạy khi db thật sự sẵn sàng nhận kết nối.

Env & secret: cấu hình mà không hardcode

Ba cơ chế truyền cấu hình vào container:

  • environment: liệt kê biến ngay trong file Compose. Tiện cho biến không nhạy cảm (tên DB, chế độ chạy).
  • env_file: nạp biến từ một file VAR=VAL. Dùng để tách cấu hình theo môi trường (dev/staging).
  • File .env: Compose tự đọc file .env ở cùng thư mục để thay thế biến trong chính file Compose (ví dụ ${DB_PASSWORD} ở trên). Đây là cách giữ giá trị nhạy cảm ra ngoài file Compose.

Cảnh báo về secret: biến môi trường không phải cơ chế bảo mật mạnh. Giá trị environment nằm ngay trong file Compose (thường được commit) và lộ ra khi ai đó chạy docker inspect. Nguyên tắc tối thiểu:

  • Không bao giờ viết mật khẩu/khoá trực tiếp trong compose.yaml.
  • Đặt secret trong .envthêm .env vào .gitignore để không lọt lên Git.
  • Với production, dùng cơ chế secret chuyên dụng (Docker secrets, hoặc Secret của K8s / vault) thay vì env thường.

profiles cho phép bật/tắt nhóm service tuỳ ngữ cảnh: gán profiles: ["debug"] cho một service, nó chỉ chạy khi bạn kích hoạt profile đó (ví dụ docker compose --profile debug up). Hữu ích cho các công cụ phụ (adminer, mailhog) chỉ cần lúc dev.

Registry: chia sẻ image giữa các máy

Compose có thể build image tại chỗ, nhưng để chạy trên máy khác (server, CI, cụm K8s) thì image phải được đẩy lên một registry — kho chứa image dùng chung.

Vòng đời cơ bản:

# (minh hoạ) gắn tag theo dạng <registry>/<repo>:<tag>
docker tag my-api:latest registry.example.com/team/my-api:1.4.0

# (minh hoạ) đẩy lên registry
docker push registry.example.com/team/my-api:1.4.0

# (minh hoạ) máy khác kéo về
docker pull registry.example.com/team/my-api:1.4.0

Có hai loại registry: công khai như Docker Hub (nơi bạn kéo postgres, nginx...), và private registry (nội bộ công ty, cloud như ECR/GCR/ACR, hoặc self-hosted). Với dữ liệu và image nội bộ nên dùng private để kiểm soát.

Ý nghĩa với CI/CD: mỗi lần code merge, pipeline build image, gắn tag (thường theo commit SHA hoặc số phiên bản), push lên registry, rồi môi trường triển khai pull đúng tag đó. Tag bất biến (immutable) giúp bạn biết chính xác phiên bản nào đang chạy và roll back được. Tránh chỉ dựa vào latest cho production vì nó là "mục tiêu di động", không cho biết đang chạy gì.

Từ Compose lên Kubernetes: khác tư duy, không chỉ khác quy mô

Compose và Kubernetes cùng giải bài toán "điều phối nhiều container", nhưng ở hai tầng khác nhau:

Docker ComposeKubernetes
Phạm viMột máyCụm nhiều máy
Tự phục hồi khi máy chếtKhôngCó (dời sang máy khác)
Scale ra nhiều hostKhông
Cân bằng tải nhiều bản saoHạn chếCó (Service)
Độ phức tạpThấp, học nhanhCao
Hợp choDev, stack nhỏ 1 máyProduction, HA

Điểm mấu chốt: Compose hợp với một máy. Khi bạn cần nhiều máy để chịu tải hoặc để đạt tính sẵn sàng cao (HA) — máy này chết vẫn còn máy khác — thì Compose không giải quyết được, vì nó không có khái niệm cụm. Lúc đó chuyển sang Kubernetes.

Đây không chỉ là "phiên bản lớn hơn của Compose" mà là khác tư duy: bạn không còn khai báo "chạy container X trên máy này", mà khai báo "tôi muốn có 3 bản sao service X ở đâu đó trong cụm", rồi K8s tự chọn máy, tự khởi động lại, tự cân bằng tải. Những khái niệm như mạng nội bộ theo tên và volume mà ta học ở đây vẫn còn — nhưng được K8s hiện thực lại ở tầng cụm (Service cho DNS, PersistentVolume cho lưu trữ).

Use case thực tế

Bối cảnh: một nhóm nhỏ vận hành cổng nội bộ tra cứu dữ liệu, gồm frontend, một API và PostgreSQL, chạy trên một server. Với Compose, họ làm như sau:

  1. Viết compose.yaml ba service với hai mạng (frontend, backend) để db chỉ nằm ở backend, không lộ cổng ra host.
  2. Gắn named volume db-data cho PostgreSQL — nhờ đó khi nâng cấp từ postgres:15 lên postgres:16 (đổi container), dữ liệu vẫn còn.
  3. Đặt mật khẩu DB trong .env (đã ignore), tham chiếu bằng ${DB_PASSWORD} — không secret nào bị commit.
  4. Thêm healthcheck bằng pg_isready cho db và depends_on: condition: service_healthy cho api, để api không crash vì kết nối lúc db chưa mở cổng.
  5. CI build image API, push lên private registry theo tag commit; trên server chỉ cần docker compose pull && docker compose up -d.

Sáu tháng sau, lượng người dùng tăng và ban lãnh đạo yêu cầu hệ thống "không được sập". Một server không đủ HA — họ bắt đầu chuyển stack này sang Kubernetes, dùng lại đúng các khái niệm mạng/volume đã quen. Compose vẫn được giữ cho môi trường dev trên máy lập trình viên.

Ghi nhớ

  • Docker Compose mô tả cả stack đa dịch vụ trong một file YAML; up/down dựng và gỡ toàn bộ. Hợp cho devstack nhỏ một máy.
  • Container trong cùng một user-defined network gọi nhau bằng TÊN SERVICE qua DNS nội bộ — không bao giờ hardcode IP (IP đổi khi tạo lại).
  • Chỉ publish (ports) những service cần truy cập từ ngoài; database nên chỉ nằm trong mạng nội bộ, không có ports.
  • Tách mạng (frontend/backend) để cô lập tầng theo nguyên tắc đặc quyền tối thiểu.
  • Lớp ghi container là ephemeral → dùng named volume cho dữ liệu bền (nhất là DB), bind mount cho dev/hot-reload.
  • Không hardcode secret: dùng .env (đã gitignore) và biến ${...}; env không phải bảo mật mạnh — production dùng secret chuyên dụng.
  • healthcheck + depends_on: condition: service_healthy để khởi động đúng thứ tự và tránh crash lúc phụ thuộc chưa sẵn sàng.
  • Registry (Docker Hub / private) để chia sẻ image; tag bất biến theo phiên bản là nền tảng của CI/CD, tránh phụ thuộc latest.
  • Cần nhiều máy / HA thì Compose hết đất dùng → chuyển sang Kubernetes; đó là khác tư duy, và các khái niệm mạng/volume ở đây được K8s hiện thực lại ở tầng cụm (Services K8s).

Bài viết liên quan

Continuous Integration/Delivery/Deployment, cấu trúc pipeline, ví dụ GitHub Actions và chiến lược release.

13 thg 7, 2026 4

Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.

13 thg 7, 2026 3

Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.

13 thg 7, 2026 3

Bản chất DevOps: phá bỏ rào cản Dev–Ops, vòng lặp vô tận, CALMS, và vì sao tự động hoá.

13 thg 7, 2026 3