Dockerfile chỉ có khoảng mười lăm lệnh, và phần lớn hướng dẫn liệt kê chúng kèm một câu mô tả. Bài này làm việc khác: chạy thử từng cái để xem nó thật sự làm gì, vì có ít nhất bốn chỗ hành vi không giống mô tả.

Dạng shell so với dạng exec: lời khuyên phổ biến chỉ đúng một phần

Lời khuyên bạn gặp ở khắp nơi: luôn dùng CMD ["prog","arg"] chứ đừng CMD prog arg, vì dạng shell làm tiến trình của bạn thành con của /bin/sh và không nhận được SIGTERM.

Tôi viết một chương trình Python bắt SIGTERM và in ra một dòng khi dừng tử tế, rồi thử cả hai:

CMD python /app.py            # dang shell
CMD ["python","/app.py"]      # dang exec
PID 1 docker stop Mã thoát Có dừng tử tế
Dạng shell python /app.py 0,12 s 0
Dạng exec python /app.py 0,13 s 0

Giống hệt nhau. Dạng shell không hề tạo thêm tiến trình sh — vì sh -c khi chỉ có đúng một việc phải làm sẽ tự thay thế chính nó bằng lệnh đó (exec). PID 1 là python, tín hiệu tới thẳng nơi cần tới.

Tôi thử tiếp những dạng phức tạp hơn, đoán rằng sh sẽ phải ở lại:

CMD echo "khoi dong" && python /app.py
CMD python /app.py > /tmp/out.log 2>&1

Vẫn exec. PID 1 vẫn là python, vẫn dừng tử tế. sh tối ưu khá tích cực: chừng nào lệnh cuối cùng là thứ duy nhất còn lại phải làm, nó tự thay thế.

Chỗ lời khuyên kia đúng thì trông thế này:

Viết PID 1 docker stop Mã thoát Dừng tử tế
CMD python /app.py; echo xong /bin/sh -c ... 5,12 s 137 không
CMD python /app.py & wait /bin/sh -c ... 5,12 s 137 không
CMD python /app.py | cat /bin/sh -c ... 5,13 s 137 không

Cả ba đều chờ hết thời gian ân hạn 5 giây rồi bị SIGKILL, và không có dòng "dừng tử tế" nào trong log. Ứng dụng không bao giờ biết là nó sắp bị tắt.

Quy luật chính xác là: sh chỉ tự thay thế khi lệnh của bạn là thứ cuối cùng còn lại phải làm. Có lệnh đứng sau, có wait, có ống dẫn — sh phải ở lại làm PID 1, và tín hiệu dừng ở đó.

Vậy lời khuyên "luôn dùng dạng exec" có sai không? Không. Nó vẫn là lời khuyên đúng, chỉ là lý do thật khác với lý do thường được nêu: không phải vì dạng shell luôn làm mất tín hiệu, mà vì bạn không đoán được lúc nào nó mất. Một lần sửa Dockerfile thêm ; echo done vào cuối là bạn âm thầm mất khả năng dừng tử tế, và triệu chứng duy nhất là mã thoát 137 lẫn trong log triển khai.

COPY và ADD không phải là hai tên của một thứ

Cùng một tệp .tar.gz, hai lệnh:

COPY goi.tar.gz /d/  ->  /d/goi.tar.gz
ADD  goi.tar.gz /d/  ->  /d/goi/tep.txt

ADD tự bung tệp nén khi nguồn là tar cục bộ. Nó cũng tải được URL. Hai tính năng đó nghe tiện nhưng làm Dockerfile khó đọc: nhìn ADD x.tar.gz /d/ bạn không biết kết quả là một tệp hay một cây thư mục nếu không nhớ luật.

Chi tiết nhỏ nhưng đáng chú ý trong kết quả: bản ADD còn có /d/goi/._tep.txt/d/._goi — tệp metadata của macOS lọt vào lúc tôi tạo tar. Chúng vô hình trên máy tôi và rất hiện hữu trong image.

Luật thực dụng: dùng COPY cho mọi thứ. Chỉ dùng ADD khi bạn cố ý muốn bung tar, và khi đó hãy viết chú thích nói rõ.

EXPOSE không mở cổng nào

EXPOSE 8080
ExposedPorts trong image : map[8080/tcp:{}]
cong da publish ra may   : (khong co)
noi tu may chu vao 8080  : KHONG (ConnectionRefusedError)

EXPOSE chỉ ghi một dòng metadata. Nó không publish cổng, không mở tường lửa, không làm gì cả lúc chạy. Muốn truy cập từ máy chủ vẫn phải -p 8080:8080.

Vậy nó để làm gì? Ba việc: làm tài liệu cho người đọc image, cho docker run -P biết cần publish gì, và cho một số công cụ điều phối đọc. Hữu ích, nhưng đừng nhầm nó với việc mở cổng.

VOLUME trong Dockerfile rò rỉ volume

Đây là lệnh gây hậu quả âm thầm nhất trong cả danh sách.

VOLUME /du-lieu

Mỗi lần bạn chạy container từ image này, Docker tạo một volume ẩn danh cho /du-lieu. Chuyện gì xảy ra với chúng thì tuỳ cách bạn xoá container:

3 lan chay --rm                       : +0 volume
3 lan chay khong --rm roi docker rm   : +3 volume

--rm dọn cả volume ẩn danh. docker rm không — phải docker rm -v. Và vì volume ẩn danh mang tên là một chuỗi băm, không ai nhận ra chúng thuộc về cái gì.

Trên máy tôi lúc đo:

volume mo coi hien co: 217

Hai trăm mười bảy volume mồ côi, tích lại từ nhiều tháng chạy container. docker system dfphần 2 báo 32,72 GB volume với 12,6 GB thu hồi được.

Luật: đừng khai VOLUME trong Dockerfile. Để người chạy container quyết định gắn gì vào đâu bằng -v hoặc bằng Compose. Khai trong Dockerfile là ép mọi người dùng phải nhận một volume mà họ không yêu cầu, và không thể tắt đi.

ARG lộ bí mật, ENV thì không tồn tại lúc build xong

Hai lệnh trông giống nhau, sống ở hai giai đoạn khác nhau:

luc BUILD: ARG=gia-tri-arg  ENV=gia-tri-env
luc CHAY : ARG=<khong co>   ENV=gia-tri-env

ARG chỉ tồn tại trong lúc build; ENV đi vào image và có mặt lúc chạy.

Nhưng "chỉ tồn tại lúc build" rất dễ bị hiểu thành "an toàn để truyền bí mật". Tôi thử đúng cách người ta hay làm:

docker build --build-arg MAT_KHAU="sieu-bi-mat-123" -t td:bimat .
$ docker history --no-trunc td:bimat | grep -o 'MAT_KHAU=[^ ]*'
MAT_KHAU=sieu-bi-mat-123
MAT_KHAU=sieu-bi-mat-123

Nguyên văn, hai chỗ, đọc bằng một lệnh. Bất kỳ ai có image — tức là bất kỳ ai kéo được từ registry của bạn — đều lấy được. Bí mật không nằm trong filesystem của container, nhưng nó nằm trong lịch sử image, và lịch sử image đi kèm image.

Cách đúng là --mount=type=secret của BuildKit, và đó là chủ đề của phần 21.

Bảng tóm tắt

Lệnh Sinh layer Có mặt lúc chạy Ghi chú
FROM (nền) Ghim digest thay vì tag, xem phần 4
RUN Xoá tệp ở RUN sau không làm image nhỏ đi
COPY Mặc định nên dùng cái này
ADD Tự bung tar, tải URL — dùng có chủ đích
WORKDIR ✅ (nhỏ) Tạo thư mục nếu chưa có
ENV ❌ (0 B) Đi vào image
ARG Lộ trong docker history
EXPOSE metadata Không mở cổng
VOLUME Tạo volume ẩn danh mỗi lần chạy
CMD / ENTRYPOINT Dùng dạng exec

Thử ba mươi giây

Kiểm tra xem ứng dụng của bạn có thật sự nhận được tín hiệu dừng không:

docker run -d --name thu <anh-cua-ban>
docker exec thu ps -o pid,args | head -3     # PID 1 la gi?
docker stop thu
docker inspect thu --format 'ExitCode={{.State.ExitCode}}'

PID 1/bin/sh -c ...ExitCode=137 nghĩa là ứng dụng của bạn chưa bao giờ có cơ hội đóng kết nối, ghi nốt bộ đệm, hay rời khỏi bộ cân bằng tải một cách tử tế.

Và tiện thể:

docker history --no-trunc <anh-cua-ban> | grep -iE 'password|token|secret|key'

Phần sau đo cache của layer: đảo thứ tự hai dòng trong Dockerfile làm thời gian build đổi bao nhiêu.