Dockerfile详解以及高级技巧

Docker是通过读取Dockerfile文件来自动构建镜像,Dockerfile其实就是一个包含了很多命令行指令的文本文件,通过这些指令来装配一个镜像。 要掌握Docker构建镜像的技巧,就必须首先了解Dockerfile的基本指令,下面先详细介绍Dockerfile中的一些常用指令。

基本指令

  • FROM

    From用来在构建镜像时指定一个基础镜像,一个有效的Dockerfile文件必须以From指令开始,可以通过AS name命令给当前这个创建的阶段一个别名, 然后这个别名在后序的From指令以及COPY --from=name可以引用。当然,严格的来说Dockerfile并不是必须以From指令开始,因为From 指令也支持变量,在From之前可以通过ARG定义变量,这个ARG必须在第一个From指令之前。

    ARG  CODE_VERSION=latest
    FROM base:${CODE_VERSION}
    CMD  /code/run-app
    
    FROM extras:${CODE_VERSION}
    CMD  /code/run-extras
    
  • MAINTAINER

    这个指令用来设置创建镜像的作者信息,已经废弃,官方提倡使用更灵活的LABEL指令,LABEL指令可以设置更多的属性。

    MAINTAINER sunjinfu@163.com
    

    通过LABEL指令代替MAINTAINER

    LABEL maintainer="sunjinfu@163.com"
    
  • LABEL

    LABEL指令主要用来给镜像增加一些metadata,一个LABEL是一个key-value形式的键值对,如果LABEL中需要包含空格或者反斜杠,必须用双引号括起来。

      LABEL "name"="sunjinfu"
      LABEL email="sunjinfu@163.com"
      LABEL version="1.0"
      LABEL description="I am one \
            good man."
    

    一个镜像可能有很多LABEL,可以通过以下两种方式尽量定义在一行中。

      LABEL "name"="sunjinfu" email="sunjinfu@163.com"
    
      LABEL "name"="sunjinfu" \
            email="sunjinfu@163.com" \
            age="20"
    

    LABEL是可以从base镜像那继承的,如果有冲突,一般先前定义的LABEL都会被覆盖,镜像具有哪些LABEL,通过docker inspect命令即可查看。

  • ENV

    ENV指令用来设置环境变量,在构建镜像阶段,后续所有的指令都可以使用它,设置环境变量有两种方式。

    • ENV <key> <value>
    • ENV <key>=<value> …

    ENV <key> <value>只能设置单个变量,而ENV <key>=<value>可以同时设置多个,可以使用"或者\包含空格。

    ENV name="sun jinfu" email=sunjinfu@163.com\ sunjinfu@126.com\ sunjinfu@gmail.com \
        address=beijing
    

    等价于

    ENV name sun jinfu
    ENV email sunjinfu@163.com sunjinfu@126.com sunjinfu@gmail.com
    ENV address beijing
    
  • VOLUME

    VOLUME指令用来创建一个给定名字的挂载点,如 VOLUME ["/data"] ,当容器运行的时候,可以很方便的将容器目录中的数据与主机目录数据共享。VOLUME 是以JSON数组形式解析的,因此必须以"括起来,如 VOLUME ["/data", "/var/log"]

  • WORKDIR

    WORKDIR用于设置工作目录,如果WORKDIR指令设置的目录不存在则会自动创建,在一个Dockerfile文件中可以通过WORKDIR设置多次工作目录,如:

    WORKDIR /a
    WORKDIR b
    WORKDIR c
    RUN pwd
    

    pwd命令的输出结果是/a/b/c,WORKDIR指令可以查找在它之前通过ENV指令设置的环境变量。

    ENV DIRPATH /path
    WORKDIR $DIRPATH/$DIRNAME
    RUN pwd
    

    pwd命令的输出结果是/path/$DIRNAME

  • EXPOSE

    EXPOSE指令用于通知Docker当前容器在运行时的监听端口,协议支持TCPUDP,默认TCP,用法:

    EXPOSE 80/tcp
    EXPOSE 80/udp
    EXPOSE 3306
    

    EXPOSE指令暴露容器端口后,执行docker run命令时带上flag大写 -P即可将容器暴露端口映射到主机的随机端口(49000~49900),可以通过flag小写-p指定 端口映射,此时实际EXPOSE指令并未发生任何作用,被覆盖。

  • HEALTHCHECK

    HEALTHCHECK指令有两种方式:

    • HEALTHCHECK [OPTIONS] CMD command #通过在容器内部运行一个命令健康检查
    • HEALTHCHECK NONE #禁用从基础镜像那继承任何健康检查

    HEALTHCHECK指令告诉Docker如何检查容器是否正常,当给一个容器定义了一个健康检查规则时,那么容器则有一个健康状态。当健康检查通过时,容器则会展示healthy, 否则展示为unhealthyHEALTHCHECK的OPTIONS参数如下:

    • –interval=DURATION (default: 30s)
    • –timeout=DURATION (default: 30s)
    • –start-period=DURATION (default: 0s)
    • –retries=N (default: 3)
    • 健康检查会在容器启动之后的interval秒首次执行,之后间隔interval秒进行健康检查,注意这里的容器启动,并不是容器内部的应用启动,比如在容器中部署了一个tomcat 应用,这个tomcat应用需要50秒才能完成启动,而容器启动只需2秒,如果interval设置为30秒,健康检查又设置为调用容器内部应用的一个接口, 每次健康检查都返回非200状态码,这样Docker就不断的重启该容器,陷入无限循环了,正确的做法是将interval设置大一点,比如60秒。
    • timeout健康检查超时时间
    • start-period 容器启动初始化时间,在这段时间内如果健康检查失败,并不是累加到retries字段上。

    健康检查的CMD指令返回状态码,0表示容器健康状态,1表示容器不健康状态,2保留状态码,暂未使用。下面这个例子,http请求的响应状态码是401才表示系统健康状态。

    HEALTHCHECK CMD curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/api/jobs/replication/1/log|grep 401
    
  • ADD

    ADD指令有两种方式,第二种方式可以支持包含空格的路径,因为其用"括起来。

    • ADD [–chown=<user>:<group>] <src>… <dest>
    • ADD [–chown=<user>:<group>] [”<src>”,… “<dest>”]

    ADD指令用于将主机中文件、目录或者通过链接指定的远程文件复制到镜像中的文件系统中,要复制的源文件、源目录都是相对于当前构建镜像的上下文。 src可以包括一些匹配表达式,如:

    ADD hom* /mydir/        # 将上下文目录中所有文件名以`hom`开始的文件复制到镜像的/mydir/目录中
    ADD hom?.txt /mydir/    # ? 匹配单个字符
    

    dest可以是一个绝对路径,也可以是一个基于WORKDIR的相对路径。

    ADD test relativeDir/          # adds "test" to `WORKDIR`/relativeDir/
    ADD test /absoluteDir/         # adds "test" to /absoluteDir/
    

    所有复制到镜像中的文件或者目录,默认都是以UID、GID为0的用户创建,除非通过--chown指定,注意,如果容器文件系统中没有/etc/passwd或者/etc/group, 或者通过--chown指定的用户信息不存在,那么ADD操作则会失败。

    ADD --chown=55:mygroup files* /somedir/
    ADD --chown=bin files* /somedir/
    

    ADD指令的源文件都是基于构建上下文,所以不允许ADD ../somefile /somefile,只能向下不能向上,添加多个文件可以写在一行。

    ADD start.sh harbor_jobservice /harbor/   
    

    也可以用\写在多行

    ADD docker-compose.clair.yml \
    docker-compose.yml \
    harbor.cfg \
    install.sh \
    registry.sql \
    prepare \
    /data/harbor/
    

    添加整个目录到镜像层中

    ADD /data/harbor   /harbor
    

    ADD指令会自动将gziptar.gz等压缩包自动解压,当然文件是否被解压不是根据文件名决定的,而是文件内容,即使有一个空的文件以.tar.gz结尾, 它也不会被解压,只是简单的将该文件添加。

    注意:

    • src有多个,那么dest必须是个目录,dest必须以/结尾
    • dest没有以/结尾,它会被认为是个常规的文件,直接将src文件的内容写到dest这个文件中
    • dest目录中任何一级目录不存在,都会被自动创建
  • COPY

    COPY指令与ADD指令功能相似,也支持两种方式。

    • COPY [–chown=<user>:<group>] <src>… <dest>
    • COPY [–chown=<user>:<group>] [”<src>”,… “<dest>”]

    COPY指令与ADD指令的区别就是它不支持远程URL文件复制,同时压缩包不会自动解压,其他用法基本与ADD指令一致。

  • USER

    USER指令用于设置容器运行时的用户名、用户组,在Dockerfile中指定用户后,后续的RUNCMD等指令,都将以该用户身份运行。

        USER user
      USER user:group
      USER uid
      USER uid:gid
    
  • RUN

    RUN指令有两种方式:

    • RUN <command>,命令用shell方式运行,在Linux中默认的shell命令/bin/sh -c,windows则是cmd /S /C
    • RUN [“executable”, “param1”, “param2”] (exec方式)

    RUN指令主要用来在当前镜像的最上层执行一些命令继而生成新的一个镜像层,用shell方式执行命令时可以通过一个\继续在文档的下一行编写一个命令。

    RUN /bin/bash -c 'source $HOME/.bashrc; \
    echo $HOME'
    

    写在一行也完全可以

    RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'
    

    如果不想用/bin/sh,想用/bin/bash执行命令,那么就不得不使用exec方式了。

    RUN ["/bin/bash", "-c", "echo hello"]
    

    exec方式被解析成JSON数组,因此每一项都必须使用"括起来,exec方式不会调用shell命令,因此RUN [ "echo", "$HOME" ]中的变量$HOME不会被替换, 要么使用shell方式运行或者RUN [ "sh", "-c", "echo $HOME" ] exec方式即可。

  • CMD

    CMD指令有三种方式:

    • CMD [“executable”,“param1”,“param2”] (exec方式,推荐)
    • CMD [“param1”,“param2”] (将param1、param2作为默认参数传递给ENTRYPOINT)
    • CMD command param1 param2 (shell方式)

    Dockerfile文件中只有一个CMD指令会生效,如果你提供多个CMD,只有最后一个生效。CMD是容器启动时执行的一种默认行为, 通过docker run运行容器时设置的命令会直接覆盖CMD,完全可以不设置CMD,设置ENTRRYPOINT即可。

  • ENTRYPOINT

    ENTRYPOINT也有两种方式:

    • ENTRYPOINT [“executable”, “param1”, “param2”] (exec,推荐)

    • ENTRYPOINT command param1 param2 (shell)

    可以通过ENTRYPOINT将容器配置成可执行程序,通过docker run运行容器时的参数均可以传递给以exec方式执行的ENTRYPOINT指令上,它的默认参数 可以通过CMD指令进行设定,docker run命令设置的参数可以直接覆盖CMD指令为ENTRRYPOINT设置的默认参数,执行docker run <image> -d命令时, 参数 -d会直接传递给ENTRRYPOINT,当然通过 docker run --entrypoint可以覆盖Dockerfile中的ENTRYPOINT

    shell执行方式会阻止CMD以及docker run等命令的任何参数,使用这种执行方式时,ENTRYPOINT相当于是/bin/sh -c的一个子命令,该子命令不能接受信号。 这就意味着ENTRYPOINT指令设定的可执行程序在容器中的PID将不是1,将不会接受Unix的任何信号,即执行docker stop <container>命令时,可执行程序无法接受到 SIGTERM

    下面举个例子,首先编写一个Dockerfile文件,然后通过该Dockerfile构建镜像test,注意CMDENTRYPOINT都是采用了推荐的exec方式。

    From centos
    ENTRYPOINT ["top", "-b"]
    CMD ["-c"]
    

    构建镜像

    #docker build -t test -f Dockerfile .
    Sending build context to Docker daemon  2.048kB
    Step 1/3 : From centos
     ---> 9f38484d220f
    Step 2/3 : ENTRYPOINT ["top", "-b"]
     ---> Running in f634088947be
    Removing intermediate container f634088947be
     ---> 02dd3aeda4d3
    Step 3/3 : CMD ["-c"]
     ---> Running in c90a93cf729d
    Removing intermediate container c90a93cf729d
     ---> 7fae0be844ef
    Successfully built 7fae0be844ef
    Successfully tagged test:latest
    

    运行容器

    # docker run --rm --name test test:latest
    top - 02:46:03 up 55 days, 17:53,  0 users,  load average: 0.71, 0.24, 0.16
    Tasks:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
    %Cpu(s):  3.3 us,  6.7 sy,  0.0 ni, 90.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    KiB Mem :  3882020 total,   561132 free,   560420 used,  2760468 buff/cache
    KiB Swap:        0 total,        0 free,        0 used.  2816488 avail Mem 
    
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    1 root      20   0   56060   1880   1444 R   0.0  0.0   0:00.03 top -b -c
    

    容器中PID为1的进程为top命令,CMD指令将参数-c传递给了ENTRYPOINT

构建技巧

  • 上下文

    在了解构建镜像上下文的概念之前,首先要了解清楚Docker的软件架构,最基础的就是Docker Client与Docker Daemon。 Docker Daemon是Docker架构中的主体部分,具备服务端的功能,能直接接收Docker Client发起的请求。Docker Client发起的 相关命令docker pulldocker build等都是请求Docker Daemon服务端。

    docker

    从上图中,你应该已经了解了,执行docker build命令构建镜像实际是在服务端进行的,只不过这个服务端运行在本地主机上,通过 docker version命令可以查看版本。

    Docker构建镜像时有一个上下文的概念,执行docker build命令时首先会把上下文目录中的所有文件全部打包发送到Docker Daemon服务端, 所以上下文目录的大小很大程度决定了你本次镜像构建的速度,这就是为什么不要直接把Linux的根目录作为构建上下文的原因, 构建镜像最好的习惯是新建一个目录作为构建的上下文,把Dockerfile中需要的文件都复制到该目录,然后执行命令构建镜像。

    下面举个例子,编写Dockerfile,这个Dockerfile只与主机中的一个readme.txt文件有关。

    From centos
    ADD readme.txt /data
    ENTRYPOINT ["top", "-b"]
    CMD ["-c"]
    

    当前Dockerfile文件、readme.txt均在/root目录下,该目录下还有其他一些与镜像无关的文件。

    [root@vm ~]# ls -lrt
    total 372568
    -rw-r--r-- 1 root root 127163815 Aug 25  2018 go1.11.linux-amd64.tar.gz
    -rw-r--r-- 1 root root      2672 Feb 13 17:04 extranet.sh
    -rw-r--r-- 1 root root 127163815 Apr 10 13:17 a.gz
    -rw-r--r-- 1 root root 127163815 Apr 10 13:18 b.gz
    drwxr-xr-x 2 root root      4096 Apr 10 13:19 tmp
    -rw-r--r-- 1 root root         8 Apr 10 13:20 readme.txt
    -rw-r--r-- 1 root root        69 Apr 10 13:21 Dockerfile
    

    构建镜像

    [root@vm ~]# docker build -t readme:v1 -f Dockerfile .
    Sending build context to Docker daemon  763.8MB
    Step 1/4 : From centos
     ---> 9f38484d220f
    Step 2/4 : ADD readme.txt /data
     ---> 90f4d2c008f2
    Step 3/4 : ENTRYPOINT ["top", "-b"]
     ---> Running in 6a07e260807c
    Removing intermediate container 6a07e260807c
     ---> 927d6cebf38d
    Step 4/4 : CMD ["-c"]
     ---> Running in 594773b76eab
    Removing intermediate container 594773b76eab
     ---> 6917f27020e0
    Successfully built 6917f27020e0
    Successfully tagged readme:v1
    You have new mail in /var/spool/mail/root
    

    从上面Docker输出信息Sending build context to Docker daemon 763.8MB,一共发送了763.8M文件到Docker Deamon,而Dockerfile只需要一个readme.txt文件,这就是 构建上下文没有正确选择,在/root目录下新建一个文件夹docker作为上下文,把readme.txt都移到docker文件夹中,进行构建。

    [root@vm172-20-0-15 ~]# docker build -t readme:v2 -f Dockerfile docker
    Sending build context to Docker daemon  2.607kB
    Step 1/4 : From centos
    ---> 9f38484d220f
    Step 2/4 : ADD readme.txt /data
    ---> Using cache
    ---> 90f4d2c008f2
    Step 3/4 : ENTRYPOINT ["top", "-b"]
    ---> Using cache
    ---> 927d6cebf38d
    Step 4/4 : CMD ["-c"]
    ---> Using cache
    ---> 6917f27020e0
    Successfully built 6917f27020e0
    Successfully tagged readme:v2
    

    从上面这个输出信息看,Docker Client只向Docker Daemon服务端发送了2.607kB大小文件。通常在大型项目中,自动化构建镜像时,一定要注意上下文的作用与范围。

  • 镜像大小

    镜像太大,同时网络带宽又有限,那么通过docker pull命令从镜像仓库拉取镜像时非常耗时,所以优化镜像大小很有必要,可以从以下几方面优化。

    • 选择合适的基础镜像
    • 优化Dockerfile中的指令编写,同一个指令尽量写在一行
    • 根据应用的开发语言,剥离相关的环境依赖,比如go运行时并不需要的编译环境

    下面以go项目container为例,选择用golang:alpine为基础镜像,这个基础镜像相比于golang:1.11.1又小很多,接着将container项目的整个源码目录复制到镜像层的GOPATH 目录下的src目录,然后执行go install编译源码,链接成可执行文件container,Dockerfile文件如下:

    FROM golang:alpine
    COPY src /go/src
    RUN go install -v container
    ENTRYPOINT ["/go/bin/container"]
    

    构建镜像

    [root@vm docker]# docker build -t container:v1.0 .
    Sending build context to Docker daemon  57.74MB
    Step 1/4 : FROM golang:alpine
     ---> 20ff4d6283c0
    Step 2/4 : COPY src /go/src
     ---> dd2d3480ebd0
    Step 3/4 : RUN go install -v container
     ---> Running in d67a1cf74365
     ---> b6c5ed0d75f5
    Removing intermediate container d67a1cf74365
    Step 4/4 : ENTRYPOINT /go/bin/container
     ---> Running in 734b0fdc6e5c
     ---> 395503e87bc1
    Removing intermediate container 734b0fdc6e5c
    Successfully built 395503e87bc1
    Successfully tagged container:v1.0
    

    查看镜像大小

    REPOSITORY    TAG                 IMAGE ID            CREATED             SIZE
    container     v1.0                395503e87bc1        2 minutes ago       386MB
    

    区区一个简单的go项目竟然达到386M,并且整个项目源码也在容器中,不安全。下面将go的编译环境去除,因为go项目运行时不依赖go sdk相关组件。 优化一下Dockerfile文件,将alpine作为最终的基础镜像。

    FROM golang:alpine AS build-env
    MAINTAINER sunjinfu@163.com
    ADD src /go/src
    RUN go build container
    
    FROM alpine
    RUN mkdir /go
    WORKDIR /go
    COPY --from=build-env /go/container /go
    EXPOSE 8080
    ENTRYPOINT ["./container"]
    

    构建镜像

    [root@vm docker]# docker build -t container:v2.0 . 
    Sending build context to Docker daemon  57.74MB
    Step 1/10 : FROM golang:alpine AS build-env
     ---> 20ff4d6283c0
    Step 2/10 : MAINTAINER sunjinfu@163.com
     ---> Using cache
     ---> ac5b51c8ee48
    Step 3/10 : ADD src /go/src
     ---> a1b828a87e8d
    Step 4/10 : RUN go build container
     ---> Running in 7f4c09d3e576
     ---> cd073b46d45d
    Removing intermediate container 7f4c09d3e576
    Step 5/10 : FROM alpine
     ---> 5cb3aa00f899
    Step 6/10 : RUN mkdir /go
     ---> Running in 8a7bd2f9025d
     ---> 05b4a219e3e5
    Removing intermediate container 8a7bd2f9025d
    Step 7/10 : WORKDIR /go
     ---> fcb8526b7b76
    Removing intermediate container a8f531d742a7
    Step 8/10 : COPY --from=build-env /go/container /go
     ---> 55df14427b9c
    Step 9/10 : EXPOSE 8080
     ---> Running in 82f9e5752c90
     ---> f5c9c6e4c1ed
    Removing intermediate container 82f9e5752c90
    Step 10/10 : ENTRYPOINT ./container
     ---> Running in 9ccd355dd431
     ---> 053388fa3e2c
    Removing intermediate container 9ccd355dd431
    Successfully built 053388fa3e2c
    Successfully tagged container:v2.0
    

    查看镜像大小,container:v2.0版本的镜像只有15MB。

    REPOSITORY        TAG            IMAGE ID            CREATED              SIZE
    container         v2.0           053388fa3e2c        About a minute ago   15.6MB
    
  • 镜像缓存

    Docker会缓存已有镜像的镜像层,构建新镜像时,如果某个镜像层已经存在,则直接利用缓存的镜像层,无须重新创建。

    From centos
    ADD readme.txt /data
    ENTRYPOINT ["top", "-b"]
    CMD ["-c"]
    

    构建镜像

    [root@vm ~]# docker build -t test:v1.0 -f Dockerfile docker
    Sending build context to Docker daemon  2.629kB
    Step 1/4 : From centos
     ---> 9f38484d220f
    Step 2/4 : ADD readme.txt /data
     ---> a097ddb31783
    Step 3/4 : ENTRYPOINT ["top", "-b"]
     ---> Running in 89a4e6cd646b
    Removing intermediate container 89a4e6cd646b
     ---> d9a2db7afdf5
    Step 4/4 : CMD ["-c"]
     ---> Running in e51982daa4bd
    Removing intermediate container e51982daa4bd
     ---> 92dc03cdc871
    Successfully built 92dc03cdc871
    Successfully tagged test:v1.0
    

    下面需要构建另外一个镜像,Dockerfile如下:

    From centos
    ADD readme.txt /data
    EXPOSE 8080
    ADD service.yml /data
    ENTRYPOINT ["top", "-b"]
    

    构建镜像,在构建test:v1.0镜像时,ADD readme.txt /data这一镜像层的id是a097ddb31783,再次构建test:v2.0时将直接利用该镜像层缓存,注意查看输出信息中的 Using cache,当然如果不想让Docker利用缓存,可以带上Flag参数--no-cache重新构建。

    [root@vm ~]# docker build -t test:v2.0 -f Dockerfile docker
    Sending build context to Docker daemon  4.143kB
    Step 1/5 : From centos
     ---> 9f38484d220f
    Step 2/5 : ADD readme.txt /data
     ---> Using cache
     ---> a097ddb31783
    Step 3/5 : EXPOSE 8080
     ---> Running in 6c845ce26992
    Removing intermediate container 6c845ce26992
     ---> 0ab30541f7ea
    Step 4/5 : ADD service.yml /data
     ---> a0344636b78c
    Step 5/5 : ENTRYPOINT ["top", "-b"]
     ---> Running in 905a22119eb7
    Removing intermediate container 905a22119eb7
     ---> f9ba0de68a00
    Successfully built f9ba0de68a00
    Successfully tagged test:v2.0
    

    Dockerfile中每一个指令都是一个镜像层,上层镜像依赖下层镜像,只要某一层发生变化,其上层所有镜像层缓存均失效。

  • 镜像调试

    镜像在构建过程中也经常会失败,当出现失败时,我们可以进行调试,通过docker run命令可以运行失败指令的前一个指令成功构建的镜像层。

    From centos
    ADD readme.txt /data
    EXPOSE 8080
    ADD service.yml /data/yaml/
    ENTRYPOINT ["top", "-b"]
    
    [root@vm ~]# docker build -t test:v2.0 -f Dockerfile docker
    Sending build context to Docker daemon  4.194kB
    Step 1/5 : From centos
     ---> 9f38484d220f
    Step 2/5 : ADD readme.txt /data
     ---> Using cache
     ---> a097ddb31783
    Step 3/5 : EXPOSE 8080
     ---> Using cache
     ---> 0ab30541f7ea
    Step 4/5 : ADD service.yml /data/yaml/
    failed to copy files: lstat /data/docker/overlay2/026d474a8bae00c99e5b126df2ebb99128f6b2978eecb341db5cead0b89f2719/merged/data/yaml: not a directory
    

    执行 ADD service.yml /data/yaml指令时发生错误,可以直接启动容器运行指令EXPOSE 8080构建的这一镜像层0ab30541f7ea。

    [root@vm172-20-0-15 ~]# docker run -it 0ab30541f7ea sh
    sh-4.2# ls
    bin  data  dev  etc  home  lib  lib64  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
    sh-4.2# cd /data
    sh: cd: /data: Not a directory
    

    进入之后,我们才恍能大悟,上面已经详细介绍过ADD指令了,当ADD指令的dest没有以/结尾时,Docker会把它当成是一个文件,在这里相当于把readme.txt的文件内容写到 了/data这个文件中了,此时/data并不是文件夹,当执行ADD service.yml /data/yaml/命令时,Docker会自动去创建/data目录,此时已经有一个/data同名文件存在, 所以创建失败。