一、docker镜像构建简介
    在构建容器化应用时,相当重要的步骤莫过于镜像制作,本文将介绍镜像制作方法以及镜像制作的建议。通常镜像的制作有两种方式:
使用现有的容器使用docker commit 生成镜像使用Dockerfile进行镜像构建
''''采用docker commit 生成的镜像实际上是容器内的文件系统进行修改在进行提交,而运行的容器实际上是在镜像的文件系统顶层添加了一层读写层,所都的修改都是基于这一层,当生成镜像时会将这一层数据保存,所以每次使用commit提交镜像时候都会比原来多一层,这样会使得镜像越来越大并且不易维护。同时,对于镜像使用者来说完全不透明,使用者不清楚该镜像怎么样构建的,是否安全等,这种方式及其不推荐。
''''而使用Dockerfile构建镜像,对于使用者来说完全透明,构建镜像的每一个步骤都在Dockerfile文件中描述的清清楚楚,同时当需要对镜像修改时候,只需修改Dockerfile文件中的指令,维护镜像只需要维护一个Dockerfile,这也是镜像构建的最佳方式。当然,要使用Dockerfile就必须明白Dockerfile的语法和各个指令,以下将作详细介绍。
''''Docker以从上到下的顺序运行Dockerfile的指令。为了指定基本映像,第一条指令必须是FROM。一个声明以#字符开头则被视为注释。可以在Docker文件中使用RUN,CMD,FROM,EXPOSE,ENV等指令。
二、Dockerfile介绍
''''Dockerfile实际上就是一个文本文件,只不过这里的文件内容被Docker Deamon识别从而进行镜像构建。
''''在构建docker镜像的时候,实际上将当前目录移动到了一个虚拟目录当中,所有的操作路径都是以虚拟路径为准。
使用Dockerfile步骤:
  1.编写Dockerfile文件,用于描述镜像生成的步骤
  2.使用docker build -t name:tag 命令构建镜像
格式:
docker build [参数] [dockerfile的路径]
docker build参数:
-c : 指定使用CPU大小
-f : 指定dockerfile路径
-t : 指定构建后的镜像名称
2.1,语法规则
- #号代表注解。
- Dockerfile每一行都是以某个指令(约定大写字母)开始,后面可加参数构成完整指令,用于描述镜像构建步骤。
- 指令从上倒下依次执行
- Dockerfile的第一个指令一定是FROM指令,用于指定基础镜像
- Dockerfile还可以使用.dockerignore文件来忽略在制作镜像时候需要忽略的文件或者目录,列如使用COPY指令时候忽略某些文件或者目录。
- 所有指令参数为数组时,最好使用双引号
2.2,环境变量引用
1.若要在Dockerfile中引环境变量则使用$variable_name或${variable_name}
2.当变量为空或者变量值未设置可以使用${variable_name:-value}来指定变量的默认值
2.3,docker build命令
docker build 命令用于基于Dockerfile构建镜像,使用语法:
docker build [OPTIONS] PATH | URL | -
其中PATH代表含有Dockfile的目录,当然也可以是URL中含有Dockerfile
常用选项:
- -t, --tag list        # 指定生成镜像标签,格式为name:tag
- -f, --file string     # 单独指定Dockerfile文件位置
- --build-arg list      # 设置构建时的变量
- --no-cache            # 构建镜像时候不使用缓存
2.4,快速开始
构建一个简单的nginx镜像:
- # 1.创建一个目录用于存放DockerFile
- [root@docker ~]# mkdir docker_project
- [root@docker ~]# cd docker_project/
- [root@docker ~/docker_project]# 
- # 2.编辑Dockerfile文件,如果文件名称不是Dockerfile需要用-f指定名称
- [root@docker ~/docker_project]# vim Dockerfile
- # 指定基础镜像为centos:7.9.2009
- FROM centos:7.9.2009
- # 3.构建镜像
- [root@docker ~/docker_project]# docker build -t centos7:v1 .
- Sending build context to Docker daemon  2.048kB
- Step 1/1 : FROM centos:7.9.2009
-  ---> 8652b9f0cb4c
- Successfully built 8652b9f0cb4c
- Successfully tagged centos7:v1
- # 4.利用制作的镜像启动容器,并查看是否运行成功
- [root@docker ~/docker_project]# docker run -d -it --name my_centos71 centos7:v1
- dd95eab722bf53fbcafbfad420b87eb5acd7cbdccfdff320a6709faca3e2bd37
- [root@docker ~/docker_project]# docker ps
- CONTAINER ID   IMAGE        COMMAND       CREATED         STATUS         PORTS     NAMES
- dd95eab722bf   centos7:v1   "/bin/bash"   3 seconds ago   Up 2 seconds             my_centos71
三,Dockerfile指令详解
3.1 FROM指令(必须)
''''FROM指令是最重要且必须为Dockerfile中的第一个非注视指令,用于为构建的镜像指定基础镜像。后续指令运行环境基于该基础镜像,构建镜像时候默认会先从主机上寻找镜像,若不存在时则从Docker HUB上拉取镜像。指定构建镜像的基础镜像(有且只能有一个基础镜像)
- 语法 :
- FROM <repository> 
- FROM <repository>[:<tag>] 
- FROM <repository>@<digest>
- FROM [基础镜像]:[镜像版本号]
- 解释:
- repository:      # 镜像仓库
- tag:             # 镜像标签,省略就是latest
- digest:          # 镜像哈希码
3.2 LABEL指令
''''LABEL用于为镜像提供元数据信息,其数据格式为key=value。
- 语法 :
- LABEL <key>=<value> <key>=<value> <key>=<value> ...
- 示例:
-   LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
''''使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
3.3 MAINTAINER (弃用)指令
''''用于提供镜像提供者的信息,可以在Docker任何位置。该语法可能废弃,推荐使用LABEL
- 语法:
- MAINTAINER <message> 
- 解释:
- message:可以是任意文本信息
- 示例:
- MAINTAINER "wd <xxx@163.com>"
3.4 COPY指令
''''用于主机中的文件或者复制到镜像中
- 语法:
- COPY [--chown=<user>:<group>] <src>... <dest>
- COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
- 解释:
- src:  # 源文件或者目录,支持通配符。如果src是目录,src目录自己不会被复制,复制的是目录中的文件
- dest: # 容器中文件系统目录,如果目录不存在自动创建创建。
- user: # 复制到容器中的文件所属用户
- group:# 复制到容器中的文件所属用户组
注意事项:
- 如果复制的src或dest中存在空格字符需使用第二种加双引号方式
- src必须是 build的上下文目录(Dockerfile同级目录或子目录),不能是父目录或者绝对路径
- 如果指定来多个src或者src中使用了通配符,则dest必须是一个目录,且必须以/结尾
3.5 ADD指令
''''ADD指令类似于COPY,但是ADD比COPY更强大,支持TAR文件和URL路径
- ADD [--chown=<user>:<group>] <src>... <dest>
- ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
- 解释:
- src:  # 源文件或者目录,支持通配符。如果src是目录,src目录自己不会被复制,复制的是目录中的文件
- dest: # 容器中文件系统目录,如果目录不存在自动创建创建。
- user: # 复制到容器中的文件所属用户
- group:# 复制到容器中的文件所属用户组
- 示例:
- ADD hom* /mydir/        
- ADD hom?.txt /mydir/  
注意事项:
- 当src是URL时,如果dest不以/结尾,则src指定的文件将被下载并且被创建为dest,如果dest以/结尾,则src指定下载的文件会保存在dest目录下。
- 当src是一个本地目录的一个tar压缩格式文件,其在容器中会被展开为目录,类型与tar -x命令,通过URL下载的tar文件则不会被解压。
- 如果指定来多个src或者src中使用了通配符,则dest必须是一个目录,且必须以/结尾,多个文件一同被复制在dest目录下
3.6 WORKDIR指令
''''用于为Dockerfile中的各个指定设置工作目录,可以使用多次,当使用相对路径时目录是基于前一个WORKDIR指令
- 语法 :
- WORKDIR dirpath
- 示例:
- WORKDIR /usr/local
3.7 ENV指令
''''用于为镜像定义所需的环境变量,并可被Dockfile中位于其以后的指令所调用,如ADD、COPY、RUN等调用格式为$variable_name或者${variable_name},此外在启动容器时候这些变量也是存在的。
- 语法:
- ENV <key> <value>
- ENV <key>=<value> ...
- 示例:
- ENV myName="John Doe”     myDog=Rex     myCat=fluffy
- ENV myCat fluffy
注意事项:
- 第一种格式中key之后的所有值会被作为value,因此一次只能设置一个变量
- 第二种格式可一次性设置多个变量,每个变量为一个key=value的键值对,如果value种包含空格,可以用反斜线(\)转义,也可以通过对value加引号进行标识,此外反斜线也可用于续行,多个变量时候建议使用。
3.8 RUN指令
''''用于在build过程中运行的程序,可以是任何指令,可以指定多个RUN,RUN指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定--no-cache参数,如:docker build --no-cache
bash -c选项说明
首先有个atest shell脚本,里面的内容为
- echo $0
- echo $1
- echo $2
- # 执行bash -c “./atest hello world”他的输出如下:
- ./atest
- hello
- world
注意事项:
- -c 第一个字符串一定要是命令路径,不能是文件名,如果把./atest前面的./去掉,那么就会报找不到命令
- 命令文件必须要有可执行权限,即./atest 的必须就有x属性
- 两种语法:
- # shell 格式默认linux采用/bin/sh -c,windows采用cmd /S /C
- RUN <command>  [linux命令]
- # exec可执行程序格式 
- RUN ["executable", "param1", "param2”]
- 示例:
- RUN yum install -y nginx
- RUN ["/bin/bash", "-c", "echo hello"]
3.9 EXPOSE指令
''''用于为容器暴露端口到外部,用于实现通讯,类似于docker run的-p选项
- 语法:
- POSE <port> [<port>/<protocol>...]
- 解释:
- port:      # 端口
- protocol:  # 协议,可以是udp或tcp,默认tcp
- 示例: 
- EXPOSE 8080
- EXPOSE 8080/udp 8088/tcp
3.10 CMD 指令
''''用于为在镜像启动时为容器候提供的默认命令,该指令可以有多个,但是只有最后一个生效。
- 语法 :
- # shell格式,含有shell环境
- CMD command param1 param2  
- # 可执行程序格式
- CMD ["executable","param1","param2”]  
- # 第三种用于为ENTRYPOINT提供默认参数
- CMD ["param1","param2”] 
注意:
- 在第一种格式中command 通常是一个shell命令,且默认以/bin/sh -c来运行它,这意味着此进程在容器的的PID不为1,不能接受unix信号,因此使用docker stop 命令停止容器时,此进程接受不到SIGTERM信号。
- 第二种格式是可执行程序运行方式,不会以"/bin/sh -c”来发起,无shell环境,所有 shell变量不能引用,但是可以用"/bin/bash -c”作为启动命令达到第一种格式效果
- 第三种格式需要结合ENTRYPOINT使用,作用是为其提供默认参数
3.11 ENTRYPOINT 指令
''''类似于CMD功能,用于为启动容器指定默认启动命令,与CMD不同的是ENTRYPOINT命令不会随着docker run 后使用的命令覆盖而会把命令作为参数,除非docker run 参数中指定了—entrypoint
- 语法 :
- ENTRYPOINT <command>
- ENTRYPOINT ["<executable>", "<param1>", "<param2>"]
注意事项:
- 与CMD类似,第一种方式默认会以/bin/sh -c 启动,而第二种则不会,也就意味着没有shell环境
- 通常ENTRPOINT用于使用ENTRPOINT脚本启动
- 当CMD与ENTRYPOINT同时存在时,CMD的参数为ENTRYPOINT提供
示例:
- ["nginx","-g","daemon off"]
3.12 USER 指令
''''用于指定构建镜像时RUN、CMD、ENTRYPOINT等指令使用的用户或UID,默认情况容器运行身份为ROOT
- 语法 :
- USER <user>[:<group>] 
- USER <UID>[:<GID>]
- 示例: 
- USER nginx
注意事项:
- 指定的USER或者GROUP必须在容器中存在,否则指令会运行失败
3.13 SHELL 指令
''''将可执行程序运行为shell环境,默认以/bin/sh -c运行
- 语法:
- SHELL ["executable", "parameters"]
- 示例:
- # 等价于 RUN echo hello
- SHELL ["echo", “hello"] 
3.14 ARG 指令
''''该指令用于在build过程中提供参数,而在命令行使用--build-arg =来传递参数值,这样可以使用参数进行构建镜像。
- # 语法:
- ARG <name>[=<default value>]
- # 示例Dockerfile:
- FROM nginx
- ARG CONF="/tmp/nginx.conf"
- LABEL Author=xm
- RUN  touch "${CONF}"
- # 构建镜像:
- [root@docker ~]# docker build  --build-arg CONF='/etc/test.conf' -t nginx:v15.2 ./     
- Sending build context to Docker daemon  225.6MB
- Step 1/4 : FROM nginx
-  ---> f09fe80eb0e7
- Step 2/4 : ARG CONF="/tmp/nginx.conf"
-  ---> Using cache
-  ---> ac081589c644
- Step 3/4 : LABEL Author=xm
-  ---> Using cache
-  ---> 53b9b0ba4460
- Step 4/4 : RUN  touch "${CONF}"
-  ---> Running in 50debe96f876
- Removing intermediate container 50debe96f876
-  ---> d8680a2433bc
- Successfully built d8680a2433bc
- Successfully tagged nginx:v15.2
- # 运行容器查看:
- [root@docker ~]# docker run --rm nginx:v15.2 ls /etc/test.conf -l
- -rw-r--r-- 1 root root 0 Feb 27 11:18 /etc/test.conf
3.15 ONBULD
''''用于在Dockerfile中定义一个触发器,当制作出来的镜像被别人用于基础镜像时候自动触发。
- 语法:
- ONBUILD [INSTRUCTION]
- 解释:
- INSTRUCTION:      # 指令可以是RUN 、COPY等
注意事项:
- ONBUILD不会触发FROM指令。
- 在镜像标签中应明确指出onbuild关键字,以标记使用其基础镜像会触发其他指令
3.16 VOLUME
''''用于在image中创建一个挂载目录,以挂载宿主机上的目录
- 语法:
- VOLUME <path>
- VOLUME ["path"]
- 解释:
- path:代表容器中的目录,与docker run 不同,
- Dockerfile中不能指定宿主机目录,默认使用docker管理的挂载点
- 示例:
- VOLUME ["/var/log/“]
- VOLUME /myvol
四、使用multi-stage
''''在构建镜像过程中,我们可能只需要某些镜像的产物,比如在运行一个go程序需要先go程序包编译后才运行,如果在一个镜像里面完成,先要经过安装编译环境,程序编译完再安装运行环境,最后运行程序,这样的镜像体积往往比较大,不利于我们使用。而真正我们需要的镜像是只有程序包和运行环境,编译环境的构建在运行容器时候是不需要的,所以Docker提供了一种解决方案就是multi-stage(多阶段构建)。
''''Docker允许多个镜像的构建可以使用同一个Dockerfile,每个镜像构建过程可以称之为一个stage,简单理解就是一个FROM指令到下一个FROM指令,而每个stage可使用上一个stage过程的产物或环境(其实还支持其他镜像的),这样一来,最终所得镜像体积相对较小。不仅如此多阶段构建同样可以很方便地将多个彼此依赖的项目通过一个Dockerfile就可轻松构建出期望的容器镜像,而不用担心镜像太大、项目环境依赖等问题。
''''通过上述介绍,我们可以在第一个stage将go程序编译得到编译后程序包,然后在第二个stage中直接拷贝编译好的go程序包到运行环境中,最后的镜像中就只有程序包和运行环境。以下作为示例:
- 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中存在两个FROM指令,也就是两个stage,第一个stage用于构建产物,而在第二个stage中使用COPY --from=0 意思将第一个stage中的/go/src/github.com/alexellis/href-counter/app拷贝到.目录,第二个stage仅仅相当于执行copy就有了构建产物,不用在安装编译环境,镜像会很缩小。
4.1 命名stage
''''默认情况下,stage未命名,可以通过整数来引用它们,第一个stage表示0,第二个表1以此类推。 但是,当有多个stage时候,这样会显得麻烦,Docker提供AS 语法可以为stage命名:
- FROM golang:1.7.3 as builder
然后在另一个stage中使用:
- COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
4.2 使用本地stage
''''除了可以使用Dockerfile中的stage外,构建镜像时候还可以直接使用本地已存在的环境和产物,例如:
- COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
五,构建镜像建议
- 
基础镜像尽量选择比体积较小的镜像,如每个官方发行的alpine镜像。虽然这版本镜像比较小,但是与之带来的是利用该类镜像运行的容器中排错的命令很少; 
- 
使用RUN指令时候,尽量把多个RUN指令合并为一个,通常做法是使用&&符号; 
- 
通过multi-stage方法减少一些不必要使用的环境来减小镜像; 
- 
安装完成软件同时删除一些不需要的文件或目录;