我們在跑 Docker 容器時,經常會用 -it
這個參數來啟動容器,並且在容器中執行命令。不過長久以來我並沒有真正深入理解這兩個參數的用法與使用情境,以至於我昨天在跑一個 MySQL 容器時,就又再次遇到相關的問題。經過深入研究後發現,覺得這背後的基礎原理還是要知道才行,所以這篇文章我打算來好好說明一下這兩個參數選項的技術細節! 👍
幾個常見的容器執行情境
以下我先描述幾種不同的容器執行情境:
-
正常執行容器 (須互動、須傳入資料)
這裡用了一個常見的 -it
命令列參數,通常你要進入容器的 Shell 環境,通常都要加這個參數。因為 -t
會建立一個虛擬終端機(pseudo-TTY
)讓你可以模擬在容器中使用終端機的環境,好讓你可以跟 Shell 進行互動。而 -i
則是讓容器在執行時持續開啟 STDIN
管道,讓你在虛擬終端機中打字的時候,可以從 Host 傳入到 Container 之中,這當中就是透過 STDIN
管道傳入的,因此你一定要加上 -i
參數。
docker run --name=ubuntu --rm -it ubuntu:latest
-
直接讓容器應用程式在背景執行 (無須互動、無須傳入資料)
這裡用了一個常見的 -d
命令列參數,通常你要將容器跑在背景當服務執行,都會讓容器進入 detach mode
(卸離模式)。當容器中的應用程式在執行時,通常不太需要使用到虛擬終端機的功能,因此不需要特別加上 -t
參數。同時,你也不會有需要開啟 STDIN
通道來使用,所以也不用特別加上 -i
參數。
docker run --name=nginx --rm -d -p 80:80 nginx:latest
-
程式會在執行容器後直接退出 (無須互動、無須傳入資料)
有時候你很有可能只是單純想執行一個應用程式而已,例如會在每個小時執行一次的排程工作(Scheduled Task),那麼你也不需要特別加上 -t
或 -i
等參數。
docker run --rm hello-world
不過,如果你執行 ubuntu:latest
的 bash
命令,一個需要虛擬終端機的 Shell 環境,卻不指定 -i
與 -t
的話,該容器會在啟動後立刻結束,因為你根本沒有什麼可以跟 Shell 環境互動的地方,也就是所謂的虛擬終端機。
docker run --name=ubuntu --rm ubuntu:latest bash
即便你再次啟動容器,也是完全無效的!
docker start ubuntu
這很明顯就是個錯誤示範,以下是刪除容器的命令:
docker rm ubuntu
-
程式會在執行容器後直接退出 (無須互動、但須傳入資料)
有時候你很有可能只是單純想執行一個應用程式而已,但會希望可以透過 STDIN
傳入一些資料,例如你想要匯入資料到 MySQL 資料庫中,就必須要用 Pipe 的方式傳入資料。此時,你就必須要用 -i
將 STDIN
管道開啟,但不需要透過虛擬終端機進行互動。
假設我們先啟動一個 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'
這個訊息沒有很容易懂,但簡單來說,這個命令根本不需要一個虛擬終端機來跟 mysql
命令互動,因為我們很單純的只是想把資料透過 STDIN
傳入,執行完後直接結束程式即可,因此加上 -t
是錯誤的用法,千萬不要被錯誤訊息給誤導,開始嘗試 winpty
這種邪魔歪道的技法。
-
直接讓容器應用程式在背景執行 (可能須互動、可能須傳入資料)
假設我們想啟動一個 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
組合快速鍵才有效果!
幾個錯誤的容器執行用法
-
僅使用 -i
參數,忘記加上 -t
參數
如下命令執行了一個 ubuntu:latest
容器,並啟動 bash
執行:
docker run -i --rm ubuntu:latest bash
這個命令所代表的意思是,將 STDIN
掛進去,但沒有啟動虛擬終端機,因此容器中的 Shell 環境無法處理「終端機」的相關操作,只能識別「資料」,因此當你在啟動容器後輸入 ls
並按下 Enter
鍵,你的 bash
收到的命令並不是 ls
命令,而是 ls\r
命令,所以就失敗了!
這時如果你按下 Ctrl-C
就會導致 bash
結束,因為沒有虛擬終端機的關係,你輸入的 Ctrl-C
直接傳給了 bash
執行,他收到之後就直接結束程序了,不像我們平常在 Shell 環境中按下 Ctrl-C
那樣單純的中斷命令。
-
僅使用 -t
參數,忘記加上 -i
參數
只建立了虛擬終端機,卻沒有傳入 STDIN
串流,此時畫面上雖然會出現 bash
的提示字元,但你完全無法輸入的。
docker run -t --rm --name=ubuntu ubuntu:latest bash
此時你若按下 Ctrl-C
容器也不會結束,因為你傳不進去,然而你也跳不出來,因為 docker run
不會理會你的 Ctrl-C
訊號。
-
僅使用 -t
參數,忘記加上 -i
參數,同時又用了 -d
進入卸離模式
只建立了虛擬終端機,就可以確保 bash
還活著,不過因為沒有 -i
的關係,下次 docker attach
一樣無法輸入任何命令:
基本上沒有這種使用情境!
docker run -t -d --rm --name=ubuntu ubuntu:latest bash
此時你若按下 Ctrl-C
容器並不會結束,因為你沒有 STDIN
管道可以傳入命令,但你可以跳出容器,那是因為 docker attach
會處理 Ctrl-C
訊號。
-
僅使用 -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
執行。
相關連結