Docker 使用镜像 - 二

使用commit理解镜像构成

docker commit 命令除了学习之外,还有一些特殊的场合适合使用,比如被入侵后保存现场等。
定制镜像,应该使用 Dockerfile 来完成,不要使用 docker commit 定制镜像。

镜像是容器的基础,每次执行 docker run 的时候都会指定哪个镜像作为容器运行的基础。
直接使用镜像可以满足一定的需求,当镜像无法直接满足的时候,就需要定制镜像。

定制一个 web 应用服务器

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

直接用浏览器访问

访问最初返回

定制化web页面,可以将文字进行更改,使用docker exec 命令进入到容器,再进行内容修改

[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 命令,可以将容器的存储层保存下来成为镜像。也就是说,在原有的镜像基础上,再叠加容器的存储层,构建成新的镜像。

docker commit 的语法格式为:

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]
[root@ip-10-1-0-142 ~]# docker commit
--author "BigMay"
--message "modify html"
webserver
nginx:v2
sha256:a09c32d8e04de96e40148839d9fce30f8bee17c5138c2745e6e44199576b0534

  • 其中 --author 是指定修改的作者,而 --message 则是记录本次修改的内容。这点和 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 具体查看镜像内的历史记录,对比nginx:latest 和 nginx:v2的历史记录

对比

使用新的镜像部署容器

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

慎用 docker commit

使用 docker commit 可以比较直观的理解镜像分层存储的概念,但是在实际的环境中不会这么使用。
在修改文件的过程中,由于命令的执行,可能会有多个文件被动或添加了。不仅仅是简单的操作,如果是安装软件包、编译构建,就可能会有大量的无关内容被添加进来,这样会导致镜像非常臃肿。
另外,使用 docker commit 就是对所有的镜像的操作都是黑箱操作,生成的镜像被称为 黑箱镜像 。也就是说,除了制作镜像的人知道执行了什么命令、怎么生成的镜像,别人无法知道做过哪些操作。而且,如果制作镜像的人,过一段时间后无法记清具体的操作。这种黑箱镜像的维护工作就非常痛苦。

回顾之前提及的镜像所使用的分层存储的概念,除了当前层外,之前的每一层都是不会发生改变的,任何修改的结果只是在当前层进行了标记、添加、修改,不会改动上一层。

使用Dockerfile 定制镜像

Dockerfile 是一个文本文件,其中包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
对比 docker commit,使用Dockerfile 可以吧每一层修改、安装、构建、操作的命令都写入一个脚本,利用这个脚本来构建、定制镜像。

以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

非常简单,只有两行,只涉及到两条指令,FROM 和 RUN

FROM 指定基础镜像

定制镜像就是以一个镜像为基础,在其上进行定制。
FROM 就是指定的 基础镜像,所以在一个Dockerfile 中 FROM的指令是必备的,并且必须是第一条指令。

Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 nodeopenjdk、python、ruby、golang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu、debian、centosfedora、alpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch
...

如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

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 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个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

其实这样也是可以的,只是创建了7层。

Dockerfile 中每一个指令都会建立一层, RUN 也不例外。每一个 RUN 的行为,都会新建立一层,在其上执行这些命令,待执行结束后,commit 这一层的修改,构建新的镜像。

再部署一个应用或一个环境的时候,把每一个步骤的执行建立一层是没有必要的,很多运行时不需要的东西都单独封装在一层,这样会导致image 非常臃肿。比如编译环境、更新的软件包等等。不仅让image产生了臃肿和多层,而且还非常耗时。

Union FS是有最大层数限制的,比如AUFS,曾经是最大不超过42层,现在是不超过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可执行文件。只是创建一层的需求,只使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将7个 RUN ,简化为一个 RUN,1层。
所以,在撰写 Dockerfile 的时候,要经常提醒自己,不是在写 shell 脚本,而是在定义每一层要如何构建。

并且,这里为了格式化海进行了换行。Dockerfile 支持 shell 类的行尾添加 \ 的命令换行方式,以及可以使用 # 进行注释的格式。
良好的格式,比如换行、缩紧、注释等,会让维护、排障更为容易,是一个比较良好的习惯。

构建镜像

使用定制的 nginx 镜像的 Dockerfile来构建镜像。

在 Dockerfile 文件所在的目录执行:

[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

从命令输出的结果,可以清晰的看到镜像构建的过程。
在 Step 2 中,RUN 指令启动了一个容器 c71a877f386f ,执行了所要求的命令,并最后提交了这一层 31aa55b35c46 ,随后删除了所用到的这个容器 c71a877f386f 。

使用 docker build 命令进行镜像构建。格式为:

docker build [选项] <上下文路径/URL/->

在这里我们指定了最终镜像的名称 -t nginx:v3,构建成功后,我们可以像之前运行 nginx:v2 那样来运行这个镜像,其结果会和 nginx:v2 一样。

镜像构建上下文(Context)

在运行 docker build 命令最后有一个 . 。. 表示当前目录,而 Dockerfile 就在当前目录,这是指定上下文路径。上下文是什么?

首先我们要理解 docker build 的工作原理。Docker 在运行时分为Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,如 docker 命令这样的客户端工具,则是通过这组API 与 Docker 引擎交互来完成各种功能。
所以,看似我们是在本机执行各种 docker 功能命令,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 架构设计,让我们操作远程服务器的Docker 引擎变得简单。

当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经?;嵝枰恍┍镜匚募粗频骄迪?,比如通过 copy 指令、add 指令等等。而docker build 命令构建镜像,是在服务端进行的,不是在本地进行的,也就是在 docker engine 中构建的。那么,如何才能让服务器端获得本地的文件呢?

这就引入了上下文的概念,当构建的时候,用户会指定构建镜像上下文的路径, 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:v3 . 中的这个 .,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:

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

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build 打包整个硬盘,这显然是使用错误。

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 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

一般情况下都会使用默认的文件名 Dockerfile,以及会将其放置于镜像构建上下文目录中。

其他 docker build 的用法

直接用 Git repo 进行构建

[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 构建,比如可以直接从 Git repo 中构建:

[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

如果出现错误信息,如下:

[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

先在系统安装git,如 yum install -y git ,然后再进行镜像 build

用给定的 tar 压缩包构建

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

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

从标准输入中读取 Dockerfile 进行构建

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

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容