SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

Docker Compose exec: mở shell tương tác trong container

Dùng docker compose exec để mở shell trong service đang chạy. Nếu service đã dừng hoặc không muốn ảnh hưởng container, dùng docker compose run --rm.

Mở shell tương tác bằng docker compose exec

docker compose exec web bash mở một shell tương tác bên trong container đang chạy dưới dạng service web. Tên đứng sau exec là tên service trong compose.yaml của bạn, không phải tên container. Nếu image không có bash, hãy yêu cầu sh thay thế.

docker compose ps
docker compose exec web bash

Chạy docker compose ps trước. Lệnh này phải liệt kê web với trạng thái running. Sau đó, lệnh thứ hai đưa bạn đến prompt bên trong container. Nhấn exit hoặc Ctrl-D để quay lại host. Service vẫn tiếp tục chạy sau khi bạn thoát, vì exec khởi động một process thứ hai bên cạnh process chính. Đóng shell không ảnh hưởng đến PID 1 (process id 1), tức process mà container được xây dựng để chạy.

Đó là một trong hai cách truy cập. exec kết nối vào một container đã tồn tại. docker compose run tạo một container mới từ cùng định nghĩa service. Hầu hết nội dung còn lại trong hướng dẫn này đều bắt nguồn từ khác biệt đó.

Vì sao -it là tùy chọn trong Compose nhưng bắt buộc khi dùng docker trực tiếp

Hai flag kiểm soát phần tương tác của một session. -i giữ stdin mở để nội dung bạn nhập được chuyển đến process. -t cấp phát pseudo terminal, gọi là TTY, để shell hiển thị prompt và xử lý phím mũi tên. Plain docker exec mặc định tắt cả hai, nên mọi ví dụ bạn đã xem đều ghi docker exec -it. docker compose exec tự bật cả hai, nên docker compose exec -it web bashdocker compose exec web bash có cùng tác dụng. Compose vẫn chấp nhận -it để thói quen dùng lệnh cũ vẫn hoạt động.

Bạn sẽ nhận ra thiếu TTY chỉ sau vài giây. Shell vẫn chạy nhưng không hiển thị prompt, và Ctrl-C không đến được process. Trường hợp ngược lại, khi bạn phải yêu cầu Compose không cấp phát TTY, có flag riêng và section riêng ở phần bên dưới.

Cần làm gì khi image không có bash

Yêu cầu một image dựa trên Alpine chạy bash sẽ khiến lệnh exec thất bại như sau:

OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown

Thông báo đó không phải là lỗi của exec. Nó cho biết binary bạn yêu cầu không có trong image. Alpine đi kèm BusyBox, cung cấp ash dưới tên /bin/sh và hoàn toàn không có bash, vì vậy hãy yêu cầu sh:

docker compose exec web sh

Các image dựa trên Debian và Ubuntu, bao gồm các tag -slim, có sẵn bash. bash cung cấp command history và khả năng completion tốt hơn. Vì vậy, hãy thử bash trước rồi fallback sang sh. sh có trong gần như mọi image đa dụng.

Một số image hoàn toàn không có shell. Distroless image và các image được build FROM scratch chỉ chứa application binary cùng các library của nó, không có thành phần nào khác. Đây là chủ ý, vì một shell không tồn tại thì không thể bị dùng để tấn công bạn. Với các image này, sh cũng thất bại với cùng thông báo và không còn shell nào khác để thử. Có 2 cách xử lý. Image distroless của Google phát hành các tag :debug có thêm BusyBox shell, nên bạn có thể tạm thời đổi tag để truy cập vào container. Hoặc khởi động một container riêng bên trong các namespace của container đích:

CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot

Bây giờ bạn có các tool của netshoot, được kết nối với network của ứng dụng, nên curl localhost:8080ss -lntp hoạt động như thể bạn đang ở bên trong ứng dụng. Filesystem bạn thấy thuộc về netshoot, không phải app. Vì process namespace được share, ls /proc/1/root/ sẽ truy cập được các file của chính container đích khi bạn chạy dưới quyền root.

Khi service không chạy, hãy dùng docker compose run --rm

exec cần một container đang chạy. Nếu trỏ lệnh này vào một service đã dừng, Docker sẽ từ chối:

service "web" is not running

Lệnh này không tự khởi động gì. docker compose run sẽ:

docker compose run --rm web bash

run tạo một container mới từ định nghĩa service web, dùng cùng image, environment, volume và network, rồi thay command của service bằng command bạn nhập. --rm xóa container đó khi bạn thoát. Nếu bỏ --rm, các container còn lại sẽ tích tụ với những tên như myproject-web-run-4f1c2b. docker compose ps -a sẽ hiển thị chúng, nhưng không có cơ chế nào khác tự dọn chúng.

Có 2 hành vi của run thường khiến người dùng bất ngờ. Lệnh này không publish port của service trừ khi bạn thêm --service-ports. Đây là chủ ý: một container thứ hai bind host port 8080 trong khi container đầu tiên vẫn đang giữ port đó sẽ fail với bind: address already in use. run cũng khởi động mọi service được liệt kê dưới depends_on trước khi mở shell, nên một lần kiểm tra nhanh có thể khởi động cả database và cache. --no-deps sẽ bỏ qua bước đó.

run đi qua ENTRYPOINT của image, còn exec thì không. exec khởi động command của bạn trực tiếp trong container hiện có, nên script entrypoint không nhận được command đó. Với run, bash của bạn được truyền thành các argument cho script đó. Nhiều image chính thức kết thúc entrypoint bằng exec "$@", nên command được chuyển tiếp trực tiếp và bạn nhận được shell. Nếu script tự diễn giải các argument của nó, script sẽ xử lý chúng theo cách khác. Khi đó, bạn có thể thay entrypoint cho riêng lần chạy đó:

docker compose run --rm --entrypoint sh web

Đây là lý do phổ biến nhất khiến một command chạy được với exec lại cho kết quả khác khi dùng run. Phần tách biệt giữa command và entrypoint giải thích mỗi lần bạn đang thay thế phần nào trong cấu hình của image.

exec hoặc run: chọn lệnh nào

  • exec cần container đang chạy. run thì không cần và có thể khởi động các dependency.
  • exec thấy danh sách process đang chạy và các file ở trạng thái hiện tại, bao gồm mọi nội dung application đã ghi kể từ khi khởi động. run tạo một bản sao sạch từ image nên không có những nội dung đó.
  • exec bỏ qua entrypoint. run sẽ chạy entrypoint.
  • run để lại một container, trừ khi bạn truyền `--rm`.

Dùng exec để kiểm tra chính xác những gì đang diễn ra. Dùng `run --rm cho một bản sao tạm thời của cùng môi trường, để chạy một migration command chỉ một lần, hoặc khi service thật không duy trì đủ lâu để bạn exec` vào.

Các flag hữu ích của exec: user, working directory và replicas

Hầu hết image chuyển sang user không phải root, vì vậy việc cài công cụ chẩn đoán bên trong shell exec sẽ dừng tại đây:

E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)

-u root cung cấp shell root trong chính container đó:

docker compose exec -u root web sh

-w /srv/app đặt working directory chỉ cho lệnh đó. -e KEY=value thêm một biến môi trường vào session của bạn, không thêm vào service. Khi một service chạy nhiều replica, --index 2 quyết định bạn sẽ vào container nào. Nếu bạn đang kiểm tra quyền sở hữu file trên một directory được mount, PUID và PGID trong image container giải thích vì sao numeric id, không phải user name, quyết định ai có quyền ghi vào đó.

Mở shell psql hoặc mysql bên trong container database

Client đã có sẵn trong image database, nên bạn không cần cài client trên host và cũng không cần publish port:

docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -p

Image Postgres có sẵn psql, image MySQL có sẵn mysql, còn image MariaDB có sẵn mariadb. Kết nối được thực hiện từ bên trong container, nên cách này vẫn hoạt động ngay cả khi file compose không publish bất kỳ database port nào. Đây là cách cấu hình an toàn hơn: không gì trên Internet có thể truy cập một port mà bạn không publish.

Có một lỗi dễ khiến bạn mất cả buổi chiều. Shell sẽ expand các biến trên host trước khi Docker nhận lệnh, nên -U "$POSTGRES_USER" sẽ gửi một chuỗi rỗng nếu biến đó chỉ tồn tại bên trong container. Dùng dấu nháy đơn và chạy shell bên trong container để expand biến đúng nơi:

docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

Không dùng docker compose run --rm db mà không có command ở đây. Lệnh đó sẽ khởi động một Postgres server thứ hai trên cùng data volume và server sẽ từ chối khởi động:

FATAL:  lock file "postmaster.pid" already exists

Lock file đang hoạt động đúng chức năng, vì hai server cùng ghi vào một data directory sẽ làm hỏng dữ liệu. Khi database đang chạy, hãy exec vào container đang hoạt động. Việc database có nên chạy trong Compose hay không là một quyết định riêng; chạy database trong Docker hoặc trên host trình bày các đánh đổi.

Service cần console khi khởi động: stdin_open và tty

execrun dùng cho các shell bạn mở thủ công. Một service có tiến trình chính vốn mang tính tương tác cần 2 khóa trong file compose:

services:
  console:
    image: python:3.12-slim
    command: python
    stdin_open: true
    tty: true

stdin_open: truedocker run -itty: truedocker run -t. Nếu thiếu 2 khóa này, container khởi động rồi thoát ngay với code 0, và docker compose ps -a hiển thị Exited (0). Không có lỗi crash. python không có terminal trên stdin sẽ đọc đến EOF ngay lập tức rồi thoát bình thường. Đây là hành vi đúng đối với một chương trình không có người nhập liệu.

Khi đã đặt cả 2 khóa, hãy attach vào tiến trình đang chạy:

docker attach $(docker compose ps -q console)

Nhấn Ctrl-P rồi Ctrl-Q để detach; tiến trình vẫn tiếp tục chạy. Tổ hợp này chỉ hoạt động khi container có cả TTY và stdin đang mở. Ngược lại, Ctrl-C sẽ gửi tín hiệu interrupt đến PID 1 và dừng service.

Để cả 2 khóa ở trạng thái tắt đối với các service thông thường. Web server không bao giờ đọc stdin. tty: true khiến nhiều chương trình chuyển sang xuất màu và line buffering vì chúng cho rằng có người đang theo dõi, làm docker compose logs chứa đầy escape code.

Vì sao lệnh exec dạng script lỗi trong cron và CI: cờ -T

Một lệnh exec chạy được trong terminal nhưng lại lỗi bên trong cron job hoặc continuous integration (CI) runner:

the input device is not a TTY

Compose mặc định yêu cầu pseudo terminal, còn cron không cấp terminal cho job, nên yêu cầu bị lỗi trước khi lệnh của bạn được chạy. -T sẽ tắt yêu cầu này:

0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump

-T quan trọng vì một lý do khác. TTY thay đổi byte stream khi stream đi ra, nên compressed dump đi qua TTY sẽ bị hỏng. Mọi output được redirect hoặc pipe đều cần -T.

Có thêm 2 chi tiết về cron. Truyền -f bằng absolute path, vì cron chạy job từ home directory, nơi không có compose file, rồi Compose dừng với no configuration file provided: not found. Ngoài ra, exec trả về exit code của lệnh mà nó chạy, nên một pg_dump bị lỗi sẽ làm script của bạn lỗi dưới set -e thay vì ghi một backup rỗng rồi báo thành công. Các lệnh thường dùng khác được tổng hợp trong cheat sheet các lệnh Compose, nên đặt tài liệu này cạnh những script đó.

Vì sao các thay đổi bên trong container biến mất

Bạn cài một tool bằng exec, sửa một file cấu hình, khắc phục sự cố, rồi một tuần sau bản sửa biến mất. Đây là cách writable layer của container hoạt động. docker compose up -d sau bất kỳ thay đổi nào đối với image tag hoặc service definition sẽ xóa container cũ và tạo container mới từ image. Mọi chỉnh sửa thủ công đều mất theo container cũ.

docker compose restart thì khác. Nó dừng rồi khởi động lại chính container đó, nên các chỉnh sửa thủ công vẫn còn. Vì vậy, một bản sửa thủ công có thể tồn tại nhiều tuần rồi biến mất trong một lần cập nhật không liên quan. Named volume và bind mount tồn tại sau cả hai thao tác, vì dữ liệu của chúng nằm bên ngoài container. bind mount và named volume giải thích nên chọn loại nào cho dữ liệu bạn muốn giữ lại.

Vì vậy, hãy xem shell mở bằng exec là nơi để đọc và kiểm tra. Khi đã xác định được cách sửa, hãy ghi nó vào nơi có thể tồn tại lâu dài: thêm package vào Dockerfile, thêm setting vào compose file. Sau đó docker compose up -d để áp dụng, rồi dùng một lệnh exec khác để xác nhận container mới thực sự có thay đổi đó.

FAQ

Sự khác nhau giữa docker compose exec và docker compose run là gì?

exec chạy một lệnh bên trong container đang chạy, bên cạnh tiến trình chính, và bỏ qua entrypoint của image. run tạo một container mới từ cùng định nghĩa service với cùng image, biến môi trường, volume và network, truyền lệnh của bạn qua entrypoint, đồng thời khởi động các service depends_on trước. run cũng không publish các port của service, trừ khi bạn thêm --service-ports. Dùng exec để kiểm tra service đang chạy. Dùng run --rm khi service đã dừng hoặc khi bạn không muốn ảnh hưởng đến service đó.

Vì sao docker compose exec báo service không chạy?

exec gắn vào một container hiện có và không thể tạo container mới, nên service đã dừng hoặc bị crash sẽ trả về service "web" is not running. Kiểm tra docker compose ps -a. Lệnh này liệt kê các container đã thoát với trạng thái như Exited (1). Đọc docker compose logs web để biết lý do container dừng. Nếu vẫn muốn mở shell, chạy docker compose run --rm --entrypoint sh web. Lệnh này tạo container mới từ cùng định nghĩa service mà không chạy lệnh khởi động đang bị lỗi.

Làm cách nào mở shell khi image không có bash?

docker compose exec web bash thất bại với exec: "bash": executable file not found in $PATH nghĩa là bash không có trong image. Đây là điều bình thường với các image được xây dựng trên Alpine. Dùng docker compose exec web sh, vì BusyBox cung cấp /bin/sh. Distroless và scratch image hoàn toàn không có shell, nên không lệnh exec nào hoạt động được. Chuyển sang tag :debug của image nếu nhà phát hành có cung cấp, hoặc khởi động một debug container trong namespace của target bằng docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot. Giá trị $CID lấy từ docker compose ps -q web.

Vì sao lệnh exec của tôi thất bại với lỗi "the input device is not a TTY" trong cron?

docker compose exec mặc định yêu cầu pseudo terminal, nhưng cron không cung cấp terminal nào, nên yêu cầu thất bại trước khi lệnh của bạn chạy. Thêm -T để tắt tùy chọn này: docker compose exec -T db pg_dump -U postgres app. Với mọi output được redirect hoặc pipe, cũng dùng -T, vì TTY thay đổi byte stream và làm hỏng binary dump. Trong cron, hãy truyền thêm -f cùng đường dẫn tuyệt đối đến file compose. Nếu không, Compose sẽ thoát với no configuration file provided: not found.

Các thay đổi tôi thực hiện bên trong container bằng exec có tồn tại sau khi restart không?

Các thay đổi đó tồn tại sau docker compose restart, vì thao tác này dùng lại cùng container. Chúng bị mất sau docker compose up -d khi image hoặc cấu hình thay đổi, vì thao tác này tạo lại container từ image và loại bỏ writable layer của container. Dữ liệu ghi vào named volume hoặc bind mount tồn tại trong cả hai trường hợp, vì dữ liệu nằm bên ngoài container. Hãy dùng exec để thực hiện các thay đổi phục vụ chẩn đoán, sau đó đưa cấu hình lâu dài vào Dockerfile hoặc file compose.