在构建容器化应用时,相当重要的步骤莫过于镜像制作,本文将介绍镜像制作方法以及镜像制作的建议。通常镜像的制作有两种方式:
使用现有的容器使用docker commit
生成镜像
你需要先启动一个容器,然后基于该容器创建一个新的镜像,此时,容器内的物理数据都会被保存下来,存至新的镜像中,即使容器不使用任何 volume时,容器的数据仅被保存在容器之内,它只在容器的生命周期内存在,会随着容器的删除而被删除,通过docker commit也会保存下来。
使用Dockerfile
进行镜像构建
采用docker commit 生成的镜像实际上是容器内的文件系统进行修改在进行提交,而运行的容器实际上是在镜像的文件系统顶层添加了一层读写层,所都的修改都是基于这一层,当生成镜像时会将这一层数据保存,所以每次使用commit提交镜像时候都会比原来多一层,这样会使得镜像越来越大并且不易维护。同时,对于镜像使用者来说完全不透明,使用者不清楚该镜像怎么样构建的,是否安全等,这种方式及其不推荐。
而使用Dockerfile构建镜像,对于使用者来说完全透明,构建镜像的每一个步骤都在Dockerfile文件中描述的清清楚楚,同时当需要对镜像修改时候,只需修改Dockerfile文件中的指令,维护镜像只需要维护一个Dockerfile,这也是镜像构建的最佳方式。当然,要使用Dockerfile就必须明白Dockerfile的语法和各个指令,以下将作详细介绍。
Dockerfile实际上就是一个文本文件,只不过这里的文件内容被Docker Deamon识别从而进行镜像构建。
使用Dockerfile步骤:
1.编写Dockerfile文件,用于描述镜像生成的步骤
2.使用docker build -t name:tag
命令构建镜像
1.#号代表注解。
2.Dockerfile每一行都是以某个指令(约定大写字母)开始,后面可加参数构成完整指令,用于描述镜像构建步骤。
3.指令从上倒下依次执行
4.Dockerfile的第一个指令一定是FROM指令,用于指定基础镜像
5.Dockerfile还可以使用.dockerignore文件来忽略在制作镜像时候需要忽略的文件或者目录,列如使用COPY指令时候忽略某些文件或者目录。
6.所有指令参数为数组时,最好使用双引号
1.若要在Dockerfile中引环境变量则使用 v a r i a b l e n a m e 或 variable_name或 variablename或{variable_name}
2.当变量为空或者变量值未设置可以使用${variable_name:-value}来指定变量的默认值
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 构建镜像时候不使用缓存
构建一个简单的nginx镜像:
1.创建一个目录用于存放DockerFile
mkdir /opt/demo -p
cd /opt/demo/
2.编辑Dockerfile文件,如果文件名称不是Dockerfile需要用-f指定名称。
FROM centos:latest #指定基础镜像为centos
LABEL Author=“wd” #指明作者
RUN yum install -y yum epel-release && yum install -y nginx && echo "${HOSTNAME}-nginx server" > /usr/share/nginx/html/index.html #运行命令安装Nginx
CMD [ "/usr/sbin/nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf”] #启动容器运行的命令
3.构建镜像
[root@app51 demo]# docker build -t nginx:v1 ./
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM centos:latest
---> 1e1148e4cc2c
Step 2/4 : LABEL Author="wd"
---> Using cache
---> 8eb3ffcb8ba3
Step 3/4 : RUN yum install -y yum epel-release && yum install -y nginx && echo "${HOSTNAME}-nginx server" > /usr/share/nginx/html/index.html
---> Using cache
---> ac91999a716e
Step 4/4 : CMD [ "/usr/sbin/nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf"]
---> Running in 323afd4ac89d
Removing intermediate container 323afd4ac89d
---> 6403c553fd04
Successfully built 6403c553fd04
Successfully tagged nginx:v1
docker build -t nginx:v1 ./ 命令默认读取Dockerfile这个文件,如果其是名字,则用-f
docker build -t nginx:v1 -f Dockerfile
4.利用制作的镜像启动容器,并查看是否运行成功.
[root@app51 demo]# docker run -d --name nginx-demo-c1 -p 8088:80 nginx:v1
08812b7def62c9ad7879dfa4182bc28a20f524e2dbc5eb6e4fe63d2b67be3cc9
[root@app51 demo]# curl http://127.0.0.1:8088
60e5de135132-nginx server #访问成功
[root@app51 demo]#
以上的Dockerfile中的每一行是一个指令,用于描述镜像生成的步骤,以下将介绍这些指令用法。
FROM指令是最重要且必须为Dockerfile中的第一个非注释指令,用于为构建的镜像指定基础镜像。后续指令运行环境基于该基础镜像,构建镜像时候默认会先从主机上寻找镜像,若不存在时则从Docker HUB上拉取镜像。
语法 :
FROM <repository>
FROM <repository>[:<tag>]
FROM <repository>@<digest>
解释:
repository:镜像仓库
tag:镜像标签,省略就是latest
digest:镜像哈希码
示例:
FROM centos:latest
LABEL用于为镜像提供元数据信息,其数据格式为key=value。
语法 :
LABEL <key>=<value> <key>=<value> <key>=<value> ...
示例:
LABEL "com.example.vendor"="ACME Incorporated”
LABEL maintainer="[email protected]"
用于提供镜像提供者的信息,可以在Docker任何位置。该语法可能废弃,推荐使用LABEL
语法:
MAINTAINER <message>
解释:
message:可以是任意文本信息
用于主机中的文件或者复制到镜像中
语法:
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["" ,... "" ]
解释:
src:源文件或者目录,支持通配符。如果src是目录,src目录自己不会被复制,复制的是目录中的文件
dest:容器中文件系统目录,如果目录不存在自动创建创建。
user:复制到容器中的文件所属用户
group:复制到容器中的文件所属用户组
注意事项:
如果复制的src或dest中存在空格字符需使用第二种加双引号方式
src必须是 build的上下文目录(Dockerfile同级目录或子目录),不能是父目录或者绝对路径
如果指定来多个src或者src中使用了通配符,则dest必须是一个目录,且必须以/结尾
示例:
COPY hom* /mydir/ #拷贝以hom开头的的所有文件
COPY hom?.txt /mydir/ #?代表占位符,可以拷贝
ADD
ADD指令类似于COPY,但是ADD比COPY更强大,支持TAR文件和URL路径
语法:
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["" ,... "" ]
解释
src:源文件或者目录,支持通配符。如果src是目录,src目录自己不会被复制,复制的是目录中的文件
dest:容器中文件系统目录,如果目录不存在自动创建创建。
user:复制到容器中的文件所属用户
group:复制到容器中的文件所属用户组
注意事项:
当src是URL时,如果dest不以/结尾,则src指定的文件将被下载并且被创建为dest,如果dest以/结尾,则src指定下载的文件会保存在dest目录下。
当src是一个本地目录的一个tar压缩格式文件,其在容器中会被展开为目录,类型与tar -x命令,通过URL下载的tar文件则不会被解压。
如果指定来多个src或者src中使用了通配符,则dest必须是一个目录,且必须以/结尾,多个文件一同被复制在dest目录下
示例:
ADD hom* /mydir/
ADD hom?.txt /mydir/
用于为Dockerfile中的各个指定设置工作目录,可以使用多次,当使用相对路径时目录是基于前一个WORKDIR指令。
语法 :
WORKDIR dirpath
示例:
WORKDIR /usr/local
用于为镜像定义所需的环境变量,并可被Dockfile中位于其以后的指令所调用,如ADD、COPY、RUN等调用格式为 v a r i a b l e n a m e 或者 variable_name或者 variablename或者{variable_name},此外在启动容器时候这些变量也是存在的。
语法:
ENV <key> <value>
ENV <key>=<value> ...
注意:
第一种格式中key之后的所有值会被作为value,因此一次只能设置一个变量
第二种格式可一次性设置多个变量,每个变量为一个key=value的键值对,如果value种包含空格,可以用反斜线(\)转义,也可以通过对value加引号进行标识,此外反斜线也可用于续行,多个变量时候建议使用。
示例:
ENV myName="John Doe” \
myDog=Rex \
myCat=fluffy
ENV myCat fluffy
用于在build过程中运行的程序,可以是任何指令,可以指定多个RUN
注意RUN和后面的cmd区别,run是指构建过程中的执行的命令,而cmd是指镜像做好后,起容器后干的第一件事
语法:
RUN <command> #shell 格式默认linux采用/bin/sh -c,windows采用cmd /S /C
RUN ["executable", "param1", "param2”] #可执行程序格式
示例:
RUN yum install -y nginx
RUN ["/bin/bash", "-c", "echo hello"]
用于为容器暴露端口到外部,用于实现通讯,类似于docker run的-p选项
语法:
EXPOSE <port> [<port>/<protocol>...]
解释:
port:端口
protocol:协议,可以是udp或tcp,默认tcp
示例:
EXPOSE 8080
EXPOSE 8080/udp 8088/tcp
用于在image中创建一个挂载目录,以挂载宿主机上的目录
语法:
VOLUME <path>
VOLUME ["path"]
解释:
path:代表容器中的目录,与docker run 不同,Dockerfile中不能指定宿主机目录,默认使用docker管理的挂载点
示例:
VOLUME ["/var/log/“]
VOLUME /myvol
用于为在镜像启动为容时候提供的默认命令,该指定可以有多个,但是只有最后一个生效。
语法 :
CMD command param1 param2 'shell格式,含有shell环境,简称shell格式'
CMD ["executable","param1","param2”] '可执行程序格式,简称exec格式'
CMD ["param1","param2”] #第三种用于为ENTRYPOINT提供默认参数
注意:
在第一种格式中command 通常是一个shell命令,且默认以/bin/sh -c来运行它,这意味着此进程在容器的的PID不为1,不能接受unix信号,因此使用docker stop
命令停止容器时,此进程接受不到SIGTERM信号。
第二种格式是可执行程序运行方式,不会以"/bin/sh -c”来发起,无shell环境,所有shell变量不能引用,但是可以用"/bin/bash -c”作为启动命令达到第一种格式效果
第三种格式需要结合ENTRYPOINT使用,作用是为其提供默认参数
详情参见Dockerfile 中的cmd、ENTRYPOINT命令
docker run 后使用的命令会覆盖此处的 cmd
类似于CMD功能,用于为启动容器指定默认启动命令,与CMD不同的是ENTRYPOINT命令不会随着docker run 后使用的命令覆盖而会把命令作为参数,除非docker run 参数中指定了—entrypoint
语法 :
ENTRYPOINT <command>
ENTRYPOINT ["" , "" , "" ]
注意事项:
示例:
[“nginx”,"-g","daemon off"]
用于指定构建镜像时RUN、CMD、ENTRYPOINT等指令使用的用户或UID,默认情况容器运行身份为ROOT
语法 :
USER <user>[:<group>]
USER <UID>[:<GID>]
注意事项:
指定的USER或者GROUP必须在容器中存在,否则指令会运行失败
示例:
USER nginx
该指令用于设置容器停止时向容器内进程发送的信号,列如 9 、SIGKILL、SIGTERM。
语法:
STOPSIGNAL signal
示例:
STOPSIGNAL SIGKILL
注意事项:
向容器发送信号只能被PID=1的进程所接收,当PID=1进程不是应用进程时候,应用进程收不到终止信号。
该指令在1.12版本中添加,用于对容器中的应用进行健康检查,不做检查使用NONE。当对容器做了健康检查时候,检查值为0表示成功,非0表示不健康。
语法:
HEALTHCHECK [OPTIONS] CMD command
其中OPTIONS有如下选项:
–interval=DURATION 检查间隔(默认: 30s)
–timeout=DURATION 超时时间(默认t: 30s)
–start-period=DURATION 等待检查的时间,默认0s代表一启动就检查 (默认: 0s)
–retries=N (default: 3) 重试次数
示例:
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1
将可执行程序运行为shell环境,默认以/bin/sh -c运行
语法:
SHELL ["executable", "parameters"]
示例:
SHELL ["echo", “hello"] #等价于 RUN echo hello
该指令用于在build过程中提供参数,而在命令行使用--build-arg
来传递参数值,这样可以使用参数进行构建镜像。
语法:
ARG <name>[=<default value>]
示例:
Dockerfile
FROM nginx
ARG CONF="/tmp/nginx.conf" '定义一个默认值,如果外部传入同名参数,会覆盖之'
LABEL Author=wd
RUN touch "${CONF}" '引用了变量CONF'
构建镜像:
[root@app51 ~]# 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=wd
---> 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@app51 ~]# docker run --rm nginx:v15.2 ls /etc/test.conf -l '启动容器后执行ls命令'
-rw-r--r-- 1 root root 0 Feb 27 11:18 /etc/test.conf '发现果然入参的文件是存在的,说明构建的时候生效了'
用于在Dockerfile中定义一个触发器,当制作出来的镜像被别人用于基础镜像时候自动触发。
语法:
ONBUILD [INSTRUCTION]
解释:
示例:
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
在构建镜像过程中,我们可能只需要某些镜像的产物,比如在运行一个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就有了构建产物,不用在安装编译环境,镜像会很缩小。
默认情况下,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 .
除了可以使用Dockerfile中的stage外,构建镜像时候还可以直接使用本地已存在的环境和产物,例如:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
我们知道了根据dockerfile来制作镜像,如果给你一个现成的镜像,你能逆向查看出dockerfile吗?
详情参见《怎么查看docker镜像的dockerfile》
Docker镜像构建