Dockerfile 命令详解
-
FROM 指定基础镜像(必选)
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而
FROM
就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。在Docker hub上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如
nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;也有一些方便开发、构建、运行各种语言应用的镜像,如node
、openjdk
、python
、ruby
、golang
等。如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如
ubuntu
、debian
、centos
、fedora
、alpine
等。FROM
命令语法:FROM <image>:<tag>
如果
tag
没有选择,默认为latest
。除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为
scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。FROM scratch ...
如果你以
scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。有的同学可能感觉很奇怪,没有任何基础镜像,我怎么去执行我的程序呢,其实对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch
会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。下面我们以一个
go
语言的helloworld
为例:FROM scratch COPY helloworld / COPY hellowold2 / CMD ["./helloworld"]
helloworld
文件就是个go
语言编译出来的可执行程序,只会打印出hello world
。docker build -t hello-go:v1 . docker run hello-go:v1
-
LABEL 设置镜像元数据
使用
LABEL
指令,可以为镜像设置元数据,例如镜像创建者或者镜像说明。旧版的Dockerfile
语法使用MAINTAINER
指令指定镜像创建者,但是它已经被弃用了。LABEL
命令语法:LABEL <key>=<value> <key>=<value> <key>=<value> ...
一个Dockerfile种可以有多个
LABEL
,如下:LABEL maintainer="cerberus43@gmail.com" LABEL version="1.0" LABEL description="This is a test dockerfile"
但是并不建议这样写,最好就写成一行,如太长需要换行的话则使用
\
符号。如下:
LABEL maintainer="cerberus43@gmail.com" \ version="1.0" \ description="This is a test dockerfile"
说明:
LABEL
会继承基础镜像种的LABEL
,如遇到key相同,则值覆盖。 -
RUN 运行命令
使用
RUN
指令,可以用来执行命令行的命令。RUN
命令有两种语法:-
shell
格式:在linux操作系统上默认 /bin/sh -c
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
-
exec
格式:RUN ["可执行文件", "参数1", "参数2"]
注意:多行命令不要写多个RUN,原因是Dockerfile中每一个指令都会建立一层,多少个RUN就构建了多少层镜像,会造成镜像的臃肿、多层,不仅仅增加了构件部署的时间,还容易出错。
下面是一个使用
apt-get
安装多个包的例子:RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion
-
-
COPY 复制文件
COPY
命令有两种语法格式:-
COPY [--chown=<user>:<group>] <源路径>... <目标路径>
-
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
和
RUN
指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。说明:
- 目标路径可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用
WORKDIR
指令来指定)。 - 目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
- 使用
COPY
指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。
复制单个文件示例:
COPY package.json /usr/src/app/
<源路径>可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:
COPY hom* /mydir/ COPY hom?.txt /mydir/
复制src目录下内容到 /tmp 目录下:
COPY src/ /tmp
复制多个目录下内容到 /tmp 目录下:
COPY src1/ src2/ /tmp
上面的命令只会将文件夹内容复制到镜像目录下,复制整个src目录到/tmp目录下,如果源目录名不存在将自动逐级创建:
COPY src/ /tmp/src
指定文件权限
在使用该指令的时候还可以加上 --chown=: 选项来改变文件的所属用户及所属组。
COPY --chown=devuser:devgroup files* /mydir/
-
-
ADD 更高级的复制文件
ADD
命令和COPY
的格式和性质基本一致。但是在COPY
基础上增加了一些功能。-
解压压缩文件并把它们添加到镜像中:
WORKDIR /app ADD nginx.tar.gz .
-
从 url 拷贝文件到镜像中:
ADD http://example.com/big.tar.xz /usr/src/things/ RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things RUN make -C /usr/src/things all
但是在Dockerfile 最佳实践官方文档中却强烈建议不要这么用!官方建议我们当需要从远程复制文件时,最好使用curl或wget命令来代替ADD命令。原因是,当使用ADD命令时,会创建更多的镜像层,当然镜像也会变的更大。
RUN mkdir -p /usr/src/things \ && curl -SL http://example.com/big.tar.xz \ | tar -xJC /usr/src/things \ && make -C /usr/src/things all
在 Docker 官方的 Dockerfile 最佳实践官方文档 中要求,尽可能的使用
COPY
,因为COPY
的语义很明确,就是复制文件而已,而ADD
则包含了更复杂的功能,其行为也不一定很清晰。最适合使用ADD
的场合,就是所提及的需要自动解压缩的场合。因此在 COPY和 ADD指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用COPY指令,仅在需要自动解压缩的场合使用ADD。
-
-
WORKDIR 指定工作目录
使用
WORKDIR
指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。语法格式为:
WORKDIR <工作目录路径>
FROM centos:7.2 #创建/usr/local/tomcat目录 RUN mkdir /usr/local/tomcat #定位到tomcat下载目录 WORKDIR /usr/local/tomcat #wget tomcat到/usr/local/tomcat目录 RUN wget http://mirrors.hust.edu.cn/apache/tomcat/tomcat-7/v7.0.86/bin/apache-tomcat-7.0.86.tar.gz
-
ENV 指定容器的环境变量
使用
ENV
指令,可以设置环境变量,无论是后面的其它指令,如RUN
,还是运行时的应用,都可以直接使用这里定义的环境变量。语法格式有两种:
-
ENV <key> <value>
-
ENV <key1>=<value1> <key2>=<value2>...
定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方
node
镜像Dockerfile
中,就有类似这样的代码:ENV NODE_VERSION 7.2.0 RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \ && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \ && ln -s /usr/local/bin/node /usr/local/bin/nodejs
在这里先定义了环境变量
NODE_VERSION
,其后的RUN
这层里,多次使用$NODE_VERSION
来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新7.2.0
即可,Dockerfile
构建维护变得更轻松了。 -
-
ARG 指定Dockerfile中的环境变量
ARG
:ARG
定义的变量用于构建Docker
镜像,在把Dockerfile
构建成镜像后,ARG
定义的变量便不在起作用;ENV
:ENV
定义的变量用于容器的环境变量,在Dockerfile
里定义后,在容器的运行时是可以使用这个变量的;上面可能读起来比较绕,看下这个实例就明白了:
ARG VAR_A=1 ENV VAR_B ${VAR_A}
通过构建镜像并启动容器后,查看环境变量如下:
$ docker exec ContainerID env VAR_B=1
从实例可看出,
ARG
定义的变量在Dockerfile中
使用,构建完镜像后,就下岗;而ENV
定义的变量会带入容器的环境变量。通常可以把ARG与ENV结合使用:
ARG buildtime_variable=default_value ENV env_var_name=$buildtime_variable
使用这种方式可以解决Dockerfile硬编码的问题,比如在微服务下很多服务的情况下,构建一个镜像修改一次Dockerfile,而使用这种方式Dockerfile是不变的,只需要在docker build的时候加上参数值就可以。
-
CMD 指定镜像启动时的命令
首先我们看官网对
CMD
的定义:The main purpose of a CMD is to provide defaults for an executing container. These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT instruction as well.
意思是,
CMD
给出的是一个容器的默认的可执行体。也就是容器启动以后,默认的执行的命令。重点就是这个默认。意味着,如果docker run
没有指定任何的执行命令或者Dockerfile
里面也没有ENTRYPOINT
,那么,就会使用CMD
指定的默认的执行命令执行。同时也从侧面说明了ENTRYPOINT
的含义,它才是真正的容器启动以后要执行命令。所以这句话就给出了
CMD
命令的一个角色定位,它主要作用是默认的容器启动执行命令。(注意不是“全部”作用)这也是为什么大多数网上博客论坛说的“
CMD
会被覆盖”,其实为什么会覆盖?因为CMD
的角色定位就是默认,如果你不额外指定,那么就执行CMD
的命令,否则呢?只要你指定了,那么就不会执行CMD
,也就是CMD
会被覆盖。比如,
ubuntu
镜像默认的CMD
是/bin/bash
,如果我们直接docker run -it ubuntu
的话,会直接进入bash
。我们也可以在运行时指定运行别的命令,如docker run -it ubuntu cat /etc/os-release
。这就是用cat /etc/os-release
命令替换了默认的/bin/bash
命令了,输出了系统版本信息。明白了
CMD
命令的主要用途。下面就看看具体用法:The CMD instruction has three forms: CMD ["executable","param1","param2"] (exec form, this is the preferred form) #exec格式,首选方法 CMD ["param1","param2"] (as default parameters to ENTRYPOINT) #为ENTRYPOINT传参用法 CMD command param1 param2 (shell form) #shell格式
因为还没有讲
ENTRYPOINT
,所以先不用看第二种用法。在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号
"
,而不要使用单引号。如果使用
shell
格式的话,实际的命令会被包装为sh -c
的参数的形式进行执行。比如:CMD echo $HOME
在实际执行中,会将其变更为:
CMD [ "sh", "-c", "echo $HOME" ]
这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。
提到
CMD
就不得不提容器中应用在前台执行和后台执行的问题。这是常出现的一个混淆。Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用
systemd
去启动后台服务,容器内没有后台服务的概念。如有人会把写成这样:
CMD service nginx start
然后发现容器执行后就立即退出了。这就是因为没有搞明白前台、后台的概念,没有区分容器和 虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。
对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。
而使用
service nginx start
命令,则是希望以后台守护进程形式启动nginx
服务。而刚才说了CMD service nginx start
会被理解为CMD [ "sh", "-c", "service nginx start"]
,因此主进程实际上是sh
。那么当service nginx start
命令结束后,sh
也就结束了,sh
作为主进程退出了,自然就会令容器退出。正确的做法是直接执行
nginx
可执行文件,并且要求以前台形式运行:CMD ["nginx", "-g", "daemon off;"]
-
ENTRYPOINT 指定容器入口命令
首先我们看官网对
ENTRYPOINT
的定义:An ENTRYPOINT allows you to configure a container that will run as an executable.
也就是说
ENTRYPOINT
才是正统地用于定义容器启动以后的执行体的,其实我们从名字也可以理解,这个是容器的“入口”。它有两种用法:
ENTRYPOINT has two forms: ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred) #exec格式,首选方法 ENTRYPOINT command param1 param2 (shell form) #shell格式
先看
exec
命令行模式,也就是带中括号的。如果docker run
命令后面有东西,那么后面的全部都会作为ENTRYPOINT
的参数。如果docker run
后面没有额外的东西,但是CMD
有,那么CMD
的全部内容会作为ENTRYPOINT
的参数,这同时是CMD
的第二种用法。这也是网上说的ENTRYPOINT
不会被覆盖。当然如果要在docker run
里面覆盖,也是有办法的,使用--entrypoint
即可。可能光看文字有点迷糊,下面看个例子:
FROM alpine ENTRYPOINT ["echo"] CMD ["CMD"]
docker build -t entrypoint-test:v1 . #会打印出CMD中定义的输出“CMD” docker run --rm entrypoint-test:v1 $CMD #会打印出docker run中传入的“docker run”覆盖CMD中的定义 docker run --rm entrypoint-test:v1 docker run $docker run
第二种是
shell
模式的。在这种模式下,任何docker run
和CMD
的参数都无法被传入到ENTRYPOINT
里。所以官网推荐第一种用法。FROM alpine ENTRYPOINT echo CMD ["CMD"]
docker build -t entrypoint-test:v2 . #不会打印出CMD中定义的“CMD” docker run --rm entrypoint-test:v2 $ #不会打印出docker run中传入的“docker run” docker run --rm entrypoint-test:v2 docker run $
最后总结下一般该怎么使用:
一般还是会用ENTRYPOINT的中括号形式作为docker 容器启动以后的默认执行命令,里面放的是不变的部分,可变部分比如命令参数可以使用CMD的形式提供默认版本,也就是执行docker run里面没有任何参数时使用的默认参数。如果我们想用默认参数,就直接docker run,如果想用其他参数,就在docker run后面加想要的参数。
ENTRYPOINT ["python3", "manage.py", "runserver"] CMD ["0.0.0.0:8000"]
-
EXPOSE 暴露端口
格式为
EXPOSE <端口1> [<端口2>...]
。EXPOSE
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在Dockerfile
中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是docker run -P
时,会自动随机映射EXPOSE
的端口。要将
EXPOSE
和在运行时使用-p <宿主端口>:<容器端口>
区分开来。-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。 -
VOLUME 定义匿名卷
VOLUME
指令用于暴露任何数据库存储文件,配置文件,或容器创建的文件和目录。强烈建议使用 VOLUME来管理镜像中的可变部分和用户可以改变的部分。两种使用方法的格式为:
VOLUME ["<路径1>", "<路径2>"...] VOLUME <路径>
之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在
Dockerfile
中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。VOLUME /data
这里的
/data
目录就会在运行时自动挂载为匿名卷,任何向/data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。 -
ONBUILD
ONBUILD
指令可以为镜像添加触发器。其参数是任意一个Dockerfile
指令。当我们在一个
Dockerfile
文件中加上ONBUILD
指令,该指令对利用该Dockerfile
构建镜像(A镜像)不会产生实质性影响。但是当我们编写一个新的
Dockerfile
文件来基于A镜像构建一个镜像(比如为B镜像)时,这时构造A镜像的Dockerfile
文件中的ONBUILD
指令就生效了,在构建B镜像的过程中,首先会执行ONBUILD
指令指定的指令,然后才会执行其它指令。需要注意的是,如果是再利用B镜像构造新的镜像时,那个
ONBUILD
指令就无效了,也就是说只能再构建子镜像中执行,对孙子镜像构建无效。其实想想是合理的,因为在构建子镜像中已经执行了,如果孙子镜像构建还要执行,相当于重复执行,这就有问题了。利用
ONBUILD
指令,实际上就是相当于创建一个模板镜像,后续可以根据该模板镜像创建特定的子镜像,需要在子镜像构建过程中执行的一些通用操作就可以在模板镜像对应的Dockerfile
文件中用ONBUILD
指令指定。 从而减少Dockerfile
文件的重复内容编写。例如:
先编写个
onbuild-test:a
镜像:FROM alpine LABEL maintainer="cerberus43@gmail.com" ONBUILD RUN echo "onbuild" >> test.txt CMD ["cat", "test.txt"]
$docker build -t onbuild-test:a . $docker run --rm onbuild-test:a
再编写个
onbuild-test:b
镜像:FROM onbuild-test:a
$docker build -t onbuild-test:b . $docker run --rm onbuild-test:b
Dockerfile最佳实践:
官方原文:Dockerfile最佳实践
-
容器应该是短暂的
通过
Dockerfile
构建的镜像所启动的容器应该尽可能短暂(生命周期短)。「短暂」意味着可以停止和销毁容器,并且创建一个新容器并部署好所需的设置和配置工作量应该是极小的。我们可以查看下12 Factor(12要素)应用程序方法的进程部分,可以让我们理解这种无状态方式运行容器的动机。 -
理解上下文context
如果注意,会看到
docker build
命令最后有一个.
。.
表示当前目录,而Dockerfile
就在当前目录,因此不少人以为这个路径是在指定Dockerfile
所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定上下文路径context
。那么什么是上下文呢?首先我们要理解
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
引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。如果在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
输出,我们其实已经看到了这个发送上下文的过程:$ docker build -t nginx:v3 . Sending build context to Docker daemon 2.048 kB ...
理解构建上下文对于镜像构建是很重要的。
context
过大会造成docker build
很耗时,镜像过大则会造成docker pull/push
性能变差以及运行时容器体积过大浪费空间资源。一般来说,应该会将
Dockerfile
置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给Docker
引擎,那么可以用.gitignore
一样的语法写一个.dockerignore
,该文件是用于剔除不需要作为上下文传递给Docker
引擎的。那么为什么会有人误以为
.
是指定Dockerfile
所在目录呢?这是因为在默认情况下,如果不额外指定Dockerfile
的话,会将上下文目录下的名为Dockerfile
的文件作为Dockerfile
。这只是默认行为,实际上
Dockerfile
的文件名并不要求必须为Dockerfile
,而且并不要求必须位于上下文目录中,比如可以用-f ../Dockerfile.php
参数指定某个文件作为Dockerfile
。 -
使用
.dockerignore
文件使用
Dockerfile
构建镜像时最好是将Dockerfile
放置在一个新建的空目录下。然后将构建镜像所需要的文件添加到该目录中。为了提高构建镜像的效率,你可以在目录下新建一个.dockerignore
文件来指定要忽略的文件和目录。.dockerignore
文件的排除模式语法和Git
的.gitignore
文件相似。 -
使用多段构建
多阶段构建从
Docker 17.05
及更高版本的守护进程与客户端的新功能, 对于那些努力优化Dockerfile
同时保持可阅读性和可维护性的人来说,多阶段构建是非常有用的。一个
Dockerfile
用于开发环境,其中包含构建应用程序所需的一切, 另一个精简版的Dockerfile
,只包含你的应用程序及运行所需的内容,用于生产环境, 这种情况实际上非常普遍,这被称为”构建器模式”。维护两个Dockerfile
并不理想。下面是一个
Dockerfile.build
与Dockerfile
的示例,采用上面的构建器模式:Dockerfile.build
FROM golang:1.7.3 WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN go get -d -v golang.org/x/net/html \ && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
Dockerfile
FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY app . CMD ["./app"]
build.sh
#!/bin/sh echo Building alexellis2/href-counter:build docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \ -t alexellis2/href-counter:build . -f Dockerfile.build docker create --name extract alexellis2/href-counter:build docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app docker rm -f extract echo Building alexellis2/href-counter:latest docker build --no-cache -t alexellis2/href-counter:latest . rm ./app
运行
build.sh
时,你需要先构建第一个镜像,创建一个容器以便将结果复制出来,然后构建第二个镜像。 两个镜像都会占用你的系统空间,并且在你的本地磁盘上依然有应用程序。在多阶段构建下,你可以在
Dockerfile
中使用多个FROM
声明,每个FROM
声明可以使用不同的基础镜像, 并且每个FROM
都使用一个新的构建阶段。你可以选择性的将文件从一个阶段复制到另一个阶段, 删除你不想保留在最终镜像中的一切。我们来调整上面的Dockerfile
以使用多阶段构建做个示例。FROM golang:1.7.3 WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=0 /go/src/github.com/alexellis/href-counter/app . CMD ["./app"]
你只需要一个Dockerfile文件即可,也不需要单独的构建脚本,只需要运行
docker build
。docker build -t alexellis2/href-counter:latest .
最终的结果是与前面一样的极小的结果,但是复杂性大大降低,你不需要创建任何中间镜像, 也根本不需要将任何文件提取到本地系统。
它是如何工作的?第二个
FROM
指令使用alpine:latest
镜像作为基础开始一个新的构建阶段,COPY --from=0
的行将前一个阶段的结果复制到新的阶段,GO SDK
及所有中间产物被抛弃,并没有保存在最终镜像中。默认情况下,构建阶段没有命名,使用它们的整数编号引用它们,从第一个
FORM
以0
开始计数。 但是你可以使用给FORM
指令添加一个as <NAME>
为其构建阶段命名。FROM golang:1.7.3 as builder WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /go/src/github.com/alexellis/href-counter/app . CMD ["./app"]
-
避免安装不需要的包
为了降低复杂性、减少依赖、减小文件大小和构建时间,应该避免安装额外的或者不必要的软件包。例如,不要在数据库镜像中包含一个文本编辑器。
-
一个容器只做一件事
应该保证在一个容器中只运行一个进程。将多个应用解耦到不同容器中,保证了容器的横向扩展和复用。例如一个
web
应用程序可能包含三个独立的容器:web
应用、数据库、缓存,每个容器都是独立的镜像,分开运行。但这并不是说一个容器就只跑一个进程,因为有的程序可能会自行产生其他进程,比如Celery
就可以有很多个工作进程。虽然“每个容器跑一个进程”是一条很好的法则,但这并不是一条硬性的规定。我们主要是希望一个容器只关注意见事情,尽量保持干净和模块化。如果容器互相依赖,你可以使用Docker 容器网络来把这些容器连接起来,我们前面已经跟大家讲解过
Docker
的容器网络模式了。 -
最小化镜像层数
在
Docker 17.05
甚至更早1.10
之 前,尽量减少镜像层数是非常重要的,不过现在的版本已经有了一定的改善了:- 在
1.10
以后,只有RUN、COPY和ADD指令会创建层,其他指令会创建临时的中间镜像,但是不会直接增加构建的镜像大小了。 - 到了
17.05
版本以后增加了多阶段构建的支持,允许我们把需要的数据直接复制到最终的镜像中,这就允许我们在中间阶段包含一些工具或者调试信息了,而且不会增加最终的镜像大小。
当然减少
RUN
、COPY
、ADD
的指令仍然是很有必要的,但是我们也需要在Dockerfile
可读性(也包括长期的可维护性)和减少层数之间做一个平衡。 - 在
-
对多行参数排序
只要有可能,就将多行参数按字母顺序排序(比如要安装多个包时)。这可以帮助你避免重复包含同一个包,更新包列表时也更容易,也更容易阅读和审查。建议在反斜杠符号
\
之前添加一个空格,可以增加可读性。 下面是来自buildpack-deps
镜像的例子:RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion
-
构建缓存
在镜像的构建过程中
docker
会遍历Dockerfile
文件中的所有指令,顺序执行。对于每一条指令,docker
都会在缓存中查找是否已存在可重用的镜像,否则会创建一个新的镜像我们可以使用
docker build --no-cache
跳过缓存ADD
和COPY
将会计算文件的checksum
是否改变来决定是否利用缓存RUN
仅仅查看命令字符串是否命中缓存,如RUN apt-get -y update
可能会有问题
如一个
node
应用,可以先拷贝package.json
进行依赖安装,然后再添加整个目录,可以做到充分利用缓存的目的。FROM node:10-alpine as builder WORKDIR /code ADD package.json /code # 此步将可以充分利用 node_modules 的缓存 RUN npm install --production ADD . /code RUN npm run build