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

MAINTANIER(depreacted)

  • 用于让Dockerfile 制作者提供本人的详细信息
  • Dockerfile 并不限制 MAINTAINER 指令可在出现的位置,但推荐将其放置于FROM 指令之后。
  • Syntax
    • MAINTANIER <author's detail>
      • <author's detail> 可是任何文本信息,但约定俗成的使用作者名称及邮件地址
      • MAINTANIER "hukey hukey@126.com"

LABEL

语法:

LABEL <key>=<value> <key>=<value> <key>=<value> <key>=<value>

COPY

  • 用于从 Docker主机复制文件至创建的新映像文件
  • Syntax
    • COPY ...
    • COPY ["", .. ""]
      • :要复制的源文件或目录,支持使用通配符
      • :目标路径,即正在创建的image的文件系统路径;建议为 使用绝对路径,否则,COPY 指定则以 WORKDIR 为其起始路径;
    • 注意:在路径中有空白字符时,通常使用第二种格式
  • 文件复制准则
    • 必须是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 ["", ... ""]
  • 操作准则

    • 同 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

VOLUME

  • 用于在image中创建一个挂载点目录,以挂载Docker host 上的卷或其他容器上的卷
  • Syntax
    • VOLUME
    • VOLUME [""]
  • 如果挂载点目录路径下此前在文件存在,docker run 命令会在卷挂载完成后将此前的所有文件复制到新挂载的卷中。

EXPOSE

  • 用于为容器打开指定要监听的端口以实现与外部通信
  • Syntax
    • EXPOSE [/] [[/] ...]
  • EXPOSE 指令可一次指定多个端口,例如:
    • EXPOSE 11211/udp 11211/tcp

注意:当需要开放多个端口时,建议在一行中定义,否则会多出来很多层级。

ENV

  • 用于为镜像定义所需的环境变量,并可被Dockerfile 文件中位于其后的其他指令(如 ENV、ADD、COPY等)所调用
  • 调用格式为 $variable_name${variable_name}
  • Syntax
    • ENV
    • ENV = ...
  • 第一种格式中,之后的所有内容均会被视作其的组成部分,因此,一次只能设置一个变量;
  • 第二种格式可用一次设置多个变量,每个变量为一个“=” 的键值对,如果中包含空格,可以以反斜线( \ ) 进行转义,也可通过对加引号进行标识;另外,反斜线也可用于续行;
  • 定义多个变量时,建议使用第二种方式,以便在同一层中完成所有功能

RUN

  • 用于指定 docker build 过程中运行的程序,其可以是任何命令
  • Syntax
    • RUN
    • RUN ["", "",""]
  • 第一种格式中, 通常是一个 shell 命令,且以 "/bin/sh -c" 来运行它,这意味着此进程在容器中的PID 不为 1,不能接收Unix信号,因此,当使用 docker stop 命令停止容器时,此进程接收不到 SIGTERM信号;
    • 第二种语法格式中的参数是一个JSON格式的数组,其中为要运行的命令,后面的为传递给命令的选项或参数;然而,此种格式指定的命令不会以 /bin/sh -c 来发起依赖于此shell特性的话,可以将其替换为类似下面的格式。
      • RUN ["/bin/sh","-c","<executable>","param1"]
  • 主意:json数组中,要使用双引号

CMD

  • 类似与RUN指令,CMD指令也可用于运行任何命令或应用程序,不过,二者的运行时间点不同。
    • RUN 指令运行于映像文件构建过程中,而CMD 指令运行于基于Dockerfile 构建出的新映像文件启动一个容器时,
    • CMD指令的首要目的在于为启动的容器指定默认要运行的程序,且其运行结束后,容器也将终止;不过,CMD 指定的命令其可以被docker run 的命令行选项所覆盖
  • 在 Dockerfile 中可以存在多个CMD指令,但仅最后一个会生效
  • Syntax
    • 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 ["","",""]
  • 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 命令将运行失败

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 不能自我嵌套,且不会触发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 语法一致。使用它排除构建无关的文件及目录

使用多阶段构建

多阶段构建可以有效减小镜像体积,特别是对于需编译语言而言,一个应用的构建过程往往如下:

  1. 安装编译工具
  2. 安装第三方库依赖
  3. 编译构建应用

而在前两步会有大量的镜像体积冗余,使用多阶段构建可以避免这一问题。

避免安装不必要的包

减小体积,减少构建时间。

一个容器只做一件事

如一个 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
posted @ 2020-12-10 07:09  hukey  阅读(322)  评论(0编辑  收藏  举报