6. Dockerfile详解
镜像的生成途径
About Dockerfile
Dockerfile 就是构建docker镜像的源码,Dockerfile 是纯文本文件。
基于Dockerfile制作docker镜像时,必须在特点的某个目录。Dockerfile 首字母必须大写。如果要打包文件,该文件必须放置在当前工作目录下。如果忽视某些文件可以定义 .dockerignore配置文件。 最后使用 docker build
来创建docker 镜像。
Dockerfile指令
FROM
- FROM 指令是最重要的一个且必须为Dockerfile文件开篇的第一个非注释行,用于为映像文件构建过程指定基准镜像,后续的指令运行于此基准镜像所提供的运行环境
- 实践中,基准镜像可以是任何可用镜像文件,默认情况下,docker build 会在 docker 主机上查找指定的镜像文件,在其不存在时,则会从 Docker Hub Registy 上拉取所需的镜像文件
- 如果找不到指定的镜像文件,docker build 会返回一个错误信息
- Syntax
- FROM
[: ] 或 - FROM
@ :指定作为 base image 的名称; :base image的标签,为可选项,省略时默认为 latest
- FROM
MAINTANIER(depreacted)
- 用于让Dockerfile 制作者提供本人的详细信息
- Dockerfile 并不限制 MAINTAINER 指令可在出现的位置,但推荐将其放置于FROM 指令之后。
- Syntax
- MAINTANIER <author's detail>
- <author's detail> 可是任何文本信息,但约定俗成的使用作者名称及邮件地址
- MAINTANIER "hukey hukey@126.com"
- MAINTANIER <author's detail>
LABEL
语法:
LABEL <key>=<value> <key>=<value> <key>=<value> <key>=<value>
COPY
- 用于从 Docker主机复制文件至创建的新映像文件
- Syntax
- COPY
... 或 - COPY ["
", .. " "] :要复制的源文件或目录,支持使用通配符 :目标路径,即正在创建的image的文件系统路径;建议为 使用绝对路径,否则,COPY 指定则以 WORKDIR 为其起始路径;
- 注意:在路径中有空白字符时,通常使用第二种格式
- COPY
- 文件复制准则
必须是build上下文中的路径,不能是其父目录中的文件 - 如果
是目录,则其内部文件或子目录会被递归复制,但 目录自身不会被复制 - 如果指定了多个
,或在 中使用了通配符,则 必须是一个目录,且必须以 / 结尾 - 如果
事先不存在,它将会被自动创建,这包括其父目录路径
# 一行命令查看容器内的信息
docker run --name tinyweb1 --rm tinyhttpd:v0.1-1 cat /data/web/html/index.html
ADD
-
ADD指令类似于COPY指令,ADD支持使用TAR文件和URL路径
-
Syntax
- ADD
... 或 - ADD ["
", ... " "]
- ADD
-
操作准则
- 同 COPY指令
- 如果
为URL且 不以 / 结尾,则 制定的文件将被下载并直接被创建为 ;如果 以 / 结尾,则文件名URL 制定的文件将被直接下载并保存为 / “tar -x” 命令;然而,通过URL 获取到的tar文件将不会自动展开; - 如果
有多个,或其间接或直接使用了通配符,则 必须是一个以 / 结尾的目录路径;如果 不以 / 结尾,则其被视作一个普通文件, 的内容将被直接写入到 ;
WORKDIR
- 用于为Dockerfile 中所有的 RUN 、CMD、ENTRYPOINT、COPY 和 ADD 定制设定工作目录
- Syntax
- WORKDIR
- 在Dockerfile文件中,WORKDIR 指令可出现多次,其路径也可以为相对路径,不过,其是相对此前一个WORKDIR 指令指定的路径
- 另外,WORKDIR 也可调用由ENV 指定定义的变量
- 例如
- WORKDIR /var/log 当设定工作目录时,会自动创建该目录。
- WORKDIR $STATEPATH
- WORKDIR
VOLUME
- 用于在image中创建一个挂载点目录,以挂载Docker host 上的卷或其他容器上的卷
- Syntax
- VOLUME
或 - VOLUME ["
"]
- VOLUME
- 如果挂载点目录路径下此前在文件存在,docker run 命令会在卷挂载完成后将此前的所有文件复制到新挂载的卷中。
EXPOSE
- 用于为容器打开指定要监听的端口以实现与外部通信
- Syntax
- EXPOSE
[/ ] [ [/ ] ...]
- EXPOSE
- EXPOSE 指令可一次指定多个端口,例如:
- EXPOSE 11211/udp 11211/tcp
注意:当需要开放多个端口时,建议在一行中定义,否则会多出来很多层级。
ENV
- 用于为镜像定义所需的环境变量,并可被Dockerfile 文件中位于其后的其他指令(如 ENV、ADD、COPY等)所调用
- 调用格式为
$variable_name
或${variable_name}
- Syntax
- ENV
或 - ENV
= ...
- ENV
- 第一种格式中,
之后的所有内容均会被视作其 的组成部分,因此,一次只能设置一个变量; - 第二种格式可用一次设置多个变量,每个变量为一个“
= ” 的键值对,如果 中包含空格,可以以反斜线( \ ) 进行转义,也可通过对 加引号进行标识;另外,反斜线也可用于续行; - 定义多个变量时,建议使用第二种方式,以便在同一层中完成所有功能
RUN
- 用于指定 docker build 过程中运行的程序,其可以是任何命令
- Syntax
- RUN
或 - RUN ["
", " "," "]
- RUN
- 第一种格式中,
通常是一个 shell 命令,且以 "/bin/sh -c" 来运行它,这意味着此进程在容器中的PID 不为 1,不能接收Unix信号,因此,当使用 docker stop 命令停止容器时,此进程接收不到 SIGTERM信号; - 第二种语法格式中的参数是一个JSON格式的数组,其中
为要运行的命令,后面的 为传递给命令的选项或参数;然而,此种格式指定的命令不会以 /bin/sh -c
来发起依赖于此shell特性的话,可以将其替换为类似下面的格式。RUN ["/bin/sh","-c","<executable>","param1"]
- 第二种语法格式中的参数是一个JSON格式的数组,其中
- 主意:json数组中,要使用双引号
CMD
- 类似与RUN指令,CMD指令也可用于运行任何命令或应用程序,不过,二者的运行时间点不同。
- RUN 指令运行于映像文件构建过程中,而CMD 指令运行于基于Dockerfile 构建出的新映像文件启动一个容器时,
- CMD指令的首要目的在于为启动的容器指定默认要运行的程序,且其运行结束后,容器也将终止;不过,CMD 指定的命令其可以被docker run 的命令行选项所覆盖
- 在 Dockerfile 中可以存在多个CMD指令,但仅最后一个会生效
- Syntax
- CMD
或 - CMD ["
", " ", " "]或 - CMD ["
", " "]
- CMD
- 前两种语法格式的意义同RUN
- 第三种则用于为 ENTRYPOINT 指令提供默认参数
RUN 和 CMD 区别:
RUN 是可以运行多次的,如果多个命令之间有关联关系,建议在一条RUN当中把多个命令写进来。
- CMD 在 docker build 阶段生成镜像;
- CMD 是定义一个镜像文件启动为容器时默认要运行的程序,而docker是用运行一个程序,所以CMD 一般只出现一次,CMD 可以出现多次,但是只有最后一个生效;
- RUN 可以出现多次,而且逐一运行;
- CMD 的用法在一些方面是不一样的,而且CMD 通常不会单独使用,而是结合 ENTRYPOINT 使用。
附加知识点:
用户启动并创建进程的接口就是 shell,这是用户能够与操作系统交互的接口,所以事实上打开命令行提示符,就相当于正在运行的一个shell进程,而后在命令行之下所创建的任何进程都应该属于shell的子进程,而且有些进程还可能直接占据当前shell 的终端设备,将进程送到后台去用 &
符号,加上 &
并不能剥离当前shell的关系,它的父进程依然是shell,如果退出了shell,这个进程依然会被关闭。要实现脱离shell,需要使用命令 nohup
这个命令就表示脱离shell,运行到后台。
在系统进程领域中,从来都是白发人送黑发人。当父进程退出时,一定会把它所属的子进程全部kill,而后在退出。如果父进程意外结束,那么它的子进程就成了孤儿进程,孤儿进程既不会释放内存,也不会终止了。
关键命令:exec
ENTRYPOINT
- 类似 CMD 指令的功能,用于为容器指定默认运行程序,从而使得容器像是一个单独的可执行程序
- 与CMD 不同的是,由 ENTRYPOINT 启动的程序不会被 docker run 命令行指定的参数所覆盖,而且,这些命令行参数会被当作参数传递给 ENTRYPOINT 指定的程序
- 不过,docker run 命令的 --entrypoint 选项的参数可覆盖 ENTRYPOINT 指令指定的程序
- Syntax
- ENTRYPOINT
- ENTRYPOINT ["
"," "," "]
- ENTRYPOINT
- docker run 命令传入的命令参数会覆盖CMD 指令的内容并且附加到 ENTRYPOINT 命令最后作为其参数使用
- Dockerfile 文件中也可以存在多个 ENTRYPOINT 指令,但仅有最后一个会生效
在创建容器时,可通过命令行中定义的命令来覆盖CMD的指令,但是有时候不允许覆盖,CMD 做不到,而 ENTRYPOINT 可以。
在Dockerfile中,如果使用的是 ENTRYPOINT 而不是CMD,定义的命令是不会被覆盖的。
[root@docker ~/img2]#cat Dockerfile
FROM busybox
LABEL maintainer="hukey<hukey@super.com>" app="httpd"
ENV WEB_DOC_ROOT="/data/web/html/"
RUN mkdir -p ${WEB_DOC_ROOT} && \
echo "<h1>busybox httpd server</h1>" > ${WEB_DOC_ROOT}/index.html
#CMD /bin/httpd -f -h ${WEB_DOC_ROOT}
#CMD ["/bin/sh","-c","/bin/httpd","-f","-h /data/web/html/"]
ENTRYPOINT /bin/httpd -f -h ${WEB_DOC_ROOT}
[root@docker ~/img2]#docker run --name web2 --rm -P tinyhttpd:v0.2-5 ls /data/web/html/
在Dockerfile 中可以定义多个 CMD ,只有最后一个生效。如果 ENTRYPOINT 定义了多个,只有最后一个生效。
如果 CMD 和 ENTRYPOINT 同时定义,CMD 的参数将当作参数传递给 ENTRYPOINT 。多数情况下,ENTRYPOINT是指定一个shell。
容器接收配置要靠环境变量。接下来通过一个示例来说明 CMD 和 ENTRYPOINT 连用。
假如运行一个 nginx,要为nginx 提供一个配置文件要如何实现,或让nginx灵活接受配置该如何实现?
[root@docker ~/img3]#cat Dockerfile
FROM nginx:1.14-alpine
LABEL maintainer="hukey<hukey@super.com>"
ENV NGX_DOC_ROOT='/data/web/html/'
ADD index.html ${NGX_DOC_ROOT}
ADD entrypoint.sh /bin/
CMD ["/usr/sbin/nginx","-g","daemon off;"]
ENTRYPOINT ["/bin/entrypoint.sh"]
------
[root@docker ~/img3]#cat entrypoint.sh
#!/bin/sh
# Author:hukey
cat > /etc/nginx/conf.d/www.conf << EOF
server {
server_name $HOSTNAME;
listen ${IP-0.0.0.0}:${PORT-80};
root ${NGX_DOC_ROOT-/usr/share/nginx/html};
}
EOF
exec "$@"
USER
- 用于指定运行image时的或运行Dockerfile中任何RUN 、CMD或ENTRYPOINT 指令指定的程序时的用户名或UID
- 默认情况下,container的运行身份为 root 用户
- Syntax
- USER
| - 需要注意的是,
可以为任意数字,但实践中其必须为 /etc/passwd 中某用户的有效UID,否则,docker run 命令将运行失败
- USER
HEALTHCHECK
当一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting
,在HEALTHCHECK
指令检查成功后变为 healthy
,如果连续一定次数失败,则会变为 unhealthy
。
HEALTHCHECK
支持下列选项:
--interval=<间隔>
:两次健康检查的间隔,默认为30秒--timeout=<时长>
:健康检查命令运行超时时间,如果超过这个时间,本次健康价差就被视为失败,默认30秒--retries=<次数>
:当连续失败指定次数后,则将容器状态视为unhealthy
,默认3次。--start-period=<时长>
:定义容器启动多少秒之后再进行检测,默认 0 秒
当使用 HEALTHCHECK
时,会返回三种状态:
0:success 状态检测成功
1:unhealthy 状态检测失败
2:reserved 预留,没意义
示例:
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1
和 CMD、ENTRYPOINT 一样,HEALTHCHECK 只能出现一次,如果写了多个,只有最后一个生效。
[root@docker ~/img3]#cat Dockerfile
FROM nginx:1.14-alpine
LABEL maintainer="hukey<hukey@super.com>"
ENV NGX_DOC_ROOT='/data/web/html/'
ADD index.html ${NGX_DOC_ROOT}
ADD entrypoint.sh /bin/
EXPOSE 80/tcp
HEALTHCHECK --start-period=3s CMD wget -O - -q http://${IP:-0.0.0.0}:${PORT:-80}/ || exit 1
CMD ["/usr/sbin/nginx","-g","daemon off;"]
ENTRYPOINT ["/bin/entrypoint.sh"]
---
[root@docker ~/img3]#cat entrypoint.sh
#!/bin/sh
# Author:hukey
cat > /etc/nginx/conf.d/www.conf << EOF
server {
server_name $HOSTNAME;
listen ${IP-0.0.0.0}:${PORT-80};
root ${NGX_DOC_ROOT-/usr/share/nginx/html};
}
EOF
exec "$@"
ONBUILD
- 用于在Dockerfile中定义一个触发器
- Dockerfile用于build映像文件,此映像文件亦可作为 base image 被另一个Dockerfile用作FROM 指令的参数,并以之构建新的映像文件
- 在后面的这个Dockerfile 中的FROM 指令在 build 过程中被执行时,将会 “触发”创建其base image 的Dockerfile 文件中的 ONBUILD 指令定义的触发器
- Syntax
- ONBUILD
- ONBUILD
- 尽管任何指令都可注册成为触发器指令,但ONBUILD 不能自我嵌套,且不会触发FROM 和MAINTAINER 指令
- 在ONBUILD 指令中使用ADD或COPY指令应该格外小心,因为新构建过程的上下文在缺少指定的源文件时会失败。
...
ONBUILD ADD http://192.168.118.45/nginx-1.18.0.tar.gz /usr/local/src/
...
构建Dockerfile 基本准则
容器应该是短暂的
通过 Dockerfile 构建的镜像所启动的容器应该尽可能短暂(ephemeral)。短暂意味着可以很快地启动并且终止。
使用 .dockerignore 排除构建无关文件
.dockerignore 语法与 .gitignore 语法一致。使用它排除构建无关的文件及目录
使用多阶段构建
多阶段构建可以有效减小镜像体积,特别是对于需编译语言而言,一个应用的构建过程往往如下:
- 安装编译工具
- 安装第三方库依赖
- 编译构建应用
而在前两步会有大量的镜像体积冗余,使用多阶段构建可以避免这一问题。
避免安装不必要的包
减小体积,减少构建时间。
一个容器只做一件事
如一个 Web 应用将会包含三个部分,Web 服务,数据库与缓存。把他们解耦到多个容器中,方便横向扩展。如果你需要网络通信,则可以将他们至于一个网络下。
减少镜像层数
- 只有RUN、COPY、ADD 会创建层数,其他指令不会增加镜像的体积
- 尽可能使用多阶段构建
例如:
# 正确写法
RUN yum install wget net-tools node -y
# 错误写法
RUN yum install -y wget
RUN yum install -y net-tools
RUN yum install -y node
将多行参数排序
便于可读性以及不小心的重复装包
RUN yum install -y wget \
net-tools \
node
充分利用构建缓存
在镜像的构建过程中 Docker 会遍历 Dockerfile 文件中的所有指令,顺序执行。对于每一条指令,Docker 都会在缓存中查找是否已存在可重用的镜像,否则会创建一个新的镜像。
我们可以使用 docker build --no-cache 跳过缓存:
- ADD 和 COPY 将会计算文件的 checksum 是否改变来决定是否利用缓存
- RUN 仅仅查看命令字符串是否命中缓存,如 RUN apt-get -y update 可能会有问题
Dockerfile 实例
以下示例来自网络:
构建 sshd 镜像
# Dockerfile
FROM centos:centos7
LABEL maintainer="hukey<hukey@super.com>"
RUN yum install -y openssh* net-tools iproute NetworkManager && \
# mkdir -p /var/run/sshd && \
echo "UseDNS no" >> /etc/ssh/sshd_config && \
sed -i '/pam_loginuid.so/d' /etc/pam.d/sshd && \
echo '123123' | passwd --stdin root && \
/usr/bin/ssh-keygen -A && \
yum clean all
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
# 制作镜像
docker build -t sshd:v0.1-1 ./
# 创建容器
docker run --name ssh_server -P --rm -d sshd:v0.1-1
构建systemctl 镜像
(基于上面ssh镜像构建)
# Dockerfile
[root@docker ~/img6]#cat Dockerfile
FROM sshd:v0.1-2
LABEL maintainer="hukey<hukey@super.com>"
RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*; \
rm -f /etc/systemd/system/*.wants/*; \
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*; \
rm -f /lib/systemd/system/anaconda.target.wants/*;
VOLUME ["/sys/fs/cgroup"]
CMD ["/usr/sbin/init"]
# 制作镜像
[root@docker ~/img6]#docker build -t systemd:v0.1-1 ./
# 创建容器
[root@docker ~/img6]#docker run --name sys-1 -d --privileged -v /sys/fs/cgroup/:/sys/fs/cgroup/:ro systemd:v0.1-1 /sbin/init
--privileged:privateged container 内的root拥有真正的root权限,否则,container内的root只是外部的一个普通用户权限。
# 进入容器
[root@docker ~/img6]#docker exec -it sys-1 /bin/bash
# 通过 systemctl 查看sshd状态
[root@ffe6f1ec35d1 /]# systemctl status sshd
● sshd.service - OpenSSH server daemon
Loaded: loaded (/usr/lib/systemd/system/sshd.service; disabled; vendor preset: enabled)
Active: inactive (dead)
Docs: man:sshd(8)
man:sshd_config(5)
构建 nginx 镜像
[root@docker ~]#mkdir -p nginx_img
[root@docker ~]#cd nginx_img/
[root@docker ~/nginx_img]#cat Dockerfile
FROM centos:centos7
LABEL maintainer="hukey<hukey@super.com>"
ENV NGX_PACKAGE="nginx-1.18.0"
#ADD http://nginx.org/download/${NGX_PACKAGE}.tar.gz /usr/local/src/
ADD http://192.168.118.45/${NGX_PACKAGE}.tar.gz /usr/local/src/
WORKDIR /usr/local/src/${NGX_PACKAGE}
RUN tar xf /usr/local/src/${NGX_PACKAGE}.tar.gz && \
cd ${NGX_PACKAGE} && \
yum install -y proc-devel gcc gcc-c++ zlib zlib-devel make openssl-devel wget && \
./configure --prefix=/usr/local/nginx \
--with-http_ssl_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-http_realip_module && \
make -j 2 && make install && \
echo "daemon off;" >> /usr/local/nginx/conf/nginx.conf && \
yum clean all && \
rm -rf /var/cache/yum/*
EXPOSE 80/tcp 443/tcp
ADD run.sh /run.sh
CMD ["/run.sh"]
---
[root@docker ~/nginx_img]#cat run.sh
#!/bin/bash
# Author:hukey
/usr/local/nginx/sbin/nginx
# 制作镜像
[root@docker ~/nginx_img]#docker build -t nginx:v0.1-1 ./
# 启动容器
[root@docker ~/nginx_img]#docker run --name web1 --rm -d -P nginx:v0.1-1
# 查看映射端口
[root@docker ~/nginx_img]#docker port web1
443/tcp -> 0.0.0.0:32788
80/tcp -> 0.0.0.0:32789
构建tomcat 镜像
[root@docker ~/tomcat]#ls
apache-tomcat-8.5.61.tar.gz Dockerfile jdk-8u77-linux-x64.tar.gz
[root@docker ~/tomcat]#cat Dockerfile
FROM centos:centos7
LABEL maintainer="hukey <hukey@super.com>"
ADD jdk-8u77-linux-x64.tar.gz /usr/local/
ADD apache-tomcat-8.5.61.tar.gz /usr/local/
ENV JAVA_HOME="/usr/local/java" \
JAVA_BIN="/usr/local/java/bin" \
JRE_HOME="/usr/local/java/jre" \
PATH="$PATH:/usr/local/java/bin:/usr/local/java/jre/bin" \
CLASSPATH="/usr/local/java/jre/bin:/usr/local/java/lib:/usr/local/java/jre/lib/charsets.jar"
WORKDIR /usr/local/
RUN mv jdk1.8.0_77 java && \
mv apache-tomcat-8.5.61 tomcat8
EXPOSE 8080
ENTRYPOINT ["/usr/local/tomcat8/bin/catalina.sh", "run"]
# 制作镜像
[root@docker ~/tomcat]#docker build -t tomcat:v0.1-1 ./
# 启动容器
[root@docker ~/tomcat]#docker run --name tomcat-1 --rm -P -d tomcat:v0.1-1
# 查看容器映射端口
[root@docker ~/tomcat]#docker port tomcat-1
8080/tcp -> 0.0.0.0:32796