跳至主要内容

2.2 一探究竟容器內部

在這個小節,我們將進入容器內部一探究竟。

在以前,連線進入一個雲端或是隔離環境內,我們常常使用 ssh 的方式來建立安全的通道,但在 Docker 的世界裡不需要這麼做,就可以輕鬆連線到容器內部。

透過指令進入容器內部

透過下方的指令,終端機將會進入另一個終端機:

$ docker container run --interactive --tty nginx bash
root@d33940b87e66:/#

終端機理論上會呈現一個可以輸入的模式,所以我們可以輸入一些基本的 Linux 指令來驗證一下:

root@d33940b87e66:/# ls
bin boot dev docker-entrypoint.d docker-entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

離開容器內部

目前的我們還待在容器的內部,只要輸入 exit 就能夠輕鬆的離開容器內部,並且回到本機的終端機輸入行。

root@d33940b87e66:/# exit
exit
$

進入運作中的容器

上一個示範是在啟動容器時進入,但現實中大部分的應用程式都是一直在執行中的狀態,所以隨時都有進入容器的可能。

首先讓 nginx 容器在背景執行:

$ docker container run --detach --publish 80:80 nginx
64d52ea0e08797aade7f86701b60ed903373f53713d6f7eca62...

利用 docker container list 確定 nginx 容器在背景執行:

$ docker container list
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
64d52ea0... nginx "/doc.. 31 se.. Up 2.. 0.0.0.0:80->80/tcp hungry..

加入 exec 這個新的指令,對正在運作中的容器下指令,都需要加入 exec 的指令才能執行,我們進入後一樣用了 ls 這個 Linux 指令來查看容器中的檔案,並且輸入 exit 退出容器。

$ docker container exec --interactive --tty 64d52 bash
root@64d52ea0e087:/# ls
bin boot dev docker-entrypoint.d docker-entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@64d52ea0e087:/# exit
exit

新的參數代表什麼?

首先是 --interactive,我們透過閱讀 Docker 官方文件得到下面的答案:

--interactive 定義

Keep STDIN open even if not attached

直接翻譯就是保持輸入模式,但可以理解成和容器之間保持互動的狀況,要如何驗證呢?

我們故意不輸入 --interactive 的參數,看看會發生什麼事情。

$ docker container run --tty nginx bash

在你還沒輸入任何 Linux 指令前,看起來應該是很正常,那我們試著來輸入指令吧!

root@d33940b87e66:/# ls
# 接著他就完全停住了

要離開的話,直接關掉終端機就可以了,用上述的例子證明了如果不加入 --interactive 這個參數,即使我們能夠輸入指令,也沒辦法和容器互動。

接著是 --tty 這個參數,透過閱讀 Docker 官方文件得到的答案是:

--tty 定義

Allocate a pseudo-TTY

直接翻譯就是分配一個虛擬的 TTY,而這個 TTY 代表甚麼呢?

這就是一個有歷史淵源的故事了,在上古時代,當時的電腦非常的昂貴,所以一台電腦要分配給多個用戶進行操作,而多個用戶自然就需要多台打字機對著電腦進行輸入。

而 tty 正是英文 Teletypewriter 的縮寫,但其實在現代,終端機和打字機的界線已經模糊不清,可以想像,終端機就是 tty,反之亦然。

具有實驗精神的我們,也可以透過故意不使用 --tty 的參數來看看會發生什麼事情?

$ docker container run --interactive nginx bash
# 這邊看起來雖然像是不能動,但還是能夠輸入指令
ls
bin
boot
dev
docker-entrypoint.d
docker-entrypoint.sh

輸入 exit 也可以正常離開,但很明顯的感受到這根本不像是一個正常的終端機,回傳的資訊也都會自動的換行,根本沒辦法好好瀏覽,所以有沒有一個正常的終端機視窗也是很重要的。

exec 又代表什麼呢?

為什麼只要是對正在運作中的容器下指令,都需要加入 exec 才能執行呢?

exec 是一個允許在運行中的 Docker 容器內執行任何指令的指令,這邊聽起來很饒口,白話文就是它可以把指令傳遞到運行中的容器內並要求容器執行指令。

讓我們用下方的例子來快速地了解一下。

首先我們讓 nginx 容器在背景執行:

$ docker container run --detach --publish 80:80 nginx
a276f434ce5900b664ccfe99f74d957539681412f07f8802821e8de938a42e0b

這時候我們若是要列出容器內的所有檔案會怎麼做呢?根據剛剛學到的指令,您可能會用下面的方式,先進入到容器內,並且輸入 ls 指令來列出所有的檔案。

$ docker container exec --interactive --tty a276 bash
root@a276f434ce59:/# ls
bin boot dev docker-entrypoint.d docker-entrypoint.sh ...
root@a276f434ce59:/# exit
exit

但根據 exec 這個指令的定義,可以把指令傳遞到運行中的容器內並要求容器執行指令,所以我們其實可以這樣做:

$ docker container exec a276 ls
bin
boot
dev
docker-entrypoint.d
docker-entrypoint.sh
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

如同定義一樣,我們要求 nginx 指令執行 ls 這段指令,並且回傳結果。

後面的 bash 是什麼?

在我們練習進入容器內部時,應該會有一個疑惑,就是為什麼要在 nginx 映像檔的後方加上一個 bash 指令呢?

首先我們利用 Docker 提供的 --help 方法來看看,後面到底能夠接受什麼樣的參數呢?

$ docker container run --help
Usage: docker container run [OPTIONS] IMAGE [COMMAND] [ARG...]
Run a command in a new container
....

可以看到 IMAGE 的後方還可以接受 COMMAND 的參數,而這個 COMMAND 就是容器啟動時會執行的指令。

那為什麼之前又不需要呢?是因為映像檔在建置的時候都會給予一個 CMD 參數來當作容器啟動時的指令,若沒有輸入額外的 COMMAND 去取代,就會使用映像檔預設的啟動指令。

而 nginx 這個映像檔預設的啟動指令就是 nginx -g daemon off,也就是啟動 nginx 的意思。

在上面的範例中,我們就是利用了 bash 這個指令來取代掉 nginx -g daemon off 啟動指令,變成容器在啟動時執行 bash 這個命令處理器。

只能用 bash 嗎?

明明市面上還有很多的命令處理器呀,為什麼我一定要用 bash 呢?那是因為大部分使用的映像檔都有支援 bash 這個命令處理器,但凡事皆有例外,讓我們試試執行一個超級輕量的 Linux 作業系統 alpine,來看看會發生甚麼事吧!

$ docker container run --interactive --tty alpine bash
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown.

Docker 告訴我們,bash 這個執行程序並不在 $PATH ( 環境變數 ) 內。

這同時也告訴了我們另一個概念,我們只能啟動映像檔建置時就已經包含的程式,在這個極輕量的作業系統中很顯然地沒有安裝 bash,故我們沒有辦法啟動。

那我們該怎麼進入 alpine 的容器呢?答案是使用 sh 這個命令處理器,基本上大部分的 Linux 系統都有預設支援 sh,所以 sh 可以說是最保險的執行程序,讓我們來執行看看吧!

$ docker container run --interactive --tty alpine sh
/ #

所以說每個作業系統中還是有些微的差距,但如果都習慣以 sh 來啟動,基本上不會遇到什麼問題喔!

為什麼容器退出了?

在我們從 nginx 容器內部離開後,眼尖的您可能會發現容器進入了退出狀態。

可以透過列出所有容器的指令來確認 nginx 進入了退出狀態。

$ docker container list --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d33940b87e66 nginx "/doc.. 7 sec.. Exited eleme...

還記得我們在第一次啟動 nginx 容器時,使用 Ctrl + C 的方式就使容器進入退出狀態吧?

但為什麼這邊只是退出了 bash 執行程序就使容器進入退出狀態了呢?

先說結論:只有終止了啟動指令所執行的程序,才會使容器進入退出狀態

這句話可能有點難以理解,用下面的例子來驗證這段結論。

這邊我們一樣讓 nginx 容器在背景執行,接著我們進入內部,並且直接退出:

$ docker container run --detach --publish 80:80 nginx
0375cb9903ab964cdae10b31d32be3009f2f228ea3fab4fc992f09011bd159c0

$ docker container exec --interactive --tty 037 bash
root@0375cb9903ab:/# exit
exit

查看一下容器列表會發現其還在運作,所以並非退出 bash 執行程序就停止了容器。

$ docker container list
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0375cb990... nginx "/doc.. 31 se.. Up 1.. 0.0.0.0:80->80/tcp tender..

這就回歸到上一個段落所提到的:只有終止了啟動指令所執行的程序,才會使容器進入退出狀態

但這邊我們退出的程序並不是這個容器的啟動指令,這個容器的啟動指令是 nginx 一開始預設的 nginx -g daemon off,所以我們退出的 bash 程序並不會造成容器進入退出狀態。

我們可以再試試看另外一個例子,這邊我們啟動一個 Ubuntu 的容器:

$ docker container run --interactive --tty --name ubuntu ubuntu
root@c4131a4bd1fa:/#

您可能會想問,為什麼不用加 bash 在最後面取代啟動指令呢?那是因為 ubuntu 這個映像檔本身的啟動指令就是 bash 喲!

所以我們不需要用 bash 來取代預設的啟動指令,ubuntu 就會自動啟動 bash 這個程序,舉一反三可得知,我們輸入 exit 後,ubuntu 容器就會進入退出狀態,因為我們終止了它的啟動指令。

root@c4131a4bd1fa:/# exit
exit

$ docker container list --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c4131a4bd1.. ubu.. "bash" 2 min.. Exited ubuntu

可以進入容器內安裝套件嗎?

答案是可以的,所有的容器在被刪除之前,都會保留原先的檔案系統,退出容器並不會讓資料流失

這邊我們重新啟動剛剛退出的 ubuntu 容器,並且更新一下作業系統的套件:

$ docker container start --interactive ubuntu
root@c4131a4bd1fa:/# apt-get update
Get:1 http://security.ubuntu.com/ubuntu jammy-securi...
Get:2 http://archive.ubuntu.com/ubuntu jammy InRe....
Get:3 http://security.ubuntu.com/ubuntu jammy-secu....
....
Reading package lists... Done

接著我們下載 curl 這個工具,來測試一下容器是否真的會保存我們的所做的變動:

root@c4131a4bd1fa:/# apt install -y curl
Reading package lists... Done
.....()
Running hooks in /etc/ca-certificates/update.d...
done.
root@c4131a4bd1fa:/# exit
exit

我們在安裝完 curl 這個工具後離開了容器,想當然容器也進入了退出狀態。

接著我們再次啟動 ubuntu 容器,若是 curl 這個工具能夠使用,就代表了容器是能夠暫時保存檔案的吧?

$ docker container start --interactive ubuntu
root@c4131a4bd1fa:/# curl google.com <- 使用 curl
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
root@c4131a4bd1fa:/# exit
exit

我們成功地利用 curl 去敲了一下 google.com 這個網站,並拿回了 HTML 的檔案,但這還不足以完全證明容器的暫存空間。

我們試試看刪除這個容器,是不是真的會造成檔案的流失。

$ docker container rm --force ubuntu
ubuntu

$ docker container run --interactive --tty --name ubuntu ubuntu # 不換行

root@74370599d4d0:/# curl google.com
bash: curl: command not found
root@74370599d4d0:/#

重新啟動的 ubuntu 容器確實是沒有 curl 這個工具。

實際的原因是因為容器在啟動時會提供一個可寫層,記錄下我們對於這個容器檔案系統的操作,而只要刪除容器,可寫層就會隨著容器而一併消失。

關於如何保存資料,都會在 Docker Volume 篇 做更深入的探討,目前只要記得原先不屬於容器內的資料都會在刪除容器時一併消失就可以了。

不同作業系統的容器練習

每一個章節都會提供一些小小的練習來熟悉本章的內容,通常會比較難一點;若是想不出來也不需要灰心,有些時候可能是指令不熟悉導致,可以往前翻閱來加強記憶喔,亦或是翻到後面的解答章節也可以幫助你解惑喔,記得 --help 以及 docs.docker.com 會是你最好的夥伴。

這個章節的練習重點是使用 --interactive & --tty 的方式進入不同的 Linux 作業系統容器並且確認 curl 這個套件的版本,可以練習到進入容器以及根據不同作業系統安裝套件,並且確認是否有安裝成功。

  1. 利用終端機指令搭配 --interactive --tty 的方式分別進入 centos:centos7 以及 ubuntu:20.04 兩個映像檔建立的容器中。
  2. 上網查查看容器的 --rm 的指令代表什麼,並且使用在這次的練習中。
  3. 在 ubuntu 的容器中使用 apt-get update && apt-get install curl 的指令安裝套件
  4. 在 centos 的容器中使用 yum update curl 的指令安裝套件
  5. 分別在兩個容器中使用 curl --version 檢查版本,也是確認安裝成功的方式

不同作業系統的容器練習解答

  1. 利用終端機指令搭配 --interactive --tty 的方式分別進入 centos:centos7 以及 ubuntu:20.04 兩個映像檔建立的容器中;在 ubuntu 的容器中使用 apt-get update && apt-get install curl 的指令安裝套件;在 centos 的容器中使用 yum update curl 的指令安裝套件;使用 curl --version 檢查版本。

進入 CentOS

$ docker container run --interactive --tty centos:centos7
[root@a04edfc93dd3 /]# yum update curl
.......安裝程序
Complete!
[root@a04edfc93dd3 /]# curl --version
curl 7.29.0 (x86_64-redhat-linux-gnu) libcurl/7.2....

進入 Ubuntu

$ docker container run --interactive --tty ubuntu:20.04
root@24093fdeb122:/# apt-get update && apt-get install curl
.......安裝程序
Running hooks in /etc/ca-certificates/update.d...
done.
root@24093fdeb122:/# curl --version
curl 7.68.0 (x86_64-pc-linux-gnu) libcurl/7.68.0 Op...
  1. 上網查查看容器的 --rm 的指令代表什麼,並且使用在這次的練習中。

--rm 的指令意味著這個容器進入退出狀態時就會自動刪除,省去了 docker container rm 的動作,至於要如何應用在這次的練習中呢?就是在啟動容器的時候加入就可以了,如下方示範:

$ docker container run --rm --interactive --tty ubuntu:20.04 # 不換行