Docker 容器初體驗

發表於2014-12-24

在本文中,我們將邁出使用Docker的第一步,學習第一個Docker容器。本章還會介紹如何與Docker進行互動的基本知識。

1 確保Docker已經就緒

首先,我們會檢視Docker是否能正常工作,然後學習基本的Docker的工作流:建立並管理容器。我們將瀏覽容器的典型生命週期:從建立、管理到停止,直到最終刪除。

第一步,檢視docker程式是否存在,功能是否正常,如程式碼清單3-1所示。

程式碼清單3-1 檢視docker程式是否正常工作

$ sudo docker info  
Containers: 0  
Images: 0  
Storage Driver: aufs  
 Root Dir: /var/lib/docker/aufs  
 Dirs: 144  
Execution Driver: native-0.1  
Kernel Version: 3.8.0-29-generic  
Registry: [https://index.docker.io/v1/]

在這裡我們呼叫了docker可執行程式的info命令,該命令會返回所有容器和映象(映象即是Docker用來構建容器的“構建塊”)的數量、Docker使用的執行驅動和儲存驅動(execution and storage driver),以及Docker的基本配置。

在前面幾章我們已經介紹過,Docker是基於客戶端-伺服器構架的。它有一個docker程式,既能作為客戶端,也可以作為伺服器端。作為客戶端時,docker程式向Docker守護程式傳送請求(如請求返回守護程式自身的資訊),然後再對返回來的請求結果進行處理。

2 執行我們的第一個容器

現在,讓我們嘗試啟動第一個Docker容器。我們可以使用docker run命令建立容器,如程式碼清單3-2所示。docker run命令提供了Docker容器的建立到啟動的功能,在本書中我們也會使用該命令來建立新容器。

程式碼清單3-2 建立第一個容器

$ sudo docker run -i -t ubuntu /bin/bash  
Pulling repository ubuntu from https://index.docker.io/v1  
Pulling image 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c (precise) from ubuntu  
Pulling 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c metadata  
Pulling 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c fs layer  
Downloading 58337280/? (n/a)  
Pulling image b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc (quantal) from ubuntu  
Pulling image 27cf784147099545 () from ubuntu  
root@fcd78e1a3569:/#

{提示}
官方文件列出了完整的Docker命令列表,你也可以使用docker help獲取這些命令。此外,你還可以使用Docker的man頁(即執行man docker-run)。

程式碼清單3-3所示命令的輸出結果非常豐富,下面我們來逐條解析。

程式碼清單3-3 docker run命令

$ sudo docker run -i -t ubuntu /bin/bash

首先,我們告訴Docker執行docker run命令,並指定了-i和-t兩個命令列引數。-i標誌保證容器中STDIN是開啟的,儘管我們並沒有附著到容器中。持久的標準輸入是互動式shell的“半邊天”,-t標誌則是另外“半邊天”,它告訴Docker為要建立的容器分配一個偽tty終端。這樣,新建立的容器才能提供一個互動式shell。若要在命令列下建立一個我們能與之進行互動的容器,而不是一個執行後臺服務的容器,則這兩個引數已經是最基本的引數了。

{提示}
官方文件上列出了docker run命令的所有標誌,此外還可以用命令docker help run檢視這些標誌。或者,也可以用Docker的man頁(也就是執行man docker-run命令)。

接下來,我們告訴Docker基於什麼映象來建立容器,示例中使用的是ubuntu映象。ubuntu映象是一個常備映象,也可以稱為“基礎”(base)映象,它由Docker公司提供,儲存在Docker HubRegistry上。

你可以用 ubuntu 基礎映象(以及類似的 fedora、debian、centos等映象)為基礎,在你選擇的作業系統上構建自己的映象。這裡,我們基於此基礎映象啟動了一個容器,並且沒有對容器進行任何改動。

那麼,在這一切的背後又都發生了什麼呢?首先Docker會檢查本地是否存在ubuntu映象,如果本地還沒有該映象的話,那麼Docker就會連線官方維護的Docker Hub Registry,檢視Docker Hub中是否有該映象。Docker一旦找到該映象,就會下載該映象並將其儲存到本地宿主機中。

隨後,Docker在檔案系統內部用這個映象建立了一個新容器。該容器擁有自己的網路、IP地址,以及一個用來和宿主機進行通訊的橋接網路介面。最後,我們告訴Docker在新容器中要執行什麼命令,在本例中我們在容器中執行/bin/bash命令啟動了一個Bash shell。

當容器建立完畢之後,Docker就會執行容器中的/bin/bash命令,這時我們就可以看到容器內的shell了,就像程式碼清單3-4所示。

程式碼清單3-4 第一個容器的shell

root@f7cbdac22a02:/#

{注意}
在第4章中,我們將會看到如何構建自己的映象並基於該映象建立容器的基礎知識。

3 使用第一個容器

現在,我們已經以root使用者登入到了新容器中,容器的IDf7cbdac22a02``,乍``看起來有些令人迷惑的字串。這是一個完整的Ubuntu系統,你可以用它來做任何事情。下面我們就來研究一下這個容器。首先,我們可以獲取該容器的主機名,如程式碼清單3-5所示。

程式碼清單3-5 檢查容器的主機名

root@f7cbdac22a02:/# hostname  
f7cbdac22a02

可以看到,容器的主機名就是該容器的ID。我們再來看看/etc/hosts檔案,如程式碼清單3-6所示。

程式碼清單3-6 檢查容器的/etc/hosts檔案

root@f7cbdac22a02:/# cat /etc/hosts  
172.17.0.4  f7cbdac22a02  
127.0.0.1 localhost  
::1 localhost ip6-localhost ip6-loopback  
fe00::0 ip6-localnet  
ff00::0 ip6-mcastprefix  
ff02::1 ip6-allnodes  
ff02::2 ip6-allrouters

Docker以在hosts檔案中為該容器的IP地址新增了一條主機配置項。我們再來看看容器的網路配置情況,如程式碼清單3-7所示。

程式碼清單3-7 檢查容器的介面

root@f7cbdac22a02:/# ip a  
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default  
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00  
    inet 127.0.0.1/8 scope host lo  
    inet6 ::1/128 scope host  
       valid_lft forever preferred_lft forever  
899: eth0: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000  
    link/ether 16:50:3a:b6:f2:cc brd ff:ff:ff:ff:ff:ff  
    inet 172.17.0.4/16 scope global eth0  
    inet6 fe80::1450:3aff:feb6:f2cc/64 scope link  
       valid_lft forever preferred_lft forever

我們可以看到,這裡有lo的環回介面,還有IP為172.17.0.4的標準eth0網路介面,和普通宿主機是完全一樣的。我們還可以檢視容器中執行的程式,如程式碼清單3-8所示。

程式碼清單3-8 檢查容器的程式

root@f7cbdac22a02:/# ps -aux  
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND  
root         1  0.0  0.0  18156  1936 ?        Ss   May30   0:00 /bin/bash  
root        21  0.0  0.0  15568  1100 ?        R+   02:38   0:00 ps -aux

接下來我們要幹些什麼呢?安裝一個軟體包怎麼樣?如程式碼清單3-9所示。

程式碼清單3-9 在第一個容器中安裝軟體包

root@f7cbdac22a02:/# apt-get update && apt-get install vim

透過上述命令,我們就在容器中安裝了Vim軟體。

你可以繼續在容器中做任何自己想做的事情。當所有工作都結束時,輸入exit,就可以返回到Ubuntu宿主機的命令列提示符了。

這個容器現在怎樣了?容器現在已經停止執行了!只有在指定的/bin/bash命令處於執行狀態的時候,我們容器也才會相應地處於執行狀態。一旦退出容器,/bin/bash命令也就結束了,這時容器也隨之停止了執行。

但容器仍然是存在的,我們可以用docker ps -a命令檢視當前系統中容器的列表

預設情況下,當執行docker ps命令時,只能看到正在執行的容器。如果指定-a標誌,選項的話,那麼docker ps命令會列出所有容器,包括正在執行的和已經停止的。

{提示}
你也可以為docker ps命令指定-l標誌,該選項會列出最後一次執行的容器,包括正在執行和已經停止的。

從該命令的輸出結果中我們可以看到關於這個容器的很多有用資訊:ID、用於建立該容器的映象、容器最後執行的命令、建立時間以及容器的退出狀態(在上面的例子中,退出狀態是0,因為容器是透過正常的exit命令退出的)。我們還可以看到,每個容器都有一個名稱。

{注意}
有三種方式可以指代唯一容器:短UUID(如f7cbdac22a02)、長UUID(如f7cbdac22a02e03c9438c729345e54db9d20cfa2ac1fc3494b6eb60872e74778)或者名稱(如gray_cat)。

4 容器命名

Docker會為我們建立的每一個容器自動生成一個隨機的名稱。例如,上面我們剛剛建立的容器就被命名為gray_cat。如果想為容器指定一個名稱,而不是使用自動生成的名稱,則可以用--name標誌來實現,如程式碼清單3-10所示。

程式碼清單3-10 給容器命名

$ sudo docker run --name bob_the_container -i -t ubuntu /bin/bash  
root@aa3f365f0f4e:/# exit

上述命令將會建立一個名為bob_the_container的容器。一個合法的容器名稱只能包含以下字元:小寫字母a~z、大寫字母A~Z、數字0~9、下劃線、圓點、橫線(如果用正規表示式來表示這些符號,就是[a-zA-Z0-9_.-])。

在很多Docker命令中,我們都可以用容器的名稱來替代容器ID,後面我們將會看到。容器名稱有助於分辨容器,當構建容器和應用程式之間的邏輯連線時,容器的名稱也有助於從邏輯上理解連線關係。具體的名稱(如web、db)比容器ID和隨機容器名好記多了。我推薦大家都使用容器名稱,以更加方便地管理容器。

容器的命名必須是唯一的。如果我們試圖建立兩個名稱相同的容器,則命令將會失敗。如果要使用的容器名稱已經存在,可以先用docker rm命令刪除已有的同名容器後,再來建立新的容器。

5 重新啟動已經停止的容器

bob_the_container容器已經停止了,接下來我們能對它做些什麼呢?如果願意,我們可以用下面的命令重新啟動一個已經停止的容器,如程式碼清單3-11所示。

程式碼清單3-11 啟動已經停止執行的容器

$ sudo docker start bob_the_container

除了容器名稱,我們也可以用容器ID來指定容器,如程式碼清單3-12所示。

程式碼清單3-12 透過ID啟動已經停止執行的容器

$ sudo docker start aa3f365f0f4e

{提示}
我們也可以使用docker restart命令來重新啟動一個容器。

這時執行不帶-a標誌的docker ps命令,就應該看到我們的容器已經開始執行了。

6 附著到容器上

Docker容器重新啟動的時候,會沿用docker run命令時指定的引數來執行,因此我們容器重新啟動後會執行一個互動式會話shell。此外,我們也可以用docker attach命令,重新附著到該容器的會話上,如程式碼清單3-13所示。

程式碼清單3-13 附著到正在執行的容器

$ sudo docker attach bob_the_container

我們也可以使用容器ID,重新附著到容器的會話上,如程式碼清單3-14所示。

程式碼清單3-14 透過ID附著到正在執行的容器

$ sudo docker attach aa3f365f0f4e

現在,我們又重新回到了容器的Bash提示符,如程式碼清單3-15所示。

程式碼清單3-15 重新附著到容器的會話

root@aa3f365f0f4e:/#

{提示}
你可能需要按下Enter鍵才能進入該會話。

如果退出容器的shell,容器也會隨之停止執行。

7 建立守護式容器

除了這些互動式執行的容器(interactive container),我們也可以建立長期執行的容器。守護式容器(daemonized container)沒有互動式會話,非常適合執行應用程式和服務。大多數時候我們都需要以守護式來執行我們的容器。下面我們就來啟動一個守護式容器,如程式碼清單3-16所示。

程式碼清單3-16 建立長期執行的容器

$ sudo docker run --name daemon_dave -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done"  
1333bb1a66af402138485fe44a335b382c09a887aa9f95cb9725e309ce5b7db3

我們在上面的docker run命令使用了-d引數,因此Docker會將容器放到後臺執行。

我們還在容器要執行的命令裡使用了一個while迴圈,該迴圈會一直列印hello world,直到容器或其程式停止執行。

透過組合使用上面的這些引數,你可以發現docker run命令並沒有像上一個容器一樣將主機的控制檯附著到新的shell會話上,而是僅僅返回了一個容器ID而已,我們還是在主機的命令列之中。如果我們執行docker ps命令,可以看到一個正在執行的容器,如程式碼清單3-17所示。

程式碼清單3-17 檢視正在執行的daemon_dave容器

CONTAINER ID IMAGE        COMMAND              CREATED      STATUS  PORTS NAMES  
1333bb1a66af ubuntu:14.04 /bin/sh -c 'while tr 32 secs ago  Up 27         daemon_dave

8 容器內部都在幹些什麼

現在我們已經有了一個在後臺執行while迴圈的守護型容器。為了探究該容器內部都在幹些什麼,我們可以用docker logs命令來獲取容器的日誌,如程式碼清單3-18所示。

程式碼清單3-18 獲取守護式容器的日誌

$ sudo docker logs daemon_dave  
hello world  
hello world  
hello world  
hello world  
hello world  
hello world  
hello world  
. . .

這裡,我們可以看到while迴圈正在向日志裡列印hello world。Docker會輸出最後幾條日誌項並返回。我們也可以在命令後使用-f引數來監控Docker的日誌,這與tail -f命令非常相似,如程式碼清單3-19所示。

程式碼清單3-19 跟蹤守護式容器的日誌

$ sudo docker logs -f daemon_dave  
hello world  
hello world  
hello world  
hello world  
hello world  
hello world  
hello world  
. . .

{提示}
可以透過Ctrl+C退出日誌跟蹤。

我們也可以跟蹤容器日誌的某一片段,和之前類似,只需要在tail命令後加入-f --lines標誌即可。例如,可以用docker logs --tail 10 daemon_dave獲取日誌的最後10行內容。另外,也可以用docker logs --tail 0 -f daemon_dave命令來跟蹤某個容器的最新日誌而不必讀取整個日誌檔案。

為了讓除錯更簡單,我們還可以使用-t標誌為每條日誌項加上時間戳,如程式碼清單3-20所示。

程式碼清單3-20 跟蹤守護式容器的最新日誌

$ sudo docker logs -ft daemon_dave  
[May 10 13:06:17.934] hello world  
[May 10 13:06:18.935] hello world  
[May 10 13:06:19.937] hello world  
[May 10 13:06:20.939] hello world  
[May 10 13:06:21.942] hello world  
. . .

{提示}
同樣,可以透過Ctr+C退出日誌跟蹤。

9 檢視容器內的程式

除了容器的日誌,我們也可以檢視容器內部執行的程式。要做到這一點,要使用docker top命令,如程式碼清單3-21所示。

程式碼清單3-21 檢視守護式容器的程式

$ sudo docker top daemon_dave

該命令執行後,我們可以看到容器內的所有程式(主要還是我們的while迴圈)、執行程式的使用者及程式ID,如程式碼清單3-22所示。

程式碼清單3-22 docker``top命令的輸出結果

PID  USER COMMAND  
977  root /bin/sh -c while true; do echo hello world; sleep 1; done  
1123 root sleep 1

10 在容器內部執行程式

在Docker 1.3之後,我們也可以透過docker exec命令在容器內部額外啟動新程式。可以在容器內執行的程式有兩種型別:後臺任務和互動式任務。後臺任務在容器內執行且沒有互動需求,而互動式任務則保持在前臺執行。對於需要在容器內部開啟shell的任務,互動式任務是很實用的。下面我們先來看一個後臺任務的例子,如程式碼清單3-23所示。

程式碼清單3-23 在容器中執行後臺任務

$ sudo docker exec -d daemon_dave touch /etc/new_config_file

這裡的-d標誌表明需要執行一個後臺程式,-d標誌之後,指定的是要在內部執行這個命令的容器的名字以及要執行的命令。上面例子中的命令會在daemon_dave容器內建立了一個空檔案,檔名為/etc/new_config_file。透過docker exec後臺命令,我們可以在正在執行的容器中進行維護、監控及管理任務。

我們也可以在daemon_dave容器中啟動一個諸如開啟shell的互動式任務,如程式碼清單3-24所示。

程式碼清單3-24 在容器內執行互動命令

$ sudo docker exec -t -i daemon_dave /bin/bashVersion:

和執行互動容器時一樣,這裡的-t和-i標誌為我們執行的程式建立了TTY並捕捉STDIN。接著我們指定了要在內部執行這個命令的容器的名字以及要執行的命令。在上面的例子中,這條命令會在daemon_dave容器內建立一個新的bash會話,有了這個會話,我們就可以在該容器中執行其他命令了。

{注意}
docker exec命令是Docker 1.3引入的,早期版本並不支援該命令。對於早期Docker版本,請參考第6章中介紹的nsenter命令。

11 停止守護式容器

要停止守護式容器,只需要執行docker stop命令,如程式碼清單3-25所示。

程式碼清單3-25 停止正在執行的Docker容器

$ sudo docker stop daemon_dave

當然,也可以用容器ID來指代容器名稱,如程式碼清單3-26所示。

程式碼清單3-26 透過容器ID停止正在執行的容器

$ sudo docker stop c2c4e57c12c4

{注意}
docker stop命令會向Docker容器程式傳送SIGTERM訊號。如果你想快速停止某個容器,也可以使用docker kill命令來向容器程式傳送SIGKILL訊號。

要想檢視已經停止的容器的狀態,則可以使用docker ps命令。還有一個很實用的命令docker ps -n x,該命令會顯示最後x個容器,不論這些容器正在執行還是已經停止。

12 自動重啟容器

如果由於某種錯誤而導致容器停止執行,我們還可以透過--restart標誌,讓Docker自動重新啟動該容器。--restart標誌會檢查容器的退出程式碼,並據此來決定是否要重啟容器。預設的行為是Docker不會重啟容器。

程式碼清單3-27是一個在docker run命令中使用—restart標誌的例子。

程式碼清單3-27 自動重啟容器

$ sudo docker run --restart=always --name daemon_dave -d ubuntu /  
bin/sh -c "while true; do echo hello world; sleep 1; done"

在本例中,--restart標誌被設定為always。無論容器的退出程式碼是什麼,Docker都會自動重啟該容器。除了always,我們還可以將這個標誌設為on-failure,這樣,只有當容器的退出程式碼為非0值的時候,才會自動重啟。另外,on-failure``還接受``一個可選的重啟次數引數,如程式碼清單3-28所示。

程式碼清單3-28 為on-failure指定count引數

--restart=on-failure:5

這樣,當容器退出程式碼為非0時,Docker會嘗試自動重啟該容器,最多重啟5次。

{注意}
--restart標誌是Docker1.2.0引入的選項。

13 深入容器

除了透過docker ps命令獲取容器的資訊,我們還可以使用docker inspect``來獲得更多的容器資訊,如程式碼清單3-29所示。

程式碼清單3-29 檢視容器

$ sudo docker inspect daemon_dave  
[{  
    "ID": "c2c4e57c12c4c142271c031333823af95d64b20b5d607970c334784430bcbd0f",  
    "Created": "2014-05-10T11:49:01.902029966Z",  
    "Path": "/bin/sh",  
    "Args": [  
        "-c",  
        "while true; do echo hello world; sleep 1; done"  
    ],  
    "Config": {  
        "Hostname": "c2c4e57c12c4",  
. . .

docker inspect命令會對容器進行詳細的檢查,然後返回其配置資訊,包括名稱、命令、網路配置以及很多有用的資料。

我們也可以用-f或者--format標誌來選定檢視結果,如程式碼清單3-30所示。

程式碼清單3-30 有選擇地獲取容器資訊

$ sudo docker inspect --format='{{ .State.Running }}' daemon_dave  
false

上面這條命令會返回容器的執行狀態,示例中該狀態為false。我們還能獲取其他有用的資訊,如容器IP地址,如程式碼清單3-31所示。

程式碼清單3-31 檢視容器的IP地址

$ sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' daemon_dave  
172.17.0.2

{提示}
--format或者-f標誌遠非表面看上去那麼簡單。該標誌實際上支援完整的Go語言模板。用它進行查詢時,可以充分利用Go語言模板的優勢。

我們也可以同時指定多個容器,並顯示每個容器的輸出結果,如程式碼清單3-32所示。

程式碼清單3-32 檢視多個容器

$ sudo docker inspect --format '{{.Name}} {{.State.Running}}' daemon_dave bob_the_container  
/daemon_dave false  
/bob_the_container false

我們可以為該引數指定要查詢和返回的檢視雜湊(inspect hash)中的任意部分。

{注意}
除了檢視容器,你還可以透過瀏覽/var/lib/docker目錄來深入瞭解Docker的工作原理。該目錄存放著Docker映象、容器以及容器的配置。所有的容器都儲存在/var/lib/docker/containers目錄下。

14 刪除容器

如果容器已經不再使用,可以使用docker rm命令來刪除它們,如程式碼清單3-33所示。

程式碼清單3-33 刪除容器

$ sudo docker rm 80430f8d0921  
80430f8d0921

{注意}
需要注意的是,執行中的Docker容器是無法刪除的!你必須先透過docker stop或docker kill命令停止容器,才能將其刪除。

目前,還沒有辦法一次刪除所有容器,不過可以透過程式碼清單3-34所示的小技巧來刪除全部容器。

程式碼清單3-34 刪除所有容器

docker rm `docker ps -a -q`

上面的docker ps命令會列出現有的全部容器,-a標誌代表列出所有(all)容器,而-q標誌則表示只需要返回容器的ID而不會返回容器的其他資訊。這樣我們就得到了容器ID的列表,並傳給了docker rm命令,從而達到刪除所有容器的目的。

小結

在本章中我們介紹了Docker容器的基本工作原理。這裡學到的內容也是本書剩餘章節中學習如何使用Docker的基礎。

本文摘自《第一本Docker書

相關文章