ENTRYPOINT và CMD đều nói cho Docker biết chạy gì. Chúng khác nhau ở chỗ tham số dòng lệnh của người dùng đi đâu, và bốn tổ hợp cho ra bốn hành vi mà đọc tài liệu xong vẫn dễ nhầm.
Tôi dùng một script in ra chính tham số nó nhận được, rồi chạy từng tổ hợp hai lần: một lần không truyền gì, một lần truyền x y.
Bốn tổ hợp
1. Chỉ có CMD — CMD ["/in-ra.sh","a","b"]
khong tham so : so tham so=2 | tham so: a b
them 'x y' : Error: exec: "x": executable file not found in $PATH
Tham số người dùng thay thế toàn bộ CMD. Docker cố chạy x như một chương trình và không tìm thấy. Đây là hành vi đúng của CMD: nó là mặc định có thể ghi đè hoàn toàn.
2. Chỉ có ENTRYPOINT — ENTRYPOINT ["/in-ra.sh","a","b"]
khong tham so : so tham so=2 | tham so: a b
them 'x y' : so tham so=4 | tham so: a b x y
Tham số người dùng được nối thêm vào sau. ENTRYPOINT là phần cố định, không ghi đè bằng dòng lệnh thường được.
3. Cả hai — ENTRYPOINT ["/in-ra.sh","co-dinh"] + CMD ["mac-dinh"]
khong tham so : so tham so=2 | tham so: co-dinh mac-dinh
them 'x y' : so tham so=3 | tham so: co-dinh x y
Đây là tổ hợp có ích nhất: ENTRYPOINT là chương trình, CMD là tham số mặc định cho nó, và người dùng ghi đè phần tham số mà không đụng tới chương trình. Đó là cách docker run postgres chạy PostgreSQL còn docker run postgres --version vẫn chạy PostgreSQL nhưng in phiên bản.
4. ENTRYPOINT dạng shell — ENTRYPOINT /in-ra.sh co-dinh + CMD ["mac-dinh"]
khong tham so : so tham so=1 | tham so: co-dinh
them 'x y' : so tham so=1 | tham so: co-dinh
CMD bị bỏ qua hoàn toàn, và tham số x y của người dùng biến mất không dấu vết. Không lỗi, không cảnh báo, container chạy bình thường và làm sai thứ bạn yêu cầu.
Đây là lý do thật sự để luôn viết ENTRYPOINT ở dạng exec — không phải vì tín hiệu (chuyện đó phức tạp hơn, xem phần 5), mà vì dạng shell làm mọi tham số truyền vào bốc hơi trong im lặng.
Bảng tóm tắt
| Không truyền gì | Truyền x y |
|
|---|---|---|
Chỉ CMD |
chạy CMD |
CMD bị thay hoàn toàn bởi x y |
Chỉ ENTRYPOINT |
chạy ENTRYPOINT |
x y nối thêm vào cuối |
| Cả hai | ENTRYPOINT + CMD |
CMD bị thay bởi x y |
ENTRYPOINT dạng shell |
chỉ ENTRYPOINT |
x y bị bỏ qua |
Script khởi động: một chữ exec đổi kết cục
Rất nhiều image dùng một script làm ENTRYPOINT để chuẩn bị trước khi chạy — chờ CSDL, sinh tệp cấu hình, đổi quyền. Cách viết script đó quyết định container có dừng được tử tế hay không.
#!/bin/sh
# kieu SAI
echo "chuan bi..."
python /app.py
#!/bin/sh
# kieu DUNG
echo "chuan bi..."
exec python "$@"
Đo với một ứng dụng bắt SIGTERM và in ra khi dừng tử tế:
| PID 1 | docker stop |
Mã thoát | Log cuối | |
|---|---|---|---|---|
Không exec |
/bin/sh /vao.sh |
5,13 s | 137 | dang chay |
Có exec |
python /app.py |
0,12 s | 0 | DUNG-TU-TE |
Bản không có exec để sh ở lại làm PID 1, python thành tiến trình con PID 7. Docker gửi SIGTERM cho PID 1; sh nhận, không làm gì, và không chuyển tiếp cho con. Sau khi hết thời gian ân hạn, container bị SIGKILL — mã 137, ứng dụng chưa kịp đóng kết nối hay ghi nốt dữ liệu.
Bản có exec thì sh tự thay thế mình bằng python, nên python trở thành PID 1 và nhận tín hiệu trực tiếp. Dừng trong 0,12 giây với mã thoát 0.
"$@" đi cùng exec cũng quan trọng: nó chuyển tiếp mọi tham số mà CMD hoặc người dùng đưa vào, nên bạn giữ được toàn bộ hành vi của tổ hợp số 3 ở trên.
Nếu bạn chỉ nhớ một câu từ bài này: dòng cuối cùng của mọi entrypoint script phải bắt đầu bằng exec.
Ghi đè lúc chạy
ENTRYPOINT không phải là bất khả xâm phạm — chỉ là không ghi đè được bằng tham số thường:
$ docker run --rm --entrypoint /bin/echo ep2:dung "da ghi de"
da ghi de
Cờ --entrypoint rất hữu ích khi gỡ lỗi: image không chạy được thì mở shell vào xem có gì bên trong.
docker run --rm -it --entrypoint sh <anh-cua-ban>
Trong Compose thì tương ứng là entrypoint: và command: — và chúng theo đúng luật ở bảng trên, nên hiểu bảng đó là hiểu luôn Compose.
Chọn thế nào
- Image là một công cụ (nginx, postgres, một CLI): dùng
ENTRYPOINTcho chương trình,CMDcho tham số mặc định. Người dùng truyền cờ khác mà không phải nhớ tên chương trình. - Image là ứng dụng của bạn, chạy một cách duy nhất: chỉ
CMDdạng exec là đủ, và người khác dễ dàngdocker run ... shđể soi khi cần. - Cần chuẩn bị trước khi chạy:
ENTRYPOINT ["/vao.sh"]+CMD [...], và script kết thúc bằngexec "$@". - Đừng bao giờ dùng dạng shell cho
ENTRYPOINT.
Thử ba mươi giây
Kiểm tra ba thứ cùng lúc trên image của bạn:
docker inspect <anh> --format 'Entrypoint={{.Config.Entrypoint}} Cmd={{.Config.Cmd}}'
docker run -d --name thu <anh>
docker exec thu ps -o pid,args | head -3
docker stop thu && docker inspect thu --format 'ExitCode={{.State.ExitCode}}'
docker rm thu
Dòng đầu mà hiện Entrypoint=[/bin/sh -c ...] nghĩa là bạn đang ở tổ hợp thứ tư, và mọi tham số người dùng truyền vào đang bị bỏ qua. PID 1 là /bin/sh cộng ExitCode=137 nghĩa là entrypoint script của bạn thiếu exec.
Phần sau là multi-stage build — cách tách môi trường build khỏi runtime, và image nhỏ đi bao nhiêu.