docker基础之通过Dockerfile定制镜像(七)
一、镜像的构成
在通过Dockerfile定制镜像之前,先理解下镜像的构成。
分层存储
我们知道 Docker 镜像 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。
因为镜像包含操作系统完整的 root
文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS (联合文件系统)的技术,将其设计为分层存储的架构。
所以,镜像并非是像一个 ISO
那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由多层文件系统联合组成。
利用commit 理解镜像构成
镜像是容器的基础,每次执行 docker run
的时候都会指定哪个镜像作为容器运行的基础。镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
要知道,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit
命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
(分层文件,只记录修改的部分,很像git,命令也相似,通过commit提交新更改)。
举个例子🌰:现在让我们以定制一个 Web 服务器为例子,来理解镜像是如何构建的。
① 从镜像仓库下载一个nginx的镜像
$ docker pull hub.c.163.com/public/nginx:1.2.1
② 定制Web服务器,用 nginx
镜像启动一个容器,命名为 webserver
,并且映射 80 端口
$ docker run --name webserver -d -p 80:80 nginx
现在,访问 http://localhost
看到默认的 Nginx 欢迎页面。
③ 通过 docker exec
命令进入容器修改 nginx 的 index.html 改变欢迎页的内容
$ docker exec -it webserver bash root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html root@3729b97e8226:/# exit exit
这个时候我们访问 http://localhost
看到的就是 Hello, Docker!了
修改了容器的文件,也就是改动了容器的存储层;通过 docker diff
命令看到具体的改动
$ docker diff webserver C /root A /root/.bash_history C /run C /usr C /usr/share C /usr/share/nginx C /usr/share/nginx/html C /usr/share/nginx/html/index.html C /var C /var/cache C /var/cache/nginx A /var/cache/nginx/client_temp A /var/cache/nginx/fastcgi_temp A /var/cache/nginx/proxy_temp A /var/cache/nginx/scgi_temp A /var/cache/nginx/uwsgi_temp
④ 用 docker commit
命令将容器保存为镜像
$ docker commit \ --author "newmeaning" \ --message "修改了index默认主页" \ webserver \ nginx:v2 sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214
通过 docker images nginx
命令可以看到新制定的镜像
$ docker images nginx REPOSITORY TAG IMAGE ID CREATED SIZE nginx v2 07e334659748 9 seconds ago 181.5 MB nginx 1.11 05a60462f8ba 12 days ago 181.5 MB nginx latest e43d811ce2f4 4 weeks ago 181.5 MB
通过 docker history
具体查看镜像内的历史记录,会发现新增了刚刚提交的这一层。
$ docker history nginx:v2 IMAGE CREATED CREATED BY SIZE COMMENT 07e334659748 54 seconds ago nginx -g daemon off; 95 B 修改了index默认主页 e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B <missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B <missing> 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B <missing> 4 weeks ago /bin/sh -c apt-key adv --keyserver hkp://pgp. 58.46 MB <missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.11.5-1 0 B <missing> 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B <missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B <missing> 4 weeks ago /bin/sh -c #(nop) ADD file:23aa4f893e3288698c 123 MB
⑤ 运行新镜像查看效果
docker run --name web2 -d -p 81:80 nginx:v2
这个时候我们访问 http://localhost
看到的是 Hello, Docker!
至此,我们第一次完成了定制镜像,使用的是 docker commit
命令,手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。
二、使用Dockerfile定制镜像
接下来我们开始学习使用Dockerfile定制镜像
Dockerfile对比docker commit
使用 docker commit
命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。
首先,如果仔细观察之前的 docker diff webserver
的结果,会发现除了真正想要修改的 /usr/share/nginx/html/index.html
文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。
此外,使用 docker commit
意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为 黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。
而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit
制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。
从刚才的 docker commit
的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
还以之前定制 nginx
镜像为例,这次我们使用 Dockerfile 来定制。
在一个空白目录中,建立一个文本文件,并命名为 Dockerfile
:
$ mkdir mynginx $ cd mynginx $ touch Dockerfile
内容为:
FROM nginx RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
这样一个Dockerfile就写好了,涉及到了两条指令,FROM
和 RUN
。
FROM 指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。而 FROM
就是指定 基础镜像,因此一个 Dockerfile
中 FROM
是必备的指令,并且必须是第一条指令。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
FROM scratch
...
对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch
会让镜像体积更加小巧。
RUN 执行命令
RUN
指令是用来执行命令行命令的。由于命令行的强大能力,RUN
指令在定制镜像时是最常用的指令之一。其格式有两种:
- shell 格式:
RUN <命令>
,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的RUN
指令就是这种格式。 - exec 格式:
RUN ["可执行文件", "参数1", "参数2"]
,这更像是函数调用中的格式。
需要注意的是,Dockerfile 中每一个指令都会建立一层,RUN
也不例外。每一个 RUN
的行为,就会新建立一层,在其上执行这些命令,执行结束后,commit
这一层的修改,构成新的镜像。
如下面这段指令,共创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。
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
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
这样将之前的 7 层,简化为了 1 层。此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt
缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
docker build 构建镜像
使用 docker build
命令进行镜像构建。其格式为:
$ docker build [选项] <上下文路径/URL/->
回到之前定制的 nginx 镜像的 Dockerfile 来,在 Dockerfile
文件所在目录执行:
$ docker build -t nginx:v3 . Sending build context to Docker daemon 2.048 kB Step 1 : FROM nginx ---> e43d811ce2f4 Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html ---> Running in 9cdc27646c7b ---> 44aa4490ce2c Removing intermediate container 9cdc27646c7b Successfully built 44aa4490ce2c
从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2
中,如同我们之前所说的那样,RUN
指令启动了一个容器 9cdc27646c7b
,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c
,随后删除了所用到的这个容器 9cdc27646c7b
。
镜像构建上下文(Context)
docker build
命令最后有一个 .
。.
表示当前目录,而 Dockerfile
就在当前目录,我们不应认为这个路径是在指定 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 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build
命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
现在就可以理解刚才的命令 docker build -t nginx:v3 .
中的这个 .
,实际上是在指定上下文的目录,docker build
命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
观察 docker build
输出,可以看到这个发送上下文的过程:
$ docker build -t nginx:v3 . Sending build context to Docker daemon 2.048 kB ...
因此,COPY
这类指令中的源文件的路径都是相对路径。这也是为什么 COPY ../package.json /app
或者 COPY /opt/xxxx /app
无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。
如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore
一样的语法写一个 .dockerignore
,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
另外,实际上 Dockerfile
的文件名并不要求必须为 Dockerfile
,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.cc
参数指定某个文件作为 Dockerfile
。
docker build 的更多用法
直接用 Git repo 进行构建
docker build
还支持从 URL 构建,比如可以直接从 Git repo 中构建:
# $env:DOCKER_BUILDKIT=0 # export DOCKER_BUILDKIT=0 $ docker build -t hello-world https://github.com/docker-library/hello-world.git #master:amd64/hello-world Step 1/3 : FROM scratch ---> Step 2/3 : COPY hello / ---> ac779757d46e Step 3/3 : CMD ["/hello"] ---> Running in d2a513a760ed Removing intermediate container d2a513a760ed ---> 038ad4142d2b Successfully built 038ad4142d2b
这行命令指定了构建所需的 Git repo,并且指定分支为 master
,构建目录为 /amd64/hello-world/
,然后 Docker 就会自己去 git clone
这个项目、切换到指定分支、并进入到指定目录后开始构建。
用指定的tar压缩包构建
如果所给出的 URL 不是个 Git repo,而是个 tar
压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。
$ docker build http://server/context.tar.gz
从标准输入中读取 Dockerfile 进行构建
docker build - < Dockerfile 或 cat Dockerfile | docker build - #如果文件名是Dockerfile可以不写 -f [文件路径]参数
如果标准输入传入的是文本文件,则将其视为 Dockerfile
,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY
进镜像之类的事情。
从标准输入中读取上下文压缩包进行构建
$ docker build - < context.tar.gz
如果发现标准输入的文件格式是 gzip
、bzip2
以及 xz
的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。
转 :https://www.cnblogs.com/nm666/p/15305561.html