我們在跑 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 執行。
相關連結