The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

深入剖析 docker run 與 docker exec 的 -i 與 -t 技術細節

我們在跑 Docker 容器時,經常會用 -it 這個參數來啟動容器,並且在容器中執行命令。不過長久以來我並沒有真正深入理解這兩個參數的用法與使用情境,以至於我昨天在跑一個 MySQL 容器時,就又再次遇到相關的問題。經過深入研究後發現,覺得這背後的基礎原理還是要知道才行,所以這篇文章我打算來好好說明一下這兩個參數選項的技術細節! 👍

TTY & STDIN

幾個常見的容器執行情境

以下我先描述幾種不同的容器執行情境:

  1. 正常執行容器 (須互動、須傳入資料)

    這裡用了一個常見的 -it 命令列參數,通常你要進入容器的 Shell 環境,通常都要加這個參數。因為 -t 會建立一個虛擬終端機(pseudo-TTY)讓你可以模擬在容器中使用終端機的環境,好讓你可以跟 Shell 進行互動。而 -i 則是讓容器在執行時持續開啟 STDIN 管道,讓你在虛擬終端機中打字的時候,可以從 Host 傳入到 Container 之中,這當中就是透過 STDIN 管道傳入的,因此你一定要加上 -i 參數。

    docker run --name=ubuntu --rm -it ubuntu:latest
    
  2. 直接讓容器應用程式在背景執行 (無須互動、無須傳入資料)

    這裡用了一個常見的 -d 命令列參數,通常你要將容器跑在背景當服務執行,都會讓容器進入 detach mode (卸離模式)。當容器中的應用程式在執行時,通常不太需要使用到虛擬終端機的功能,因此不需要特別加上 -t 參數。同時,你也不會有需要開啟 STDIN 通道來使用,所以也不用特別加上 -i 參數。

    docker run --name=nginx --rm -d -p 80:80 nginx:latest
    
  3. 程式會在執行容器後直接退出 (無須互動、無須傳入資料)

    有時候你很有可能只是單純想執行一個應用程式而已,例如會在每個小時執行一次的排程工作(Scheduled Task),那麼你也不需要特別加上 -t-i 等參數。

    docker run --rm hello-world
    

    不過,如果你執行 ubuntu:latestbash 命令,一個需要虛擬終端機的 Shell 環境,卻不指定 -i-t 的話,該容器會在啟動後立刻結束,因為你根本沒有什麼可以跟 Shell 環境互動的地方,也就是所謂的虛擬終端機

    docker run --name=ubuntu --rm ubuntu:latest bash
    

    即便你再次啟動容器,也是完全無效的!

    docker start ubuntu
    

    這很明顯就是個錯誤示範,以下是刪除容器的命令:

    docker rm ubuntu
    
  4. 程式會在執行容器後直接退出 (無須互動、但須傳入資料)

    有時候你很有可能只是單純想執行一個應用程式而已,但會希望可以透過 STDIN 傳入一些資料,例如你想要匯入資料到 MySQL 資料庫中,就必須要用 Pipe 的方式傳入資料。此時,你就必須要用 -iSTDIN 管道開啟,但不需要透過虛擬終端機進行互動。

    假設我們先啟動一個 MySQL 資料庫:

    docker run --name=db -d --rm -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=mydb -p 3306:3306 mysql:8.0.29
    

    如果我們要傳入一段很常的 SQL 執行,你就必須這樣寫:

    echo 'SHOW DATABASES' | docker exec -i db mysql -u root -p123456
    

    不過,如果你加上 -it 參數的話,就會出現錯誤:

    echo 'SHOW DATABASES' | docker exec -it db mysql -u root -p123456
    
    the input device is not a TTY.  If you are using mintty, try prefixing the command with 'winpty'
    

    the input device is not a TTY.  If you are using mintty, try prefixing the command with 'winpty'

    這個訊息沒有很容易懂,但簡單來說,這個命令根本不需要一個虛擬終端機來跟 mysql 命令互動,因為我們很單純的只是想把資料透過 STDIN 傳入,執行完後直接結束程式即可,因此加上 -t 是錯誤的用法,千萬不要被錯誤訊息給誤導,開始嘗試 winpty 這種邪魔歪道的技法。

  5. 直接讓容器應用程式在背景執行 (可能須互動、可能須傳入資料)

    假設我們想啟動一個 SQL Server 服務,可以參考 Quickstart: Run SQL Server Linux container images with Docker 文件上的命令來執行:

    docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=YourStrong@Passw0rd" -p 1433:1433 --name sql1 --hostname sql1 -d mcr.microsoft.com/mssql/server:2022-latest
    

    你可以發現他並沒有使用 -it 參數,但有使用 -d 參數,這意味著你無法跟該容器的主要應用程式互動,只能透過 docker logs 命令來查詢該應用程式輸出在 STDOUT 的內容:

    docker logs sql1
    

    如果你執行以下 docker attach 命令,那你就慘了,因為你目前的終端機環境將會完全無法回應,因為你完全無法跟容器內的應用程式互動!

    docker attach sql1
    

    此時你只能開啟另一個視窗,並且將容器停用或刪除,原本的終端機環境才能離開。

    如果我們在 SQL Server 容器啟動後,還想跟容器進行互動,那就一定要在執行容器時加入 -it 參數,先把虛擬終端機與 STDIN 通道開啟,讓之後透過 docker attach 掛載到目前終端機時可以進行互動!

    docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=YourStrong@Passw0rd" -p 1433:1433 --name sql1 --hostname sql1 -d -it mcr.microsoft.com/mssql/server:2022-latest
    

    加上 -it 之後,此時就有趣了,因為你有兩種不同的方法透過 docker attach 將容器中的應用程式掛載到當前命令列環境中:

    第一種:附加容器時,不要開啟 STDIN 通道!

    docker attach 命令加入 --no-stdin 參數,可以讓你跳入 SQL Server 容器「查看」應用程式回應的訊息,因為你有個虛擬終端機可以跟應用程式進行互動,但你不能「輸入」任何字元到應用程式之中,這種情況你就只能「看」不能「碰」,只有「輸出」、沒有「輸入」。

    docker attach --no-stdin sql1
    

    你在執行上述命令後,按下 Ctrl-C 快速鍵,事實上並不會傳給 SQL Server 容器,因為你無法輸入任何訊息到容器中,此時 STDIN 管道是關閉的!所以你的 Ctrl-C 事實上是傳給了 docker.exe 程式,而這個命令會讓你跳出 docker attach 命令執行,回到原本的終端機環境中。

    第二種:附加容器時,預設會開啟 STDIN 通道!

    執行 docker attach 命令時,不但有虛擬終端機可用,還可以從輸入資料到容器中,且該資料也會傳入容器正在執行的應用程式中。

    docker attach sql1
    

    當你在執行 docker attach 之後,由於 SQL Server 主程式並不是一個 Shell 環境,所以你輸入任何資料都是沒用的,而當 SQL Server 主程式沒有輸出 Log 時,你也有可能會感覺畫面凍結的感覺,但這並不代表程式不動了,而是他不會理會你的終端機操作而已。此時,當你按下 Ctrl-C 快速鍵,此時你會發現是 SQL Server 容器中的應用程式會接收到的 SIGINT 訊號,所以 SQL Server 容器中的應用程式就會停止執行,而容器也會自動停止!

    如果你希望 docker attach 之後,不要將程式停止中斷,你要在不能按下 Ctrl-C 的情況下跳出,就只能按下 Ctrl-p Ctrl-q 組合快速鍵,按完就會跳離容器,回到原本的命令提示字元視窗!不過,只有你的 docker run 在使用 -it 參數的時候,這個 Ctrl-p Ctrl-q 組合快速鍵才有效果!

幾個錯誤的容器執行用法

  1. 僅使用 -i 參數,忘記加上 -t 參數

    如下命令執行了一個 ubuntu:latest 容器,並啟動 bash 執行:

    docker run -i --rm ubuntu:latest bash
    

    這個命令所代表的意思是,將 STDIN 掛進去,但沒有啟動虛擬終端機,因此容器中的 Shell 環境無法處理「終端機」的相關操作,只能識別「資料」,因此當你在啟動容器後輸入 ls 並按下 Enter 鍵,你的 bash 收到的命令並不是 ls 命令,而是 ls\r 命令,所以就失敗了!

    image

    這時如果你按下 Ctrl-C 就會導致 bash 結束,因為沒有虛擬終端機的關係,你輸入的 Ctrl-C 直接傳給了 bash 執行,他收到之後就直接結束程序了,不像我們平常在 Shell 環境中按下 Ctrl-C 那樣單純的中斷命令。

  2. 僅使用 -t 參數,忘記加上 -i 參數

    只建立了虛擬終端機,卻沒有傳入 STDIN 串流,此時畫面上雖然會出現 bash 的提示字元,但你完全無法輸入的。

    docker run -t --rm --name=ubuntu ubuntu:latest bash
    

    此時你若按下 Ctrl-C 容器也不會結束,因為你傳不進去,然而你也跳不出來,因為 docker run 不會理會你的 Ctrl-C 訊號。

  3. 僅使用 -t 參數,忘記加上 -i 參數,同時又用了 -d 進入卸離模式

    只建立了虛擬終端機,就可以確保 bash 還活著,不過因為沒有 -i 的關係,下次 docker attach 一樣無法輸入任何命令:

    基本上沒有這種使用情境!

    docker run -t -d --rm --name=ubuntu ubuntu:latest bash
    

    此時你若按下 Ctrl-C 容器並不會結束,因為你沒有 STDIN 管道可以傳入命令,但你可以跳出容器,那是因為 docker attach 會處理 Ctrl-C 訊號。

  4. 僅使用 -i 參數,忘記加上 -t 參數,同時又用了 -d 進入卸離模式

    如下命令執行了一個 ubuntu:latest 容器,並啟動 bash 執行,但立即卸離終端機環境:

    docker run -i -d --rm --name=ubuntu ubuntu:latest bash
    

    這段命令是可以成功執行的,他有開啟 STDIN 管道,且沒有虛擬終端機,又加上 -d 卸離模式。當你透過 docker attach ubuntu 進去後,一樣沒有虛擬終端機可用,但因為還是有 STDIN 可用,所以你可以輸入命令,容器中的 bash 也會收到資料,但就是無法識別你所輸入的命令,原因跟第 1 點相同。

    這裡按下 Ctrl-C 之後,這個 docker attach 預設會對 bash 送出 SIGINT 訊號,而 bash 收到這個訊號後,就不會再接收任何命令與資料,因為容器不會停止,整個畫面就卡住了,此時你必須停用該容器才能跳出來。如果你希望 docker attach 可以在按下 Ctrl-C 的時候,對 bash 送出 SIGKILL 訊號,那你必須這樣執行:

    docker attach --sig-proxy=false ubuntu
    

    docker attach--sig-proxy 參數預設為 true,因此按下 Ctrl-C 其實會送出 SIGINT 中斷訊號。

    官網文件可以得知,當你使用 --sig-proxy=false 參數時 (Signal Proxy),使用者按下 Ctrl-C 的時候會送出 SIGKILL 訊號,理論上 bash 應該會停止,但事實卻不然。我想了一下,當你不使用 --sig-proxy 的時候,就代表 Docker 不會幫你代理轉送 Signal 到容器中,反而是會送到 docker.exe 程序,所以事實上你是將 SIGKILL 傳給了 docker attach 命令,所以變成是該命令被停止了!😅

總結

只用 -i 的使用情境是有的,就是單純要用 STDIN 透過 pipe 傳入資料到容器時!

只用 -t 的使用情境應該很少見,開虛擬終端機(pseudo-TTY)給 Container 用,可是不讓你 STDIN,也就是不能人工輸入命令或透過 Pipe 傳入資料的意思。

我能想到唯一的使用情境,應該是 Container 內的程式需要依賴 pseudo-TTY 才能運作,但又不需要 STDIN 輸入,單純的只需要 STDOUT 而已。這時程式不會死掉,會繼續執行,也會傳 STDOUT 出來。

另一種可能的情境,透過 Shell Script 登入 PTT 這樣的 BBS 站台,你不需要人為介入操作,因為你主要透過 Shell Script 自動化執行,不過執行過程需要 pseudo-TTY 才能跟跟 BBS 進行互動,同時還能得到 Console Color 支援。但這種應用情境應該極為少見才對!

最後總結一下心得:

  • 如果要執行 Shell 通常會用 -it 一起用。
  • 如果要執行背景服務,通常都不用加 -it 就能跑。
  • 如果要在容器中執行小工具,需要 pipe 資料進去執行,那就用 -i 即可。
  • 如果要在容器中透過 pseudo-TTY 自動化一些程式執行(例如登入PTT),那就用 -t 執行。

相關連結