Docker Compose: command và entrypoint khác gì?
ENTRYPOINT chạy chương trình, còn command truyền đối số. Xem đủ 4 cách override trong Compose và vì sao đặt entrypoint sẽ xóa CMD của image.
Lệnh command và entrypoint của Docker Compose, trong một quy tắc
Trong Docker Compose, entrypoint: đặt chương trình sẽ chạy và command: đặt các đối số truyền cho chương trình đó. Tiến trình của container là danh sách entrypoint, nối với danh sách command ở cuối. Mọi hành vi khác trên trang này đều bắt nguồn từ câu đó.
Hai key này tương ứng với hai chỉ thị trong Dockerfile. entrypoint: thay thế ENTRYPOINT của image. command: thay thế CMD của image. Chúng không độc lập với nhau, và đây là điểm khiến nhiều người gặp khó: đặt entrypoint: cũng loại bỏ CMD của image. Đặc tả Compose nêu rõ điều này. Nếu entrypoint khác null, Compose sẽ bỏ qua command mặc định của image.
Đọc những gì image đã khai báo
Trước khi ghi đè bất kỳ nội dung nào, hãy xem image đã cung cấp sẵn những gì.
docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16Bạn nhận được ["docker-entrypoint.sh"] và ["postgres"], nên container chạy docker-entrypoint.sh postgres. Script đó tạo thư mục dữ liệu trong lần khởi động đầu tiên, đọc các biến POSTGRES_*, hạ quyền xuống user postgres, rồi thực thi các đối số được truyền vào. Xác định chính xác phần nào bạn muốn thay đổi là quyết định quan trọng nhất. Để truyền một flag cho database, bạn thay thế command:. Nếu thay thế entrypoint:, toàn bộ bước thiết lập đó sẽ không chạy.
Bốn tổ hợp, thể hiện trên một image nhỏ
Tạo một image chỉ có nhiệm vụ in danh sách đối số mà nó được khởi chạy cùng.
FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]docker build -t argdemo .services:
demo:
image: argdemoChạy docker compose up sau mỗi lần chỉnh sửa và đọc dòng duy nhất mà nó ghi vào log.
- Không đặt key nào. Process là
/bin/echo ep cmdvà log hiển thịep cmd. - Chỉ đặt
command: ["cmd2"]. Process là/bin/echo ep cmd2. Entrypoint không thay đổi, chỉ có các đối số thay đổi. - Chỉ đặt
entrypoint: ["/bin/echo", "ep2"]. Process là/bin/echo ep2và log hiển thịep2.cmdtừ image bị loại bỏ mà không có cảnh báo nào. - Đặt cả hai key. Process là
/bin/echo ep2 cmd2. Đây là trường hợp duy nhất bạn kiểm soát toàn bộ danh sách đối số.
Vì sao đặt entrypoint sẽ xóa CMD của image
CMD của image được ghi dưới dạng danh sách đối số mặc định cho ENTRYPOINT của image đó. Khi thay entrypoint, các đối số này sẽ thuộc về một chương trình không còn chạy nữa. Vì vậy Compose loại bỏ chúng thay vì tạo một command line mà tác giả image không hề dự định. docker run --entrypoint cũng hoạt động giống như vậy. Đây là hành vi của Docker, không phải lỗi riêng của Compose.
Hệ quả rất cụ thể. nginx:1.27 khai báo ENTRYPOINT ["/docker-entrypoint.sh"] và CMD ["nginx", "-g", "daemon off;"]. Khi đặt entrypoint: /custom-init.sh, script của bạn sẽ khởi động với một danh sách đối số rỗng. Một script kết thúc bằng exec "$@" theo cách thông thường sẽ không có gì để exec. Khi đó exec không làm gì, script chạy đến dòng cuối, rồi container thoát với code 0 mà không có thông báo lỗi nào.
Tự thêm lại các đối số:
services:
web:
image: nginx:1.27
entrypoint: /custom-init.sh
command: ["nginx", "-g", "daemon off;"]Quy tắc cần nhớ: mỗi khi đặt entrypoint:, hãy quyết định luôn command: trong cùng lần chỉnh sửa.
Dạng exec và dạng shell, và điểm khác biệt của Compose
Dockerfile hỗ trợ 2 cú pháp. CMD ["nginx", "-g", "daemon off;"] là dạng exec: binary chạy trực tiếp, không có shell tham gia. CMD nginx -g "daemon off;" là dạng shell: Docker chuyển thành /bin/sh -c 'nginx -g "daemon off;"', nên shell chạy trước và chương trình của bạn trở thành tiến trình con của shell.
Compose không áp dụng quy tắc đó, và đây là điểm khiến nhiều người nhầm lẫn. Chuỗi trong command: được tách thành các đối số rồi thực thi trực tiếp, không có wrapper /bin/sh -c. Tài liệu tham chiếu của Compose nêu rõ: trường command không chạy trong context SHELL được định nghĩa trong image. Vì vậy, nếu cần các tính năng của shell, bạn phải tự gọi shell.
Đó là lý do command: echo "hello $$HOSTNAME" in ra nguyên văn hello $HOSTNAME. Không có shell nào đọc chuỗi này, nên không có gì mở rộng nó. Khi cần shell, hãy gọi shell:
services:
demo:
image: alpine:3.20
command: /bin/sh -c 'echo "hello $$HOSTNAME"'Tín hiệu, PID 1 và docker compose down đúng cách
docker compose stop và docker compose down gửi SIGTERM đến PID 1 bên trong từng container, chờ stop_grace_period, rồi gửi SIGKILL. Thời gian chờ mặc định là 10 giây.
PID 1 có vai trò đặc biệt trong Linux. Kernel không áp dụng hành động mặc định của tín hiệu cho PID 1. Vì vậy, một tiến trình không cài SIGTERM handler sẽ bỏ qua SIGTERM khi chạy dưới PID 1. Tiến trình đó sẽ chạy hết thời gian chờ rồi bị kill ngay, làm gián đoạn mọi connection đang mở hoặc transaction chưa commit.
Đặt một shell trước chương trình sẽ làm tình huống này dễ xảy ra hơn, vì shell là PID 1 và hầu hết shell không chuyển tiếp tín hiệu đến process con. Một số shell tự thay thế bằng lệnh cuối trong chuỗi -c, nên đôi khi chương trình của bạn vẫn chạy dưới PID 1. Điều này phụ thuộc vào shell và chuỗi lệnh cụ thể, vì vậy không được đoán. Hãy đọc nội dung đó:
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echoNếu PID 1 hiển thị là /bin/sh -c ... thay vì chương trình của bạn, có 2 cách sửa. Dùng exec form trong image, hoặc giữ shell và chuyển quyền điều khiển process bằng exec:
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec thay thế process shell bằng chương trình của bạn thay vì fork process con. Vì vậy, chương trình của bạn kế thừa PID 1 và nhận được tín hiệu.
Một số chương trình tạo process con nhưng không bao giờ reap chúng, khiến xuất hiện zombie process, vì PID 1 cũng là reaper. Compose có một switch cho trường hợp này:
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true chạy một init process nhỏ dưới PID 1. Process này chuyển tiếp tín hiệu đến chương trình của bạn và reap các process con. stop_grace_period cho phép quá trình shutdown thực sự chậm có thêm thời gian. Nếu chương trình của bạn yêu cầu một tín hiệu khác, stop_signal: SIGQUIT thay đổi tín hiệu mà Compose gửi. Dùng docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27 để xem image hiện tại yêu cầu tín hiệu nào.
Nếu mỗi service trong một stack luôn mất 10 giây khi docker compose down, đó là dấu hiệu không có process nào xử lý SIGTERM. Hãy sửa việc này trước khi đổ lỗi cho tooling, rồi xem sự khác nhau giữa docker compose down và stop để biết mỗi subcommand xóa những gì.
Sự khác nhau giữa exec form và shell form còn xuất hiện ở một nơi khác. Healthcheck viết dưới dạng test: ["CMD", "curl", "-f", "http://localhost/"] sẽ chạy binary trực tiếp, còn test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] sẽ chạy thông qua shell để || có tác dụng. Cách viết healthcheck cho Compose để phản ánh đúng lỗi trình bày phần còn lại của trường này.
Thêm một flag vào image chính thức
Đây là phần hầu hết người đọc cần. Bạn muốn thêm một flag cho postgres nhưng không được làm ảnh hưởng đến script khởi tạo.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
command: postgres -c max_connections=200 -c shared_buffers=256MB
volumes:
pgdata:Chỉ command: được thay đổi, nên docker-entrypoint.sh vẫn chạy và vẫn thực thi nội dung bạn cung cấp. Hãy kiểm tra kết quả thay vì mặc định cho rằng mọi thứ đúng:
docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'Output phải hiển thị 200. Nếu vẫn hiển thị 100, hãy chạy docker compose config và xác nhận command bạn mong đợi có trong output sau khi hợp nhất. Compose hợp nhất các file override bằng cách thay thế hoàn toàn command, không nối thêm vào đó. Vì vậy, file thứ hai cũng đặt command: sẽ âm thầm ghi đè giá trị trước đó.
${POSTGRES_PASSWORD} ở trên được Compose mở rộng trên host từ file .env trước khi container tồn tại. File biến môi trường và secret trong Compose giải thích giá trị đó có thể được lưu an toàn ở đâu.
Chạy migration một lần bằng docker compose run
docker compose run tạo một container mới từ cùng định nghĩa service và thay thế command bằng nội dung bạn nhập sau tên service. Entrypoint của image vẫn chạy, nên container được chuẩn bị giống hệt container chạy lâu dài.
docker compose run --rm app python manage.py migrate--rmxóa container khi command kết thúc. Nếu không dùng tùy chọn này, mỗi lần chạy sẽ để lại một container đã dừng, có thể xem trongdocker compose ps -a.- Các port không được publish. Container
runbỏ quaports:của service, trừ khi bạn thêm--service-ports, nên không thể xung đột với service đang chạy. - Các dependency được khởi động trước. Mọi thành phần trong
depends_onsẽ chạy trước command của bạn, còn--no-depssẽ bỏ qua bước đó. - Container nhận một tên được tạo tự động như
myproject-app-run-9f2c1a, nên không bao giờ trùng với container của service.
Để thay thế cả entrypoint, có một flag cho việc này:
docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'Danh sách argument thu được là /bin/sh -c 'python manage.py migrate', vì các từ sau tên service vẫn là command. docker compose exec là công cụ còn lại và hoạt động khác: nó chạy một process bên trong container đã chạy sẵn, đồng thời hoàn toàn bỏ qua cả entrypoint: và command:. Dùng run cho task cần container mới, và dùng exec để kiểm tra bên trong container đang chạy. Bảng tra cứu command của Compose trình bày các subcommand còn lại cạnh nhau.
Tại sao container của tôi thoát ngay lập tức?
Hãy bắt đầu bằng exit code, vì nó nhanh chóng thu hẹp nguyên nhân.
docker compose ps -a
docker compose logs appExit code 0 và không có output. Lệnh đã chạy xong. Nguyên nhân phổ biến nhất là một entrypoint: đã ghi đè CMD của image, khiến entrypoint chạy với danh sách đối số rỗng và không có gì để chuyển tiếp.
Lỗi kết thúc bằng permission denied. Script không có quyền executable bên trong image, thường vì quyền này chưa từng được đặt cho file trong repository. Đặt quyền này trong lúc build bằng COPY --chmod=0755 entrypoint.sh /entrypoint.sh.
Lỗi kết thúc bằng no such file or directory đối với một file mà bạn nhìn thấy rõ trong image. Script sử dụng ký tự xuống dòng của Windows. Khi đó, dòng đầu tiên được đọc là #!/bin/sh cộng với một byte carriage return, nên kernel tìm một interpreter có chứa byte đó trong tên nhưng không tìm thấy. Chạy dos2unix entrypoint.sh, sau đó thêm * text eol=lf vào .gitattributes để lỗi này không quay lại.
executable file not found in $PATH. Binary có tên trong command: không có trong image, hoặc bạn đã viết một shell built-in như cd ở nơi chỉ chấp nhận một chương trình thực.
Truy cập shell trong image có entrypoint bị lỗi
Khi entrypoint dừng trước khi bạn kịp kiểm tra gì, hãy thay thế nó:
docker compose run --rm --entrypoint /bin/sh appNếu lệnh đó trả về executable file not found in $PATH, image hoàn toàn không có shell. Các image dựa trên Distroless và scratch thường không đi kèm shell. Bạn vẫn có thể đọc filesystem từ bên ngoài mà không cần khởi động entrypoint:
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probeKhi cần giữ container chạy để có thể attach nhiều lần, hãy chạy một process không bao giờ thoát. Đặt cấu hình này trong một file override và không commit file đó:
services:
app:
entrypoint: ["tail", "-f", "/dev/null"]
command: []command: [] không bắt buộc, vì đặt entrypoint: đã xóa CMD của image, nhưng ghi rõ nó giúp thể hiện chủ đích cho người đọc file sau này. Khởi động container rồi truy cập vào đó:
docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/shBây giờ chạy entrypoint thật bằng tay và theo dõi vị trí nó dừng. Như vậy, thông báo lỗi sẽ xuất hiện trên terminal thay vì trong một container đã dừng chỉ sau nửa giây. Nếu bạn vẫn đang tạo stack đầu tiên, stack Compose đầu tiên trên VPS trình bày cấu trúc file mà toàn bộ phần trên giả định.
FAQ
Vì sao container của tôi thoát ngay sau khi chạy docker compose up?
Kiểm tra docker compose ps -a để biết exit code. Exit 0 và không có output thường có nghĩa là bạn đã đặt entrypoint: cho service. Việc này cũng xóa CMD của image, nên entrypoint chạy với danh sách đối số rỗng rồi kết thúc. Thêm lại các đối số bằng command:. Lỗi kết thúc bằng permission denied có nghĩa là entrypoint script không có quyền thực thi. Lỗi kết thúc bằng no such file or directory đối với một file tồn tại có nghĩa là script dùng line ending của Windows, nên dòng shebang của nó chỉ định một interpreter không tồn tại.
Đặt entrypoint trong Compose có xóa CMD của image không?
Có. Nếu entrypoint khác null, Compose bỏ qua command mặc định do image khai báo. Đây là hành vi được tài liệu hóa và phù hợp với docker run --entrypoint. Lý do là CMD của image được viết dưới dạng các đối số dành cho ENTRYPOINT của image đó. Vì vậy, khi bạn thay entrypoint, các đối số cũ không còn thuộc về thành phần nào nữa. Đặt command: trong cùng service nếu entrypoint mới vẫn cần các đối số.
Một chuỗi trong Compose command có được chạy qua shell không?
Không. Khác với CMD trong Dockerfile, một chuỗi trong command: của Compose được tách thành các đối số và thực thi trực tiếp, không có wrapper /bin/sh -c. Vì vậy, $VARIABLE không bao giờ được shell bên trong container mở rộng. Khi cần shell, hãy tự gọi shell, như trong command: /bin/sh -c 'echo "hello $$HOSTNAME"'. $$ kép dùng để escape dấu dollar, để Compose truyền dấu này vào container thay vì mở rộng nó trên host.
Vì sao docker compose down mất 10 giây khi chỉ có một container?
Compose gửi SIGTERM đến PID 1, chờ stop_grace_period (mặc định là 10 giây), rồi gửi SIGKILL. Kernel không áp dụng hành động mặc định của signal cho PID 1. Vì vậy, một chương trình không có handler SIGTERM sẽ bỏ qua signal và luôn chờ hết khoảng thời gian này. Dùng docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' ' để xác định PID 1 thực sự là tiến trình nào. Nếu đó là shell, hãy chuyển image sang exec form hoặc viết exec bên trong chuỗi shell. Nếu process tạo ra các process con nhưng không reaping chúng, hãy đặt init: true cho service.