docker run là một lệnh gộp. Nó làm hai việc — tạo container rồi khởi động nó — và giấu đi một mô hình trạng thái mà bạn sẽ phải hiểu ngay khi có thứ gì đó không dừng đúng cách.

Sáu trạng thái, đo từng bước

truoc khi tao           : (khong ton tai)
sau docker create       : created
sau docker start        : running
sau khi tien trinh xong : exited | ma thoat = 7
start lai lan nua       : running
sau docker rm           : (khong ton tai)

Ba điều đáng chú ý:

created là một trạng thái thật. Container đã có lớp ghi, đã có cấu hình mạng, chỉ chưa chạy. Phần 1 đo được docker create mất 74,4 ms và docker start mất 193,1 ms — nên nếu bạn cần khởi động thật nhanh, tạo trước rồi start là một thủ thuật có thật.

exited không phải là đã biến mất. Container giữ nguyên lớp ghi, log, và mã thoát. Máy tôi lúc đo có 61 container ở đủ trạng thái, chiếm 2,53 GB. Đó là lý do docker ps (chỉ hiện cái đang chạy) luôn cho cảm giác dễ chịu hơn docker ps -a.

Container đã thoát khởi động lại được, và nó chạy tiếp trên đúng lớp ghi cũ. Đây là khác biệt lớn giữa docker startdocker run: cái đầu dùng lại container, cái sau dựng cái mới tinh.

docker stop: đừng tin mặc định

Đây là phép đo làm tôi phải quay lại kiểm tra ba lần.

Tôi dựng một container cố tình bỏ qua SIGTERM, rồi đo docker stop:

Lệnh Thời gian Mã thoát
docker stop (không đặt -t) 1,16 s 137
docker stop -t 2 2,15 s 137
docker kill 0,14 s 137
docker stop trên container xử lý TERM 1,08 s 0

Con số đầu tiên sai so với mọi thứ tôi từng đọc: thời gian ân hạn mặc định của docker stop được ghi ở khắp nơi là 10 giây. Đo được 1,16.

Không đoán, tôi hỏi thẳng container:

$ docker inspect kt --format '{{.Config.StopTimeout}}'
1

Rồi hỏi những container do người khác tạo trên cùng máy:

/imageworkflow-web    : StopTimeout=1
/imageworkflow-server : StopTimeout=1
/elated_greider       : StopTimeout=1

Tất cả đều là 1. Và khi đặt tường minh thì nó tôn trọng đúng:

voi --stop-timeout 10 : Config.StopTimeout = 10
docker stop mat 10,16 s

Tôi không xác định được chắc chắn con số 1 đến từ đâu — daemon.json trên máy không khai gì về stop-timeout, nên nhiều khả năng là mặc định của bản Docker này (29.3.1, Docker Desktop). Tôi không kết luận rằng "Docker 29 đổi mặc định thành 1", vì tôi chỉ có một máy để đo.

Nhưng bài học thực dụng thì không phụ thuộc vào việc máy bạn là 1 hay 10:

Đừng dựa vào thời gian ân hạn mặc định. Hãy khai tường minh.

docker run --stop-timeout 30 ...
# trong Compose
services:
  api:
    stop_grace_period: 30s

Vì con số này quyết định ứng dụng của bạn có kịp đóng kết nối, ghi nốt bộ đệm và dừng tử tế hay không. Một giây là quá ít cho gần như mọi thứ có trạng thái.

Mã thoát 137 nghĩa là bị SIGKILL (128 + 9). Thấy nó trong log nghĩa là container đã không tự dừng trong thời gian ân hạn và bị giết. Nếu dịch vụ của bạn thường xuyên thoát với 137 lúc triển khai, bạn đang mất dữ liệu ở đâu đó mà không ai báo.

Còn docker kill thì không có ân hạn: 0,14 giây, gửi SIGKILL ngay. Dùng nó khi gỡ lỗi, đừng dùng trong kịch bản triển khai.

Lớp ghi: co lại được, khác với image

Mỗi container có một lớp filesystem ghi được nằm trên các layer chỉ đọc của image. docker ps -s cho biết nó to bao nhiêu:

moi tao, chua ghi gi  : SizeRw = 4,1 kB   (virtual 9,51 MB)
sau khi ghi 30 MB     : 31,5 MB           (virtual 41 MB)
sau khi XOA tep do    : 4,1 kB            (virtual 9,51 MB)
sau khi dung container: 4,1 kB

Chú ý dòng thứ ba: xoá tệp làm lớp ghi co lại thật. Đây là điểm ngược hẳn với layer của image — phần 2 đo được rằng xoá tệp ở một layer sau không làm image nhỏ đi, vì layer chỉ cộng thêm chứ không trừ đi.

Khác biệt nằm ở chỗ: layer image đã đóng băng và được chia sẻ giữa nhiều container, nên không sửa được. Lớp ghi thì thuộc về riêng container này và là một thư mục bình thường, xoá là mất.

Hai cột trong docker ps -s cũng đáng phân biệt: SizeRw là phần container tự ghi, còn virtual là tổng cả layer image — mà layer image thì dùng chung, nên cộng cột virtual của nhiều container sẽ ra con số phóng đại, đúng như cái bẫy docker images ở phần trước.

Container đã thoát vẫn giữ log

Log không nằm trong lớp ghi — nó nằm ở chỗ khác trên máy chủ:

$ docker inspect nhat-ky --format '{{.LogPath}}'
/var/lib/docker/containers/822ba76f.../822ba76f...-json.log

Tôi cho một container in 200 000 dòng rồi đo:

docker logs tra ve : 200 000 dong | 10,0 MB
tep tren dia       : 23,4 MB
cau hinh xoay vong : map[]   (rong — khong xoay vong)

Nội dung log 10 MB nhưng tệp trên đĩa 23,4 MB, gấp 2,3 lần, vì driver json-file bọc mỗi dòng vào một đối tượng JSON kèm dấu thời gian và tên luồng. Và cấu hình xoay vòng rỗng — mặc định không có giới hạn nào cả.

Chúng ta sẽ mổ kỹ chuyện này ở phần về log driver. Điều cần nhớ bây giờ, vì nó thuộc về vòng đời: log sống cùng container, và chỉ mất khi docker rm. Container đã exited vài tháng vẫn giữ nguyên tệp log của nó.

Bốn lệnh nên phân biệt cho rõ

Lệnh Làm gì Còn lại gì
docker stop gửi SIGTERM, chờ ân hạn, rồi SIGKILL container exited, lớp ghi và log còn
docker kill SIGKILL ngay như trên
docker rm xoá container lớp ghi và log mất
docker run --rm tự rm khi thoát không còn gì để khám nghiệm

--rm rất tiện cho lệnh dùng một lần, nhưng nó cũng xoá luôn bằng chứng. Khi một container thoát mà bạn không hiểu vì sao, hãy chạy lại không có --rm rồi docker inspectdocker logs cái xác của nó.

Thử ba mươi giây

Kiểm tra thời gian ân hạn thật trên máy bạn — con số này quyết định ứng dụng có kịp dừng tử tế không:

docker run -d --name thu-tin-hieu alpine sh -c 'trap "" TERM; while true; do sleep 1; done'
time docker stop thu-tin-hieu
docker inspect thu-tin-hieu --format 'StopTimeout={{.Config.StopTimeout}} ExitCode={{.State.ExitCode}}'
docker rm thu-tin-hieu

ExitCode=137 nghĩa là container đã bị giết chứ không tự dừng. Nếu time báo khoảng một giây, bạn vừa biết mình có ít thời gian hơn tưởng rất nhiều — và đó là lý do nên khai --stop-timeout tường minh cho mọi dịch vụ có trạng thái.

Phần sau bàn về registry, tag và digest — và đo chuyện gì xảy ra khi một tag bị đẩy đè.