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

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

![TTY & STDIN](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/58b34f4e8f1aac85f033-203151909-a600a78f-af3d-44e9-bf26-82a7707fae44.png)

### 幾個常見的容器執行情境

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

1.  正常執行容器 (須互動、須傳入資料)
    
    這裡用了一個常見的 `-it` 命令列參數，通常你要進入容器的 Shell 環境，通常都要加這個參數。因為 `-t` 會建立一個**虛擬終端機**(`pseudo-TTY`)讓你可以模擬在容器中使用終端機的環境，好讓你可以跟 Shell 進行互動。而 `-i` 則是讓容器在執行時持續開啟 `STDIN` 管道，讓你在**虛擬終端機**中打字的時候，可以從 Host 傳入到 Container 之中，這當中就是透過 `STDIN` 管道傳入的，因此你一定要加上 `-i` 參數。
    
    ```bash
    docker run --name=ubuntu --rm -it ubuntu:latest
    ```
    
2.  直接讓容器應用程式在背景執行 (無須互動、無須傳入資料)
    
    這裡用了一個常見的 `-d` 命令列參數，通常你要將容器跑在背景當服務執行，都會讓容器進入 `detach mode` (卸離模式)。當容器中的應用程式在執行時，通常不太需要使用到**虛擬終端機**的功能，因此不需要特別加上 `-t` 參數。同時，你也不會有需要開啟 `STDIN` 通道來使用，所以也不用特別加上 `-i` 參數。
    
    ```bash
    docker run --name=nginx --rm -d -p 80:80 nginx:latest
    ```
    
3.  程式會在執行容器後直接退出 (無須互動、無須傳入資料)
    
    有時候你很有可能只是單純想執行一個應用程式而已，例如會在每個小時執行一次的排程工作(Scheduled Task)，那麼你也不需要特別加上 `-t` 或 `-i` 等參數。
    
    ```bash
    docker run --rm hello-world
    ```
    
    不過，如果你執行 `ubuntu:latest` 的 `bash` 命令，一個需要**虛擬終端機**的 Shell 環境，卻不指定 `-i` 與 `-t` 的話，該容器會在啟動後立刻結束，因為你根本沒有什麼可以跟 Shell 環境互動的地方，也就是所謂的**虛擬終端機**。
    
    ```bash
    docker run --name=ubuntu --rm ubuntu:latest bash
    ```
    
    即便你再次啟動容器，也是完全無效的！
    
    ```bash
    docker start ubuntu
    ```
    
    這很明顯就是個錯誤示範，以下是刪除容器的命令：
    
    ```bash
    docker rm ubuntu
    ```
    
4.  程式會在執行容器後直接退出 (無須互動、但須傳入資料)
    
    有時候你很有可能只是單純想執行一個應用程式而已，但會希望可以透過 `STDIN` 傳入一些資料，例如你想要匯入資料到 MySQL 資料庫中，就必須要用 Pipe 的方式傳入資料。此時，你就必須要用 `-i` 將 `STDIN` 管道開啟，但不需要透過**虛擬終端機**進行互動。
    
    假設我們先啟動一個 MySQL 資料庫：
    
    ```bash
    docker run --name=db -d --rm -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=mydb -p 3306:3306 mysql:8.0.29
    ```
    
    如果我們要傳入一段很常的 SQL 執行，你就必須這樣寫：
    
    ```bash
    echo 'SHOW DATABASES' | docker exec -i db mysql -u root -p123456
    ```
    
    不過，如果你加上 `-it` 參數的話，就會出現錯誤：
    
    ```bash
    echo 'SHOW DATABASES' | docker exec -it db mysql -u root -p123456
    ```
    
    ```bash
    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'](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/6e603e2a415e26108168-203132800-958f5691-b69e-4cbb-ae20-d5d2f7e947cb.png)
    
    這個訊息沒有很容易懂，但簡單來說，這個命令根本不需要一個虛擬終端機來跟 `mysql` 命令互動，因為我們很單純的只是想把資料透過 `STDIN` 傳入，執行完後直接結束程式即可，因此加上 `-t` 是錯誤的用法，千萬不要被錯誤訊息給誤導，開始嘗試 `winpty` 這種邪魔歪道的技法。
    
5.  直接讓容器應用程式在背景執行 (可能須互動、可能須傳入資料)
    
    假設我們想啟動一個 SQL Server 服務，可以參考 [Quickstart: Run SQL Server Linux container images with Docker](https://learn.microsoft.com/en-us/sql/linux/quickstart-install-connect-docker?view=sql-server-ver16&pivots=cs1-powershell&WT.mc_id=DT-MVP-4015686) 文件上的命令來執行：
    
    ```bash
    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` 的內容：
    
    ```bash
    docker logs sql1
    ```
    
    如果你執行以下 `docker attach` 命令，那你就慘了，因為你目前的終端機環境將會完全無法回應，因為你完全無法跟容器內的應用程式互動！
    
    ```bash
    docker attach sql1
    ```
    
    > 此時你只能開啟另一個視窗，並且將容器停用或刪除，原本的終端機環境才能離開。
    
    如果我們在 SQL Server 容器啟動後，還想跟容器進行互動，那就一定要在執行容器時加入 `-it` 參數，先把虛擬終端機與 `STDIN` 通道開啟，讓之後透過 `docker attach` 掛載到目前終端機時可以進行互動！
    
    ```bash
    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 容器「查看」應用程式回應的訊息，因為你有個**虛擬終端機**可以跟應用程式進行互動，但你不能「輸入」任何字元到應用程式之中，這種情況你就只能「看」不能「碰」，只有「輸出」、沒有「輸入」。
    
    ```bash
    docker attach --no-stdin sql1
    ```
    
    > 你在執行上述命令後，按下 `Ctrl-C` 快速鍵，事實上並不會傳給 SQL Server 容器，因為你無法輸入任何訊息到容器中，此時 `STDIN` 管道是關閉的！所以你的 `Ctrl-C` 事實上是傳給了 `docker.exe` 程式，而這個命令會讓你跳出 `docker attach` 命令執行，回到原本的終端機環境中。
    
    **第二種：附加容器時，預設會開啟 `STDIN` 通道！**
    
    執行 `docker attach` 命令時，不但有**虛擬終端機**可用，還可以從輸入資料到容器中，且該資料也會傳入容器正在執行的應用程式中。
    
    ```bash
    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` 執行：
    
    ```bash
    docker run -i --rm ubuntu:latest bash
    ```
    
    這個命令所代表的意思是，將 `STDIN` 掛進去，但沒有啟動**虛擬終端機**，因此容器中的 Shell 環境無法處理「終端機」的相關操作，只能識別「資料」，因此當你在啟動容器後輸入 `ls` 並按下 `Enter` 鍵，你的 `bash` 收到的命令並不是 `ls` 命令，而是 `ls\r` 命令，所以就失敗了！
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/bfefabfc644822bd71c6-203140452-39e890cc-6dc9-4a15-b7a0-063b0b37a4c1.png)
    
    這時如果你按下 `Ctrl-C` 就會導致 `bash` 結束，因為沒有**虛擬終端機**的關係，你輸入的 `Ctrl-C` 直接傳給了 `bash` 執行，他收到之後就直接結束程序了，不像我們平常在 Shell 環境中按下 `Ctrl-C` 那樣單純的中斷命令。
    
2.  僅使用 `-t` 參數，忘記加上 `-i` 參數
    
    只建立了**虛擬終端機**，卻沒有傳入 `STDIN` 串流，此時畫面上雖然會出現 `bash` 的提示字元，但你完全無法輸入的。
    
    ```bash
    docker run -t --rm --name=ubuntu ubuntu:latest bash
    ```
    
    > 此時你若按下 `Ctrl-C` 容器也不會結束，因為你傳不進去，然而你也跳不出來，因為 `docker run` 不會理會你的 `Ctrl-C` 訊號。
    
3.  僅使用 `-t` 參數，忘記加上 `-i` 參數，同時又用了 `-d` 進入卸離模式
    
    只建立了**虛擬終端機**，就可以確保 `bash` 還活著，不過因為沒有 `-i` 的關係，下次 `docker attach` 一樣無法輸入任何命令：
    
    基本上沒有這種使用情境！
    
    ```bash
    docker run -t -d --rm --name=ubuntu ubuntu:latest bash
    ```
    
    > 此時你若按下 `Ctrl-C` 容器並不會結束，因為你沒有 `STDIN` 管道可以傳入命令，但你可以跳出容器，那是因為 `docker attach` 會處理 `Ctrl-C` 訊號。
    
4.  僅使用 `-i` 參數，忘記加上 `-t` 參數，同時又用了 `-d` 進入卸離模式
    
    如下命令執行了一個 `ubuntu:latest` 容器，並啟動 `bash` 執行，但立即卸離終端機環境：
    
    ```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` 訊號，那你必須這樣執行：
    
    ```bash
    docker attach --sig-proxy=false ubuntu
    ```
    
    > `docker attach` 的 `--sig-proxy` 參數預設為 `true`，因此按下 `Ctrl-C` 其實會送出 `SIGINT` 中斷訊號。
    
    從[官網文件](https://docs.docker.com/engine/reference/commandline/attach/)可以得知，當你使用 `--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` 執行。

### 相關連結

-   [docker attach | Docker Documentation](https://docs.docker.com/engine/reference/commandline/attach/)
-   [My process became PID 1 and now signals behave strangely | HackerNoon](https://hackernoon.com/my-process-became-pid-1-and-now-signals-behave-strangely-b05c52cc551c)
    -   Source code: https://github.com/AaronKalair/init-proc
    -   解決方案: `docker run --init alpine ps` ([Specify an init process](https://docs.docker.com/engine/reference/run/#specify-an-init-process))
    -   可以跟 `docker run alpine ps` 比較一下差異，就知道了！
-   [When would I use `--interactive` without `--tty` in a Docker container? - Stack Overflow](https://stackoverflow.com/a/35462735/910074)
-   [docker - Error "The input device is not a TTY" - Stack Overflow](https://stackoverflow.com/a/57565119/910074)
-   [Docker: Install containers for SQL Server on Linux - SQL Server | Microsoft Learn](https://learn.microsoft.com/en-us/sql/linux/quickstart-install-connect-docker?view=sql-server-ver16&pivots=cs1-powershell&WT.mc_id=DT-MVP-4015686)
