Docker 使用鏡像 - 二

使用commit理解鏡像構(gòu)成

docker commit 命令除了學(xué)習(xí)之外,還有一些特殊的場合適合使用,比如被入侵后保存現(xiàn)場等。
定制鏡像,應(yīng)該使用 Dockerfile 來完成,不要使用 docker commit 定制鏡像。

鏡像是容器的基礎(chǔ),每次執(zhí)行 docker run 的時候都會指定哪個鏡像作為容器運行的基礎(chǔ)。
直接使用鏡像可以滿足一定的需求,當(dāng)鏡像無法直接滿足的時候,就需要定制鏡像。

定制一個 web 應(yīng)用服務(wù)器

[root@ip-10-1-0-142 ~]# docker run --name webserver -itd -p 80:80 nginx:latest
e829ded69fa81c0afddcce54109898e02e15a205cf07e63d101de8c572dc7a54

直接用瀏覽器訪問

訪問最初返回

定制化web頁面,可以將文字進行更改,使用docker exec 命令進入到容器,再進行內(nèi)容修改

[root@ip-10-1-0-142 ~]# docker exec -it webserver bash
root@e829ded69fa8:/# echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html


更新后的返回

修改了容器的文件,也就是改動了容器的存儲層。使用 docker diff 命令查看具體的改動

修改之前
[root@ip-10-1-0-142 ~]# docker diff e8
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
C /run
A /run/nginx.pid
C /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf

修改之后
[root@ip-10-1-0-142 ~]# docker diff webserver
C /run
A /run/nginx.pid
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /root
A /root/.bash_history
C /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp

鏡像定制好,然后進行鏡像保存。
我們運行在一個容器的時候(不使用卷),我們的任何文件修改都會被記錄于容器存儲層里。而Docker 提供了一個docker commit 命令,可以將容器的存儲層保存下來成為鏡像。也就是說,在原有的鏡像基礎(chǔ)上,再疊加容器的存儲層,構(gòu)建成新的鏡像。

docker commit 的語法格式為:

docker commit [選項] <容器ID或容器名> [<倉庫名>[:<標(biāo)簽>]]
[root@ip-10-1-0-142 ~]# docker commit
--author "BigMay"
--message "modify html"
webserver
nginx:v2
sha256:a09c32d8e04de96e40148839d9fce30f8bee17c5138c2745e6e44199576b0534

  • 其中 --author 是指定修改的作者,而 --message 則是記錄本次修改的內(nèi)容。這點和 git 版本控制相似,不過這里這些信息可以省略留空。

查看定制好的鏡像文件

[root@ip-10-1-0-142 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v2 a09c32d8e04d About a minute ago 133MB
nginx latest 08b152afcfae 4 days ago 133MB
httpd latest 73b8cfec1155 4 days ago 138MB
centos latest 300e315adb2f 7 months ago 209MB

還可以使用 docker history 具體查看鏡像內(nèi)的歷史記錄,對比nginx:latest 和 nginx:v2的歷史記錄

對比

使用新的鏡像部署容器

[root@ip-10-1-0-142 ~]# docker run -it --name webservr2 -p 8080:80 nginx:v2

慎用 docker commit

使用 docker commit 可以比較直觀的理解鏡像分層存儲的概念,但是在實際的環(huán)境中不會這么使用。
在修改文件的過程中,由于命令的執(zhí)行,可能會有多個文件被動或添加了。不僅僅是簡單的操作,如果是安裝軟件包、編譯構(gòu)建,就可能會有大量的無關(guān)內(nèi)容被添加進來,這樣會導(dǎo)致鏡像非常臃腫。
另外,使用 docker commit 就是對所有的鏡像的操作都是黑箱操作,生成的鏡像被稱為 黑箱鏡像 。也就是說,除了制作鏡像的人知道執(zhí)行了什么命令、怎么生成的鏡像,別人無法知道做過哪些操作。而且,如果制作鏡像的人,過一段時間后無法記清具體的操作。這種黑箱鏡像的維護工作就非常痛苦。

回顧之前提及的鏡像所使用的分層存儲的概念,除了當(dāng)前層外,之前的每一層都是不會發(fā)生改變的,任何修改的結(jié)果只是在當(dāng)前層進行了標(biāo)記、添加、修改,不會改動上一層。

使用Dockerfile 定制鏡像

Dockerfile 是一個文本文件,其中包含了一條條的指令,每一條指令構(gòu)建一層,因此每一條指令的內(nèi)容,就是描述該層應(yīng)當(dāng)如何構(gòu)建。
對比 docker commit,使用Dockerfile 可以吧每一層修改、安裝、構(gòu)建、操作的命令都寫入一個腳本,利用這個腳本來構(gòu)建、定制鏡像。

以nginx鏡像為例,使用 Dockerfile 來定制。
首先,在一個空白目錄中,建立一個文本文件,命名為 Dockerfile :

[root@ip-10-1-0-142 ~]# mkdir mynginx
[root@ip-10-1-0-142 ~]# cd mynginx/
[root@ip-10-1-0-142 mynginx]# touch Dockerfile
[root@ip-10-1-0-142 mynginx]# vim Dockerfile
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

非常簡單,只有兩行,只涉及到兩條指令,F(xiàn)ROM 和 RUN

FROM 指定基礎(chǔ)鏡像

定制鏡像就是以一個鏡像為基礎(chǔ),在其上進行定制。
FROM 就是指定的 基礎(chǔ)鏡像,所以在一個Dockerfile 中 FROM的指令是必備的,并且必須是第一條指令。

Docker Hub 上有非常多的高質(zhì)量的官方鏡像,有可以直接拿來使用的服務(wù)類的鏡像,如 nginx、redismongo、mysql、httpd、php、tomcat 等;也有一些方便開發(fā)、構(gòu)建、運行各種語言應(yīng)用的鏡像,如 node、openjdk、pythonruby、golang 等。可以在其中尋找一個最符合我們最終目標(biāo)的鏡像為基礎(chǔ)鏡像進行定制。

如果沒有找到對應(yīng)服務(wù)的鏡像,官方鏡像中還提供了一些更為基礎(chǔ)的操作系統(tǒng)鏡像,如 ubuntu、debiancentos、fedoraalpine 等,這些操作系統(tǒng)的軟件庫為我們提供了更廣闊的擴展空間。

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

FROM scratch
...

如果你以 scratch 為基礎(chǔ)鏡像的話,意味著你不以任何鏡像為基礎(chǔ),接下來所寫的指令將作為鏡像第一層開始存在。

不以任何系統(tǒng)為基礎(chǔ),直接將可執(zhí)行文件復(fù)制進鏡像的做法并不罕見,對于 Linux 下靜態(tài)編譯的程序來說,并不需要有操作系統(tǒng)提供運行時支持,所需的一切庫都已經(jīng)在可執(zhí)行文件里了,因此直接 FROM scratch 會讓鏡像體積更加小巧。使用 Go 語言 開發(fā)的應(yīng)用很多會使用這種方式來制作鏡像,這也是為什么有人認(rèn)為 Go 是特別適合容器微服務(wù)架構(gòu)的語言的原因之一。

RUN 執(zhí)行命令

RUN 指令是用來執(zhí)行命令行命令的。由于命令行的強大能力,RUN 指令在指定鏡像時是最常用的指令之一。其格式有兩種:
shell 格式:RUN <命令>,就像直接在命令行中輸入的命令一樣。剛才寫的 Dockerfile 中的 RUN 指令就是這種格式。

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

exec 格式:RUN ["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"],這更像是函數(shù)調(diào)用中的格式。
既然 RUN 就像shell 腳本一樣可以執(zhí)行命令,那么我們是否就可以像 Shell 腳本一樣把每個命令對應(yīng)一個RUN呢?比如:

FROM debian:stretch

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.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

其實這樣也是可以的,只是創(chuàng)建了7層。

Dockerfile 中每一個指令都會建立一層, RUN 也不例外。每一個 RUN 的行為,都會新建立一層,在其上執(zhí)行這些命令,待執(zhí)行結(jié)束后,commit 這一層的修改,構(gòu)建新的鏡像。

再部署一個應(yīng)用或一個環(huán)境的時候,把每一個步驟的執(zhí)行建立一層是沒有必要的,很多運行時不需要的東西都單獨封裝在一層,這樣會導(dǎo)致image 非常臃腫。比如編譯環(huán)境、更新的軟件包等等。不僅讓image產(chǎn)生了臃腫和多層,而且還非常耗時。

Union FS是有最大層數(shù)限制的,比如AUFS,曾經(jīng)是最大不超過42層,現(xiàn)在是不超過127層

上面的Dockerfile 正確的寫法是:

FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget'
&& apt-get update
&& apt-get install -y $buildDeps
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.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

首先,之前所有的命令只有一個目的,就是編譯安裝redis可執(zhí)行文件。只是創(chuàng)建一層的需求,只使用一個 RUN 指令,并使用 && 將各個所需命令串聯(lián)起來。將7個 RUN ,簡化為一個 RUN,1層。
所以,在撰寫 Dockerfile 的時候,要經(jīng)常提醒自己,不是在寫 shell 腳本,而是在定義每一層要如何構(gòu)建。

并且,這里為了格式化海進行了換行。Dockerfile 支持 shell 類的行尾添加 \ 的命令換行方式,以及可以使用 # 進行注釋的格式。
良好的格式,比如換行、縮緊、注釋等,會讓維護、排障更為容易,是一個比較良好的習(xí)慣。

構(gòu)建鏡像

使用定制的 nginx 鏡像的 Dockerfile來構(gòu)建鏡像。

在 Dockerfile 文件所在的目錄執(zhí)行:

[root@ip-10-1-0-142 mynginx]# docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : RUN echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html
---> Running in c71a877f386f
Removing intermediate container c71a877f386f
---> 31aa55b35c46
Successfully built 31aa55b35c46
Successfully tagged nginx:v3

從命令輸出的結(jié)果,可以清晰的看到鏡像構(gòu)建的過程。
在 Step 2 中,RUN 指令啟動了一個容器 c71a877f386f ,執(zhí)行了所要求的命令,并最后提交了這一層 31aa55b35c46 ,隨后刪除了所用到的這個容器 c71a877f386f 。

使用 docker build 命令進行鏡像構(gòu)建。格式為:

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

在這里我們指定了最終鏡像的名稱 -t nginx:v3,構(gòu)建成功后,我們可以像之前運行 nginx:v2 那樣來運行這個鏡像,其結(jié)果會和 nginx:v2 一樣。

鏡像構(gòu)建上下文(Context)

在運行 docker build 命令最后有一個 . 。. 表示當(dāng)前目錄,而 Dockerfile 就在當(dāng)前目錄,這是指定上下文路徑。上下文是什么?

首先我們要理解 docker build 的工作原理。Docker 在運行時分為Docker 引擎(也就是服務(wù)端守護進程)和客戶端工具。Docker 的引擎提供了一組 REST API,被稱為 Docker Remote API,如 docker 命令這樣的客戶端工具,則是通過這組API 與 Docker 引擎交互來完成各種功能。
所以,看似我們是在本機執(zhí)行各種 docker 功能命令,但實際上,一切都是使用的遠程調(diào)用形式在服務(wù)端(Docker 引擎)完成。也因為這種 C/S 架構(gòu)設(shè)計,讓我們操作遠程服務(wù)器的Docker 引擎變得簡單。

當(dāng)我們進行鏡像構(gòu)建的時候,并非所有定制都會通過 RUN 指令完成,經(jīng)常會需要將一些本地文件復(fù)制到鏡像,比如通過 copy 指令、add 指令等等。而docker build 命令構(gòu)建鏡像,是在服務(wù)端進行的,不是在本地進行的,也就是在 docker engine 中構(gòu)建的。那么,如何才能讓服務(wù)器端獲得本地的文件呢?

這就引入了上下文的概念,當(dāng)構(gòu)建的時候,用戶會指定構(gòu)建鏡像上下文的路徑, docker build 命令得知這個路徑后,會將路徑下的所有內(nèi)容打包,然后上傳到 Docker 引擎。Docker 引擎收到這個上下文包后,展開就會獲得構(gòu)建鏡像所需的一切文件。

如果在 Dockerfile 中這么寫:

COPY ./package.json /app/

這不是要復(fù)制執(zhí)行 docker build 命令所在目錄下的 package.json文件,也不是復(fù)制 Dockerfile 所在目錄下的 package.json,而是復(fù)制 上下文(context)目錄下的 package.json 文件。

因此,COPY 這類指令中的源文件的路徑都是相對路徑。這也是初學(xué)者經(jīng)常會問的為什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 無法工作的原因,因為這些路徑已經(jīng)超出了上下文的范圍,Docker 引擎無法獲得這些位置的文件。如果真的需要那些文件,應(yīng)該將它們復(fù)制到上下文目錄中去。

現(xiàn)在就可以理解剛才的命令 docker build -t nginx:v3 . 中的這個 .,實際上是在指定上下文的目錄,docker build 命令會將該目錄下的內(nèi)容打包交給 Docker 引擎以幫助構(gòu)建鏡像。

如果觀察 docker build 輸出,我們其實已經(jīng)看到了這個發(fā)送上下文的過程:

[root@ip-10-1-0-142 mynginx]# docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048kB

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

一般來說,應(yīng)該會將 Dockerfile 置于一個空目錄下,或者項目根目錄下。如果該目錄下沒有所需文件,那么應(yīng)該把所需文件復(fù)制一份過來。如果目錄下有些東西確實不希望構(gòu)建時傳給 Docker 引擎,那么可以用 .gitignore 一樣的語法寫一個 .dockerignore,該文件是用于剔除不需要作為上下文傳遞給 Docker 引擎的。

那么為什么會有人誤以為 . 是指定 Dockerfile 所在目錄呢?這是因為在默認(rèn)情況下,如果不額外指定 Dockerfile 的話,會將上下文目錄下的名為 Dockerfile 的文件作為 Dockerfile。

[root@ip-10-1-0-142 mynginx]# docker build -t nginx:v5 -f demo .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : RUN echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html
---> Using cache
---> 31aa55b35c46
Successfully built 31aa55b35c46
Successfully tagged nginx:v5

一般情況下都會使用默認(rèn)的文件名 Dockerfile,以及會將其放置于鏡像構(gòu)建上下文目錄中。

其他 docker build 的用法

直接用 Git repo 進行構(gòu)建

[root@ip-10-1-0-142 mynginx]# docker build --help

Usage: docker build [OPTIONS] PATH | URL | -

Build an image from a Dockerfile

Options:
--add-host list Add a custom host-to-IP mapping (host:ip)
--build-arg list Set build-time variables
--cache-from strings Images to consider as cache sources
--cgroup-parent string Optional parent cgroup for the container
--compress Compress the build context using gzip
--cpu-period int Limit the CPU CFS (Completely Fair Scheduler) period
--cpu-quota int Limit the CPU CFS (Completely Fair Scheduler) quota
-c, --cpu-shares int CPU shares (relative weight)
--cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)
--cpuset-mems string MEMs in which to allow execution (0-3, 0,1)
--disable-content-trust Skip image verification (default true)
-f, --file string Name of the Dockerfile (Default is 'PATH/Dockerfile')
--force-rm Always remove intermediate containers
--iidfile string Write the image ID to the file
--isolation string Container isolation technology
--label list Set metadata for an image
-m, --memory bytes Memory limit
--memory-swap bytes Swap limit equal to memory plus swap: '-1' to enable unlimited swap
--network string Set the networking mode for the RUN instructions during build (default
"default")
--no-cache Do not use cache when building the image
--pull Always attempt to pull a newer version of the image
-q, --quiet Suppress the build output and print image ID on success
--rm Remove intermediate containers after a successful build (default true)
--security-opt strings Security options
--shm-size bytes Size of /dev/shm
-t, --tag list Name and optionally a tag in the 'name:tag' format
--target string Set the target build stage to build.
--ulimit ulimit Ulimit options (default [])
[root@ip-10-1-0-142 mynginx]# docker build -f nginx:v4 ./dem

docker build 還支持從 URL 構(gòu)建,比如可以直接從 Git repo 中構(gòu)建:

[root@ip-10-1-0-142 mynginx]# !229
docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
Sending build context to Docker daemon 19.46kB
Step 1/3 : FROM scratch
--->
Step 2/3 : COPY hello /
---> 73f19933b2d9
Step 3/3 : CMD ["/hello"]
---> Running in 0c6155341ab9
Removing intermediate container 0c6155341ab9
---> dd3f92cd2824
Successfully built dd3f92cd2824
Successfully tagged hello-world:latest

如果出現(xiàn)錯誤信息,如下:

[root@ip-10-1-0-142 mynginx]# docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
unable to prepare context: unable to find 'git': exec: "git": executable file not found in $PATH

先在系統(tǒng)安裝git,如 yum install -y git ,然后再進行鏡像 build

用給定的 tar 壓縮包構(gòu)建

docker build http://<server-001>/context.tar.gz

如果所給出的 URL 不是個 Git repo,而是個 tar 壓縮包,那么 Docker 引擎會下載這個包,并自動解壓縮,以其作為上下文,開始構(gòu)建。

從標(biāo)準(zhǔn)輸入中讀取 Dockerfile 進行構(gòu)建

docker build -t nginx:v1 - < Dockerfile

[root@ip-10-1-0-142 mynginx]# docker build -t nginx:v1 - < demo
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : RUN echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html
---> Running in 4a04eff723cd
Removing intermediate container 4a04eff723cd
---> a772a8d3759e
Successfully built a772a8d3759e
Successfully tagged nginx:v1

cat Dockerfile | docker build -

[root@ip-10-1-0-142 mynginx]# cat demo | docker build -t nginx:v2 -
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 08b152afcfae
Step 2/2 : RUN echo '<h1>Hello, Word!</h1>' > /usr/share/nginx/html/index.html
---> Using cache
---> a772a8d3759e
Successfully built a772a8d3759e
Successfully tagged nginx:v2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容