Phần 53/60 của sê-ri Docker và Docker Compose. Phần 52 đo cách nhiều tệp Compose gộp lại trong một project; bài này đứng lùi ra một bước và đo chính khái niệm "project" — cái gì đặt tên network/volume tự sinh, cái gì phân biệt hai project khác nhau, và câu hỏi trong brief hôm nay: docker compose down xong thì cái gì còn nằm lại trên máy.

Thiết lập đo

Docker Compose v5.4.0, Engine 29.7.2. Service tối giản dùng chung mọi phép đo dưới đây:

services:
  app:
    image: alpine:3.20
    command: ["sleep", "3600"]
    volumes:
      - data:/data
volumes:
  data: {}

Đọc trạng thái thật bằng docker network ls, docker volume ls, docker ps — không đoán qua docker compose config, vì câu hỏi ở đây là cái gì thật sự tồn tại trên máy, không phải cái gì được khai báo.

Tên tự sinh: hai dấu phân cách khác nhau trong cùng một project

Chạy docker compose up -d trong thư mục compose-net-vol/ (không đặt name:, không có -p), rồi soi cả ba loại tài nguyên nó tạo ra:

docker network ls | grep compose-net-vol
docker volume ls | grep compose-net-vol
docker ps --format "{{.Names}}" | grep compose-net-vol
compose-net-vol_default      (network)
compose-net-vol_data         (volume)
compose-net-vol-app-1        (container)

Network và volume dùng dấu gạch dưới (_), container lại dùng dấu gạch ngang (-). Không phải lỗi đánh máy giữa hai công cụ — cả ba đều sinh từ cùng một project name, chỉ khác quy ước đặt tên cho từng loại tài nguyên. Nhớ nhầm dấu là gõ sai tên khi debug bằng tay (docker network inspect compose-net-vol-default báo "no such network") — rất dễ vấp vì mắt quen đọc container name có gạch ngang trước.

Project lấy tên từ đâu, và ai thắng khi có nhiều nguồn

Bốn cách đặt tên project, đo độc lập từng cách bằng docker network ls sau up -d:

Cách Lệnh Tên network sinh ra
Tên thư mục (mặc định) docker compose up -d (không đặt gì) compose-net-vol_default
Khoá name: trong compose.yml name: du-an-demo du-an-demo_default
Biến COMPOSE_PROJECT_NAME COMPOSE_PROJECT_NAME=tu-bien-moi-truong docker compose up -d tu-bien-moi-truong_default
Cờ -p docker compose -p tu-co-p up -d tu-co-p_default

Khi nhiều nguồn cùng có mặt, đo bằng cách đặt cả ba lên cùng một lần chạy — compose.yml khai name: tu-name-field, đồng thời set COMPOSE_PROJECT_NAME=tu-env và gọi -p tu-p:

COMPOSE_PROJECT_NAME=tu-env docker compose -p tu-p up -d
docker network ls | grep tu-p_default   # có

-p thắng. Bỏ -p, chỉ còn biến môi trường và name: cạnh tranh nhau:

COMPOSE_PROJECT_NAME=tu-env docker compose up -d
docker network ls | grep tu-env_default   # có, không phải tu-name-field

Biến môi trường thắng name:. Thứ tự ưu tiên đo được, từ mạnh nhất xuống: -p > COMPOSE_PROJECT_NAME > name: trong compose.yml > tên thư mục. Hệ quả thực tế: CI chạy nhiều job song song trên cùng một checkout mà không set -p hoặc COMPOSE_PROJECT_NAME riêng cho từng job sẽ đụng tên network/volume — job sau up vào đúng resource job trước đang dùng, vì cả hai đọc ra cùng một tên thư mục.

Phạm vi: cùng tên service, khác project là khác hẳn tài nguyên

Dựng hai project riêng thư mục (proj-a/, proj-b/) với đúng cùng một docker-compose.yml — cùng khai service db, cùng khai volume tên data. Lên cả hai, ghi dữ liệu khác nhau vào từng bên:

cd proj-a && docker compose up -d && docker compose exec db sh -c "echo A > /data/who.txt"
cd proj-b && docker compose up -d && docker compose exec db sh -c "echo B > /data/who.txt"
docker volume ls | grep -E "proj-a_data|proj-b_data"
proj-a_data
proj-b_data

Hai volume riêng biệt dù compose.yml giống hệt nhau — tên volume tự sinh luôn có tiền tố project, nên "data" của proj-a và "data" của proj-b chưa từng là cùng một ổ đĩa. Đọc lại xác nhận không lẫn: proj-a đọc ra A, proj-b đọc ra B.

Mạng cũng cách ly, và cách ly thật ở tầng IP, không chỉ ở tầng DNS tên service. Hai mạng nhận hai subnet khác nhau:

docker network inspect proj-a_default --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
docker network inspect proj-b_default --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
172.19.0.0/16
172.20.0.0/16

Không dừng ở việc DNS của proj-a không phân giải được tên service db của proj-b (chuyện đương nhiên, khác network thì khác zone DNS nội bộ). Đo thẳng bằng ping vào IP thật của container bên kia, bỏ qua DNS hoàn toàn:

docker compose exec db sh -c "ping -c2 -W1 172.20.0.2"
2 packets transmitted, 0 packets received, 100% packet loss

Hai bridge network mặc định của hai project bị Docker cách ly ở tầng mạng, không chỉ "không biết tên nhau". Đây là lý do hai project độc lập chạy trên cùng một máy không cần lo đụng cổng nội bộ hay đụng route — muốn cho chúng nói chuyện với nhau phải chủ động khai một network chung.

Mạng và volume khai external: true nằm ngoài phạm vi này

Nếu network hay volume được khai external: true, project không "sở hữu" nó — chỉ gắn vào. Tạo trước một network và một volume bằng tay, cho một project dùng chung với network mặc định của nó:

docker network create mang-co-san
docker volume create du-lieu-co-san
# compose.yml của proj-external khai thêm:
#   networks: { mang-ngoai: { external: true, name: mang-co-san } }
#   volumes:  { du-lieu-ngoai: { external: true, name: du-lieu-co-san } }
docker compose up -d
docker compose down -v
Network proj-external_default Removing
Network proj-external_default Removed

down -v — cờ mạnh nhất để dọn sạch — chỉ xoá proj-external_default (network tự sinh, thuộc phạm vi project). mang-co-sandu-lieu-co-san còn nguyên, docker network lsdocker volume ls xác nhận. Compose không bao giờ động vào tài nguyên nó không tạo ra, dù -v có mặt hay không — an toàn khi chia network/volume giữa nhiều project, nhưng cũng có nghĩa dọn tay những resource đó là trách nhiệm của người vận hành, không ai giúp.

Cái gì còn lại sau down

Câu hỏi chính của brief hôm nay. Ghi một dòng đánh dấu vào volume, down không kèm -v, up lại, đọc lại:

docker compose exec app sh -c "echo du-lieu-quan-trong > /data/marker.txt"
docker compose down
docker network ls | grep compose-net-vol    # rỗng
docker volume ls | grep compose-net-vol     # compose-net-vol_data — còn
docker compose up -d
docker compose exec app cat /data/marker.txt
du-lieu-quan-trong

Container và network bị xoá sạch — đo được bằng docker ps -adocker network ls, không còn dấu vết. Volume thì không đụng tới, và dữ liệu bên trong nguyên vẹn qua một vòng downup. Thêm -v mới xoá volume:

docker compose down -v
docker volume ls | grep compose-net-vol   # rỗng

Bảng tổng hợp

Tài nguyên Tên tự sinh Còn lại sau down Còn lại sau down -v
Container <project>-<service>-<n> (gạch ngang) Xoá Xoá
Network mặc định <project>_default (gạch dưới) Xoá Xoá
Volume có tên <project>_<tên> (gạch dưới) Còn, dữ liệu nguyên vẹn Xoá
Network/volume external: true tên do người tạo đặt, không có tiền tố Còn Vẫn còn

Lỗi thường gặp

Tưởng down dọn sạch, script deploy vẫn thấy dữ liệu cũ

down không kèm -v cố tình giữ volume — đó là hành vi bảo vệ dữ liệu, không phải sót. Script CI chạy docker compose down && docker compose up -d để "làm mới môi trường test" sẽ vẫn thấy dữ liệu database của lần chạy trước, vì volume chưa từng bị xoá. Muốn môi trường sạch thật sự, down -v mới đúng ý.

Hai project trùng tên thư mục ở hai đường dẫn khác nhau, tưởng tách biệt

Tên project mặc định lấy từ tên thư mục cuối cùng trên đường dẫn, không phải toàn bộ đường dẫn. /srv/app-a/blog/home/user/blog đều cho project name blog nếu không đặt name: hay -p — hai project ở hai máy chủ khác nhau vô tình cùng namespace, dễ nhầm khi đọc log docker network ls trên một máy chạy nhiều checkout của cùng repo.

Thử ba mươi giây

Xác nhận ngay dự án Compose hiện tại đang lấy tên project từ đâu, và những gì down sẽ để lại:

docker compose config --format json | grep -m1 '"name"'
docker compose ps -a --format "{{.Name}}"
docker volume ls --filter "label=com.docker.compose.project=$(docker compose config --format json | grep -m1 '"name"' | cut -d'"' -f4)"

Dòng cuối liệt kê đúng những volume sẽ sống sót qua lần down tiếp theo — nếu danh sách trống, down -v không mất gì thêm ngoài network và container.

Bài viết liên quan