1. 程式人生 > >Docker快速入門(二)

Docker快速入門(二)

AS 重復 為什麽 函數調用 apt-get curl 命令 IE pwd SQ

上篇文章《Docker快速入門(一)》介紹了docker的基本概念和image的相關操作,本篇將進一步介紹image,容器和Dockerfile。

1 image文件

(1)Docker 把應用程序及其依賴,打包在 image 文件裏面。
(2)只有通過這個image文件,才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根據 image 文件生成容器的實例。
(3)同一個 image 文件,可以生成多個同時運行的容器實例。
(4)image 是二進制文件。實際開發中,一個 image 文件往往通過繼承另一個 image 文件,加上一些個性化設置而生成。
(5)image 文件是通用的,一臺機器的 image 文件拷貝到另一臺機器,照樣可以使用。

(6)一般來說,為了節省時間,我們應該盡量使用別人制作好的 image 文件,而不是自己制作。即使要定制,也應該基於別人的 image 文件進行加工,而不是從零開始制作。
(7)為了方便共享,image 文件制作完成後,可以上傳到網上的倉庫。Docker 的官方倉庫 Docker Hub 是最重要、最常用的 image 倉庫。此外,出售自己制作的 image 文件也是可以的。

下面我們舉例介紹幾個命令:

docker container run hello-world

該命令會根據 image 文件,生成一個正在運行的容器實例。
註意:docker container run命令具有自動抓取 image 文件的功能。如果發現本地沒有指定的 image 文件,就會從倉庫自動抓取。因此,前面的docker image pull命令並不是必需的步驟。

如果運行成功,如下:

[@sjs_123_183 ~]# docker container run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://cloud.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

[@sjs_123_183 ~]#

輸出以上內容後,hello world就會停止運行,容器自動終止。有些容器提供服務,並不會自動終止,如Ubuntu的image:

[@sjs_123_183 ~]# docker container run -it ubuntu bash
root@cd902f829884:/#
root@cd902f829884:/# pwd
/
root@cd902f829884:/#  

這時候,我們打開另一個終端,通過:

docker container ls  

可以看到本機正在運行的容器實例:

[@sjs_123_183 ~]# docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
cd902f829884        ubuntu              "bash"              50 seconds ago      Up 50 seconds                           nifty_pike

此時通過

docker container kill [CONTAINER ID]  

該命令可以終止正在運行的容器。如下:

[@sjs_123_183 ~]# docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
cd902f829884        ubuntu              "bash"              4 minutes ago       Up 4 minutes                            nifty_pike
[@sjs_123_183 ~]# docker container kill cd9
cd9

2 容器文件

image 文件生成的容器實例,本身也是一個文件,稱為容器文件。也就是說,一旦容器生成,就會同時存在兩個文件: image 文件和容器文件。而且關閉容器並不會刪除容器文件,只是容器停止運行而已。

docker container kill [CONTAINER ID]    # 列出正在運行的容器
docker container ls --all              # 列出本機所有容器,包括終止運行的容器  

如下:

[@sjs_123_183 ~]# docker container ls --all
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
cd902f829884        ubuntu              "bash"                   6 minutes ago       Exited (137) About a minute ago                       nifty_pike
3ca03ca1cd7c        hello-world         "/hello"                 11 minutes ago      Exited (0) 11 minutes ago                             laughing_booth
d225defb10db        ubuntu              "bash"                   13 minutes ago      Exited (137) 13 minutes ago                           fervent_galileo
77182065e27d        ubuntu              "bash"                   16 minutes ago      Exited (127) 15 minutes ago                           ecstatic_kilby
ebf4e2421f51        hello-world         "/hello"                 20 minutes ago      Exited (0) 20 minutes ago                             heuristic_albattani
081ccb2d6eed        nginx               "nginx -g ‘daemon of…"   4 weeks ago         Exited (0) 4 weeks ago                                adoring_poincare
fea01895c580        hello-world         "/hello"                 4 weeks ago         Exited (0) 4 weeks ago                                vibrant_goldwasser

命令的輸出結果之中,包括容器的 ID, 即CONTAINER ID。很多地方都需要提供這個 ID,比如上一節終止容器運行的docker container kill命令。
終止運行的容器文件,依然會占據硬盤空間,可以使用docker container rm [CONTAINER ID]命令刪除。參見:

[@sjs_123_183 ~]# docker container ls --all
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
cd902f829884        ubuntu              "bash"                   6 minutes ago       Exited (137) About a minute ago                       nifty_pike
3ca03ca1cd7c        hello-world         "/hello"                 11 minutes ago      Exited (0) 11 minutes ago                             laughing_booth
d225defb10db        ubuntu              "bash"                   13 minutes ago      Exited (137) 13 minutes ago                           fervent_galileo
77182065e27d        ubuntu              "bash"                   16 minutes ago      Exited (127) 15 minutes ago                           ecstatic_kilby
ebf4e2421f51        hello-world         "/hello"                 20 minutes ago      Exited (0) 20 minutes ago                             heuristic_albattani
081ccb2d6eed        nginx               "nginx -g ‘daemon of…"   4 weeks ago         Exited (0) 4 weeks ago                                adoring_poincare
fea01895c580        hello-world         "/hello"                 4 weeks ago         Exited (0) 4 weeks ago                                vibrant_goldwasser
[@sjs_123_183 ~]# docker container rm 3ca ebf
3ca
ebf
[@sjs_123_183 ~]# docker container ls --all
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                        PORTS               NAMES
cd902f829884        ubuntu              "bash"                   10 minutes ago      Exited (137) 5 minutes ago                        nifty_pike
d225defb10db        ubuntu              "bash"                   17 minutes ago      Exited (137) 17 minutes ago                       fervent_galileo
77182065e27d        ubuntu              "bash"                   20 minutes ago      Exited (127) 19 minutes ago                       ecstatic_kilby
081ccb2d6eed        nginx               "nginx -g ‘daemon of…"   4 weeks ago         Exited (0) 4 weeks ago                            adoring_poincare
fea01895c580        hello-world         "/hello"                 4 weeks ago         Exited (0) 4 weeks ago                            vibrant_goldwasser  

上面的例子是我們通過容器ID,刪除兩個hello-world容器文件。

3 編寫Dockerfile

學會使用 image 文件以後,接下來的問題就是,如何可以生成 image 文件?如果你要推廣自己的軟件,勢必要自己制作 image 文件。這就需要用到 Dockerfile 文件。它是一個文本文件,用來配置 image。Docker 根據 該文件生成二進制的 image 文件。鏡像的定制實際上就是定制每一層所添加的配置、文件。如果我們可以把每一層修改、安裝、構建、操作的命令都寫入一個腳本,用這個腳本來構建、定制鏡像,可以重復的使用、鏡像構建透明。Dockerfile 是一個文本文件,其內包含了一條條的指令(Instruction),每一條指令構建一層因此每一條指令的內容,就是描述該層應當如何構建

以 nginx 鏡像為例,這次我們使用 Dockerfile 來定制。

mkdir -p /search/odin/xnginx    # 新建一個目錄
cd  /search/odin/xnginx         # cd 到該目錄下
touch Dockerfile                # 創建一個叫Dockerfile的文件    

在Dockerfile中寫入下面兩行,保存退出:

FROM nginx
RUN echo ‘<h1>Hello, Docker! Hello, xnginx!</h1>‘ > /usr/share/nginx/html/index.html  

這個Dockerfile很簡單,涉及到了兩條指令,FROMRUN。

(1)FROM指定基礎鏡像
定制鏡像,那一定是以一個鏡像為基礎,在其上進行定制。就像我們之前運行了一個 nginx 鏡像的容器,再進行修改一樣,基礎鏡像是必須指定的。
FROM 就是指定基礎鏡像,因此一個 Dockerfile 中 FROM 是必備的指令並且必須是第一條指令

在 Docker Store 上有非常多的高質量的官方鏡像,有可以直接拿來使用的服務類的鏡像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;也有一些方便開發、構建、運行各種語言應用的鏡像,如 node、openjdk、python、ruby、golang 等。可以在其中尋找一個最符合我們最終目標的鏡像為基礎鏡像進行定制。
如果沒有找到對應服務的鏡像,官方鏡像中還提供了一些更為基礎的操作系統鏡像,如 ubuntu、debian、centos、fedora、alpine 等,這些操作系統的軟件庫為我們提供了更廣闊的擴展空間。

除了選擇現有鏡像為基礎鏡像外,Docker 還存在一個特殊的鏡像,名為 scratch。這個鏡像是虛擬的概念,並不實際存在,它表示一個空白的鏡像。

FROM scratch
...

以 scratch 為基礎鏡像的話,意味著你不以任何鏡像為基礎,接下來所寫的指令將作為鏡像第一層開始存在。
不以任何系統為基礎,直接將可執行文件復制進鏡像的做法並不罕見,比如 swarm、coreos/etcd。對於 Linux 下靜態編譯的程序來說,並不需要有操作系統提供運行時支持,所需的一切庫都已經在可執行文件裏了,因此直接 FROM scratch 會讓鏡像體積更加小巧。使用 Go 語言 開發的應用很多會使用這種方式來制作鏡像,這也是為什麽有人認為 Go 是特別適合容器微服務架構的語言的原因之一。

(2)RUN 執行命令
RUN 指令是用來執行命令行命令的。由於命令行的強大能力,RUN 指令在定制鏡像時是最常用的指令之一。其格式有兩種:

格式一: 
shell 格式:RUN <命令>,就像直接在命令行中輸入的命令一樣。剛才寫的 Dockerfile 中的 RUN 指令就是這種格式。
RUN echo ‘<h1>Hello, Docker!</h1>‘ > /usr/share/nginx/html/index.html

格式二:
exec 格式:RUN ["可執行文件", "參數1", "參數2"],這更像是函數調用中的格式。

RUN 就像 Shell 腳本一樣可以執行命令,很多初學者在寫Dockerfile的時候會像Shell 腳本一樣把每個命令對應一個 RUN,比如這樣:

FROM debian:jessie

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

Dockerfile 中每一個指令都會建立一層,RUN 也不例外。每一個 RUN 的行為,就和剛才我們手工建立鏡像的過程一樣:新建立一層,在其上執行這些命令,執行結束後,commit 這一層的修改,構成新的鏡像。
而上面的這種寫法,創建了 7 層鏡像。這是完全沒有意義的,而且很多運行時不需要的東西,都被裝進了鏡像裏,比如編譯環境、更新的軟件包等等。結果就是產生非常臃腫、非常多層的鏡像,不僅僅增加了構建部署的時間,也很容易出錯。

正確的寫法是:

FROM debian:jessie

RUN buildDeps=‘gcc libc6-dev make‘     && apt-get update     && apt-get install -y $buildDeps     && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"     && mkdir -p /usr/src/redis     && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1     && make -C /usr/src/redis     && make -C /usr/src/redis install     && rm -rf /var/lib/apt/lists/*     && rm redis.tar.gz     && rm -r /usr/src/redis     && apt-get purge -y --auto-remove $buildDeps

(1)之前所有的命令只有一個目的,就是編譯、安裝 redis 可執行文件。
因此沒有必要建立很多層,這只是一層的事情。這裏沒有使用很多個 RUN 對一一對應不同的命令,而是僅僅使用一個 RUN 指令,並使用 && 將各個所需命令串聯起來。將之前的 7 層,簡化為了 1 層。在撰寫 Dockerfile 的時候,要經常提醒自己,這並不是在寫 Shell 腳本,而是在定義每一層該如何構建。
(2)這裏為了格式化還進行了換行。
Dockerfile 支持 Shell 類的行尾添加 \ 的命令換行方式,以及行首 # 進行註釋的格式。良好的格式,比如換行、縮進、註釋等,會讓維護、排障更為容易,這是一個比較好的習慣。
(3)還可以看到這一組命令的最後添加了清理工作的命令,刪除了為了編譯構建所需要的軟件,清理了所有下載、展開的文件,並且還清理了 apt 緩存文件。
這是很重要的一步,我們之前說過,鏡像是多層存儲,每一層的東西並不會在下一層被刪除,會一直跟隨著鏡像。因此鏡像構建時,一定要確保每一層只添加真正需要添加的東西,任何無關的東西都應該清理掉。
很多人初學 Docker 制作出了很臃腫的鏡像的原因之一,就是忘記了每一層構建的最後一定要清理掉無關文件

4 構建鏡像

到目前為止,我們明白了這個 Dockerfile 的內容,接下來就讓我們構建這個鏡像吧。在 Dockerfile 文件所在目錄執行:

docker build -t nginx:v2 .  

詳細如下:

[@sjs_123_183 xnginx]# docker build -t nginx:v2 .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
 ---> c5c4e8fa2cf7
Step 2/2 : RUN echo ‘<h1>Hello, Docker! Hello, xnginx!</h1>‘ > /usr/share/nginx/html/index.html
 ---> Running in e955070ac2c9
Removing intermediate container e955070ac2c9
 ---> 1beca7b40dee
Successfully built 1beca7b40dee
Successfully tagged nginx:v2
[@sjs_123_183 xnginx]#

從命令的輸出結果中,我們可以清晰的看到鏡像的構建過程。
在 Step 2 中,如同我們之前所說的那樣,RUN 指令啟動了一個容器 e955070ac2c9,執行了所要求的命令,並最後提交了這一層 1beca7b40dee,隨後刪除了所用到的這個容器 e955070ac2c9。
這裏我們使用了 docker build 命令進行鏡像構建。其格式為:

docker build [選項] <上下文路徑/URL/->

這裏我們指定了最終鏡像的名稱 -t nginx:v2,現在讓我們啟動自己構建的容器:

docker run --name webserver -d -p 80:80 nginx:v2

這條命令會用 nginx 鏡像啟動一個容器,命名為 webserver,並且映射了 80 端口,這樣我們可以用curl 命令去訪問這個 nginx 服務器。詳情如下:

[@sjs_123_183 ~]# docker run --name webserver -d -p 80:80 nginx:v2
a9f012a96d98262bffd30286c9d23dfe929b032c2149fa67105d29ddde71b763
[@sjs_123_183 ~]# curl http://127.0.0.1:80
<h1>Hello, Docker! Hello, xnginx!</h1>
[@sjs_123_183 ~]#  

通過瀏覽器也是可以訪問的,如果你是本機的瀏覽器需要訪問:http://localhost。此處由於我在遠程的linux上,只需要訪問ip即可。

技術分享圖片

5 上下文路徑

細心的同學可能已經註意到docker build 命令最後有一個 . 號。. 表示當前目錄,而 Dockerfile 就在當前目錄,因此不少初學者以為這個路徑是在指定 Dockerfile 所在路徑,這麽理解其實是不準確的。
前文已經說到docker build 的命令格式,這個 . 號是指定上下文路徑。那麽什麽是上下文呢?

(1)首先我們要理解 docker build 的工作原理。Docker 在運行時分為 Docker 引擎(也就是服務端守護進程)和客戶端工具。Docker 的引擎提供了一組 REST API,被稱為 Docker Remote API,而如 docker 命令這樣的客戶端工具,則是通過這組 API 與 Docker 引擎交互,從而完成各種功能。
因此,雖然表面上我們好像是在本機執行各種 docker 功能,但實際上,一切都是使用的遠程調用形式在服務端(Docker 引擎)完成。也因為這種 C/S 設計,讓我們操作遠程服務器的 Docker 引擎變得輕而易舉。

(2)當我們進行鏡像構建的時候,並非所有定制都會通過 RUN 指令完成,經常會需要將一些本地文件復制進鏡像,比如通過 COPY 指令、ADD 指令等。而 docker build 命令構建鏡像,其實並非在本地構建,而是在服務端,也就是 Docker 引擎中構建的。那麽在這種客戶端/服務端的架構中,如何才能讓服務端獲得本地文件呢
這就引入了上下文的概念。當構建的時候,用戶會指定構建鏡像上下文的路徑,docker build 命令得知這個路徑後,會將路徑下的所有內容打包,然後上傳給 Docker 引擎。這樣 Docker 引擎收到這個上下文包後,展開就會獲得構建鏡像所需的一切文件。

如果在 Dockerfile 中這麽寫:

COPY ./package.json /app/

這並不是要復制執行 docker build 命令所在的目錄下的 package.json,也不是復制 Dockerfile 所在目錄下的 package.json,而是復制 上下文(context) 目錄下的 package.json
因此,COPY 這類指令中的源文件的路徑都是相對路徑。這也是初學者經常會問的為什麽 COPY ../package.json /app 或者 COPY /opt/xxxx /app 無法工作的原因,因為這些路徑已經超出了上下文的範圍,Docker 引擎無法獲得這些位置的文件。如果真的需要那些文件,應該將它們復制到上下文目錄中去。
現在就可以理解剛才的命令 docker build -t nginx:v2 . 中的這個 .,實際上是在指定上下文的目錄,docker build 命令會將該目錄下的內容打包交給 Docker 引擎以幫助構建鏡像。

在docker build命令的輸出內容中有這麽一句:

Sending build context to Docker daemon  2.048kB  

這實際上就是發送上下文的過程。

(1)理解構建上下文對於鏡像構建是很重要的,避免犯一些不應該的錯誤。
比如有些初學者在發現 COPY /opt/xxxx /app 不工作後,於是幹脆將 Dockerfile 放到了硬盤根目錄去構建,結果發現 docker build 執行後,在發送一個幾十 GB 的東西,極為緩慢而且很容易構建失敗。那是因為這種做法是在讓 docker build 打包整個硬盤,這顯然是使用錯誤。

(2)正確的做法是,將 Dockerfile 置於一個空目錄下,或者項目根目錄下
如果該目錄下沒有所需文件,那麽應該把所需文件復制一份過來。如果目錄下有些東西確實不希望構建時傳給 Docker 引擎,那麽可以用 .gitignore 一樣的語法寫一個 .dockerignore,該文件是用於剔除不需要作為上下文傳遞給 Docker 引擎的。

(3)那麽為什麽會有人誤以為 . 是指定 Dockerfile 所在目錄呢?
這是因為在默認情況下,如果不額外指定 Dockerfile 的話,會將上下文目錄下的名為 Dockerfile 的文件作為 Dockerfile。
這只是默認行為,實際上 Dockerfile 的文件名並不要求必須為 Dockerfile,而且並不要求必須位於上下文目錄中,比如可以用 -f ../Dockerfile.php 參數指定某個文件作為 Dockerfile。
當然,一般大家習慣性的會使用默認的文件名 Dockerfile,以及會將其置於鏡像構建上下文目錄中。

Docker快速入門(二)