Docker Compose exec 取得互動式 shell 指令
使用 docker compose exec 進入執行中的服務容器;服務已停止或不想干擾現有容器時,改用 run --rm 建立臨時 shell。
使用 docker compose exec 取得互動式 shell
docker compose exec web bash 會在已以 web 服務執行中的容器內開啟互動式 shell。exec 後面的名稱是 compose.yaml 中的服務名稱,不是容器名稱。如果映像沒有 bash,請改用 sh。
docker compose ps
docker compose exec web bash先執行 docker compose ps。輸出應列出 web,其狀態為 running。接著,第二個命令會將提示字元帶到容器內;輸入 exit 或按 Ctrl-D 即可返回主機。離開後服務仍會繼續執行,因為 exec 會在主要程序旁啟動第二個程序。關閉 shell 不會影響 PID 1(process id 1),也就是容器建立時預定執行的程序。
這是進入容器的兩種方式之一。exec 會加入已存在的容器。docker compose run 則會依相同的服務定義建立新容器。本指南幾乎所有其他內容,都是由這項差異延伸而來。
為什麼 Compose 中的 -it 可選,但使用原生 docker 時卻是必要的
兩個旗標會控制工作階段的互動功能。-i 會保持 stdin 開啟,讓您輸入的內容傳送至程序。-t 會配置虛擬終端機(TTY),讓 shell 顯示提示字元並處理方向鍵。原生 docker exec 預設會關閉這兩項功能,因此您看到的每個範例都會寫入 docker exec -it。docker compose exec 會替您同時開啟兩者,因此 docker compose exec -it web bash 與 docker compose exec web bash 的作用相同。Compose 仍接受 -it,讓熟悉舊用法的使用者可以繼續使用。
您能在幾秒內察覺缺少 TTY。shell 會執行,但不會顯示提示字元,Ctrl-C 也無法傳送至程序。相反地,若您必須要求 Compose 不配置 TTY,則有專用旗標,並在後文另有專節說明。
沒有 bash 時的處理方式
對以 Alpine 為基礎的 image 要求使用 bash 時,執行 exec 會失敗,如下所示:
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown這項訊息不是 exec 本身的問題。它表示你要求執行的 binary 不在該 image 中。Alpine 內建 BusyBox,將 ash 提供為 /bin/sh,完全不包含 bash,因此應改為要求 sh:
docker compose exec web sh以 Debian 和 Ubuntu 為基礎的 image(包括 -slim tags)則包含 bash。bash 也提供 command history 和較完整的 completion。因此,請先嘗試 bash,失敗後再改用 sh。幾乎所有通用用途的 image 都包含 sh。
有些 image 完全沒有 shell。Distroless image,以及以 FROM scratch 建置的 image,會刻意只包含 application binary 及其 libraries,不包含其他內容,因為不存在的 shell 無法被攻擊者利用。在這些 image 中,sh 會因相同原因失敗,也沒有其他 shell 可供嘗試。有兩種方式可行。Google 的 distroless images 提供加入 BusyBox shell 的 :debug tags,因此可以暫時切換 tag 以進入容器。或者,在目標容器的 namespaces 中啟動另一個 container:
CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot現在,netshoot 的工具已指向 application 的 network,因此 curl localhost:8080 和 ss -lntp 的行為就像在該 application 內部執行。你看到的 filesystem 屬於 netshoot,而不是該 app。由於共用 process namespace,當你是 root 時,ls /proc/1/root/ 可以存取目標容器自己的檔案。
服務未執行時,請使用 docker compose run --rm
exec 需要容器正在執行。若指定已停止的服務,指令會拒絕執行:
service "web" is not running它不會替你啟動任何項目。docker compose run 會:
docker compose run --rm web bashrun 會依據 web 服務定義建立新容器,並使用相同的映像、環境變數、磁碟區與網路;同時將服務的命令替換為你輸入的命令。--rm 會在你離開時刪除該容器。若省略 --rm,殘留容器會以 myproject-web-run-4f1c2b 等名稱累積;docker compose ps -a 會列出這些容器,而其他機制不會清理它們。
run 有兩項容易令人意外的行為。除非加入 --service-ports,否則它不會發布服務的連接埠。這是刻意的設計:如果第一個容器仍占用主機的 8080 連接埠,第二個容器綁定相同連接埠時會因 bind: address already in use 而失敗。它也會在顯示 shell 之前,先啟動服務在 depends_on 下列出的所有項目。因此,快速檢查容器內容可能會啟動資料庫與快取服務。--no-deps 可跳過這個步驟。
run 會經過映像的 ENTRYPOINT,但 exec 不會。exec 會直接在現有容器中啟動你的命令,因此 entrypoint script 不會接收到該命令。在 run 下,你的 bash 會以引數形式傳遞給該 script。許多官方映像會以 exec "$@" 結束 entrypoint,因此命令會直接傳遞下去,讓你取得 shell。若 script 會自行解讀引數,則會以其他方式處理這些引數;此時可針對該次執行替換 entrypoint:
docker compose run --rm --entrypoint sh web這是命令在 exec 下可正常運作、但在 run 下行為不同的最常見原因。命令與 entrypoint 的區分說明了每次執行時,你替換的是映像設定中的哪一部分。
exec 或 run:如何選擇
- exec 需要執行中的容器。run 不需要,且 run 可能會啟動相依服務。
- exec 會看到目前正在執行的程序清單與檔案,包括應用程式啟動後寫入的內容。run 會取得映像檔的乾淨副本,因此不會包含這些內容。
- exec 會略過 entrypoint。run 會執行 entrypoint。
- 除非傳入
--rm,否則 run 會留下容器。
使用 exec 查看目前實際發生的情況。使用 run --rm 建立相同環境的一次性副本、執行單次的 migration 命令,或在實際服務無法維持足夠長的執行時間以便使用 exec 進入時使用。
實用的 exec 旗標:使用者、工作目錄與複本
大多數映像檔會切換至非 root 使用者,因此若要在 exec shell 中安裝診斷工具,必須使用以下方式:
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)-u root會在同一個容器中提供 root shell:
docker compose exec -u root web sh-w /srv/app只會為該命令設定工作目錄。-e KEY=value會將環境變數加入目前的工作階段,不會加入服務。服務執行多個複本時,--index 2會決定你要進入哪個容器。若你要追查掛載目錄的檔案擁有權問題,容器映像檔中的 PUID 與 PGID 說明了為何決定寫入權限的是數值 ID,而不是使用者名稱。
在資料庫容器內取得 psql 或 mysql shell
用戶端已經包含在資料庫映像檔中,因此主機不需要安裝用戶端,也不需要發布連接埠:
docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -pPostgres 映像檔包含 psql,MySQL 映像檔包含 mysql,MariaDB 映像檔包含 mariadb。連線是在容器內建立,因此即使 Compose 檔案完全沒有發布資料庫連接埠,也能正常運作。這是較安全的配置:未發布的連接埠不會被網際網路上的任何主機存取。
有一個陷阱曾讓許多人花上一個下午排查。Docker 看到命令之前,shell 就會先在主機上展開變數,因此當該變數只存在於容器內時,-U "$POSTGRES_USER" 傳送的是空字串。使用單引號,並在容器內啟動 shell,才能在正確的位置展開變數:
docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'這裡不要在沒有命令的情況下使用 docker compose run --rm db。這會在相同的資料磁碟區上啟動第二個 Postgres 伺服器,而該伺服器會拒絕啟動:
FATAL: lock file "postmaster.pid" already exists鎖定檔案正在發揮作用,因為兩個伺服器同時寫入同一個資料目錄會造成資料損毀。資料庫執行期間,請使用 exec 進入執行中的容器。資料庫是否應該放在 Compose 中是另一項決策,在 Docker 或主機上執行資料庫 說明了其中的取捨。
啟動時需要主控台的服務:stdin_open 與 tty
exec 和 run 適用於您手動開啟的 shell。主要程序本身需要互動操作的服務,必須在 compose 檔案中設定兩個鍵:
services:
console:
image: python:3.12-slim
command: python
stdin_open: true
tty: truestdin_open: true 是 docker run -i,而 tty: true 是 docker run -t。未設定這兩個鍵時,容器會立即以代碼 0 啟動並結束,且 docker compose ps -a 會顯示 Exited (0)。這不是當機。python 在 stdin 沒有終端機時會立即讀到檔案結尾,然後正常結束;對於沒有人輸入內容的程式而言,這是正確行為。
設定這兩個鍵後,連接至執行中的程序:
docker attach $(docker compose ps -q console)按下 Ctrl-P,再按 Ctrl-Q 即可離開連接,同時讓程序繼續執行。只有在容器同時啟用 TTY 且保持 stdin 開啟時,這組按鍵才有效。Ctrl-C 則會將中斷訊號傳送給 PID 1,並停止服務。
一般服務應保持這兩個鍵未設定。Web 伺服器不會讀取 stdin,而 tty: true 會讓許多程式切換為彩色輸出及行緩衝,因為它們會判定有人正在查看輸出,導致 docker compose logs 被跳脫碼填滿。
在 cron 和 CI 中執行腳本失敗的原因:-T 旗標
在終端機中可正常執行的 exec 命令,在 cron 工作或持續整合(CI)執行器中卻失敗:
the input device is not a TTYCompose 預設會要求配置 pseudo terminal,但 cron 不會為工作提供終端機,因此命令尚未執行前,該要求就會失敗。-T 可停用此要求:
0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump-T 還有另一個重要原因。TTY 會在輸出資料流離開前改寫其中的位元組,因此,經過 TTY 傳送的壓縮傾印檔抵達時會損毀。所有重新導向或透過管線傳送的輸出都需要使用 -T。
cron 還有兩個細節需要注意。使用絕對路徑傳入 -f,因為 cron 會從家目錄執行工作,而該目錄沒有 compose 檔案,Compose 隨後會以 no configuration file provided: not found 停止。exec 會回傳所執行命令的結束碼,因此失敗的 pg_dump 會在 set -e 下使腳本失敗,而不是寫入空的備份並回報成功。其他日常命令整理在Compose 命令速查表中,適合與這些腳本放在一起參考。
為什麼在容器內進行的變更會消失
你使用 exec 安裝工具、編輯設定檔並修正問題,但一週後修正內容卻消失了。這是容器的可寫入層依設計運作的結果。docker compose up -d 對映像標籤或服務定義進行任何變更後,都會刪除舊容器,並根據映像建立新容器;所有手動編輯內容也會隨舊容器一併消失。
docker compose restart 則不同。它會停止並重新啟動同一個容器,因此手動編輯內容仍會保留。這就是為什麼手動修正看似可以維持數週,卻會在無關的更新期間消失。Named volumes 和 bind mounts 在這兩種操作中都能保留,因為其資料位於容器外部;bind mounts 與 named volumes 說明了應如何為預計保留的資料選擇其中一種。
因此,請將 exec shell 視為用來讀取和測試的環境。確認修正方式後,請將它寫入可持久保存的位置:將套件寫入 Dockerfile,將設定寫入 compose file。接著執行 docker compose up -d 套用變更,再使用另一個 exec 確認新容器確實包含該修正。
FAQ
docker compose exec 和 docker compose run 有何差異?
exec 會在已經執行中的容器內、主程序之外執行命令,且會略過映像的 entrypoint。run 會依相同的服務定義,使用相同的映像、環境變數、磁碟區與網路建立新容器,將命令傳入 entrypoint,並先啟動所有 depends_on 服務。除非加入 --service-ports,否則 run 不會發布該服務的連接埠。使用 exec 檢查正在執行的服務。服務已停止,或不希望影響服務時,使用 run --rm。
為什麼 docker compose exec 會顯示服務未執行?
exec 會連接至現有容器,無法建立容器。因此,服務已停止或當機時會顯示 service "web" is not running。檢查 docker compose ps -a;該命令會列出已結束的容器,其狀態可能顯示為 Exited (1)。再讀取 docker compose logs web,查看容器停止的原因。若仍要取得 shell,請執行 docker compose run --rm --entrypoint sh web。這會依相同的服務定義建立新的容器,且不會執行有問題的啟動命令。
映像沒有 bash 時,如何開啟 shell?
docker compose exec web bash 若失敗並顯示 exec: "bash": executable file not found in $PATH,表示映像中沒有 bash。這對以 Alpine 為基礎建立的映像很正常。請使用 docker compose exec web sh,因為 BusyBox 提供 /bin/sh。Distroless 與 scratch 映像完全不包含 shell,因此任何 exec 命令都無法運作。若發布者提供該映像的 :debug 標籤,請改用該標籤;或者使用 docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot 在目標容器的 namespace 中啟動 debug 容器,其中 $CID 來自 docker compose ps -q web。
為什麼在 cron 中執行 exec 命令時,會顯示「the input device is not a TTY」?
docker compose exec 預設會要求 pseudo terminal,但 cron 不會提供,因此命令尚未執行前要求就會失敗。加入 -T 將其關閉:docker compose exec -T db pg_dump -U postgres app。對任何重新導向或管線輸出的內容,也請使用 -T,因為 TTY 會改變位元組串流,導致 binary dump 損壞。在 cron 中,也請傳入 -f,並指定 compose file 的絕對路徑,否則 Compose 會結束並顯示 no configuration file provided: not found。
使用 exec 在容器內進行的變更,重新啟動後是否仍會保留?
這些變更會在 docker compose restart 後保留,因為該操作會重用相同的容器。若映像或設定有所變更,執行 docker compose up -d 後變更會遺失,因為該操作會依映像重新建立容器,並捨棄容器的可寫入層。寫入 named volume 或 bind mount 的資料在這兩種情況下都會保留,因為資料位於容器之外。請使用 exec 進行診斷性變更,再將永久設定寫入 Dockerfile 或 compose file。