zoukankan      html  css  js  c++  java
  • Docker技术入门与实战 第二版-学习笔记-3-Dockerfile 指令详解

    前面已经讲解了FROM、RUN指令,还提及了COPY、ADD,接下来学习其他的指令

    5.Dockerfile 指令详解

    1> COPY 复制文件

    格式:

    •  COPY  <源路径> ...<目标路径>
    • COPY ["<源路径1>",..."<目标路径>"]

    和 RUN指令一样,也有两种格式,一种类似于shell命令行,一种类似于exec函数调用

    COPY指令将从构建上下文目录中 <源路径>的文件/目录复制到新的一层的镜像内的 <目标路径>位置。比如:

     COPY package.json /usr/src/app/

    <源路径>可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match规则,如

    COPY hom* /mydir/
    COPY hom?.txt /mydir/

    1)如果 <源路径>是一个文件,且<目标路径>是以/结尾的,则docker会把目标路径当作一个目录,会把源文件拷贝到该目录下。

    2)如果 <源路径>是一个文件,且<目标路径>不是以/结尾的,则docker会将其视为一个文件,那么可能分下面几种情况:

    •   如果目标文件不存在,将会以该目标文件名字为名创建一个文件,内容与源文件相同;
    •  如果目标文件存在,就直接用源文件的内容覆盖目标文件的内容,目标文件名不变;
    • 如果这个不是以/结尾<目标路径>其实就是一个目录,那么会将源文件拷贝到该目录下。⚠️这种情况下最好还是加/

    3)如果<源路径>是个目录,且<目标路径>并不存在,则会先创建<目标路径>,然后将<源路径>的所有内容拷贝进来

    4)如果<源路径>是个目录,<目标路径>存在,则直接拷贝即可

    5)<目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工 作目录可以用 WORKDIR指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

    此外,还需要注意一点,使用 COPY指令,源文件的各种元数据都会保留。比如 读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建 相关文件都在使用 Git 进行管理的时候。

    2> ADD 更高级的复制文件(相对于COPY)——但是尽可能使用COPY(除了需自动解压缩的情况)

    其在COPY的基础上增加了一些功能,如:

    1)比如<源路径>可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到<目标路径>去。下载后的文件权限自动设置为600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整

    2)如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。

    所以不如直接使用 RUN指令,然后使用 wget或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

    3)具有自动解压缩的功能:如果 <源路径>为一个 tar压缩文件的话,压缩格式为gzip , bzip2以及 xz的情况下, ADD指令将会自动解压缩这个压缩文件到 <目标路径>去,如:

    FROM scratch
    ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /

    但是如果我们仅仅只是希望复制整个压缩文件,而不希望将其解压缩时,就不能够使用ADD指令了

    ⚠️所以尽可能的使用COPY ,因为 COPY的语义很明确,就是复制文件而已,而 ADD则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD的场合,就是所提及的需要自动解压缩的场合。

    而且指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢

    3> CMD 容器启动命令(就是声明容器启动时会执行的命令)

    格式:

    • shell格式: CMD <命令>
    • exec格式:CMD["可执行文件","参数1","参数2"...]
    • 参数列表格式:CMD["参数1","参数2"...] 。在指定了ENTRYPOINT指令后,使用该格式的CMD指定具体的参数

    容器就是进程。因此在启动容器时,需要指定所运行的程序和参数。

    CMD指令就是用于指定默认的容器主进程的启动命令的。

    在运行时,可以指定新的命令来替代镜像设置中的这个默认命令,如ubuntu镜像中默认的CMD为/bin/bash,如果运行docker run -it ubuntu,就会直接进入bash.

    我们也可以直接在运行时指定其他命令,如docker run -it ubuntu cat /etc/os-release.这样cat /etc/os-release这个输出系统版本信息的命令就会替代/bin/bash命令

    ⚠️一般推荐使用exec格式,因为该格式在解析时会被解析成json数组,所以一定要使用双引号,不能使用单引号

    如果使用的是shell格式,那么实际的命令会作为命令sh -c的参数来执行,如

    CMD echo $HOME

    实际上变成:

     CMD [ "sh", "-c", "echo $HOME" ]

    这就是为什么可以使用环境变量$HOME的原因,因为可以被shell解析

    ⚠️提到 CMD就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

    Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机 里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。

    对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就无意义了,其不关心其他辅助进程

    如错误命令:

     CMD service nginx start

    会发现容器执行后就立即退出了,为什么?

    其实service nginx start命令是希望upstart以后台守护进程形式启动nginx服务。

    但是上面的命令其实会变成CMD [ "sh", "-c", "service nginx start" ],主进程实际上是sh。所以当service nginx start命令结束后,sh也就结束了,sh主进程将退出,自然容器也会跟着退出

    正确写法是直接执行nginx可执行文件,然后以前台形式运行:

     CMD ["nginx", "-g", "daemon off;"]

    4> ENTRYPOINT 入口点——指定容器启动程序及参数

    与CMD相似,但是在docker build运行时需要 --entrypoint参数来指定

    当指定了ENTRYPOINT指令后,CMD的内容将作为参数传给ENTRYPOINT,如上面CMD的格式3中所说

    为什么有了CMD还需要ENTRYPOINT?

    1)让镜像变成像命令一样使用

    举例——如果我们想要知道自己当前公网IP的镜像,用CMD实现。则Dockerfile为:

    FROM ubuntu:16.04
    RUN apt-get update 
        && apt-get install -y curl 
        && rm -rf /var/lib/apt/lists/*
    CMD [ "curl", "-s", "http://ip.cn" ]

    构建镜像myip:

    docker build -t myip .

    然后我们要查询当前公网IP,则运行:

    docker run myip

    这就会默认运行CMD指令中的 curl -s http://ip.cn

    有问题的地方是:

    如果这个时候我希望得到HTTP头信息,就是需要在curl命令后加 -i 参数,但是如果我运行:

    docker run myip -i

    会报错,因为实际上是用-i将整个curl -s http://ip.cn命令给覆盖了,所以只能运行:

    docker run myip curl -s http://ip.cn -i

    这就是CMD不好的地方,但是使用ENTRYPOINT就能够解决这个问题

    ENTRYPOINT实现方案为:

    FROM ubuntu:16.04
    RUN apt-get update 
        && apt-get install -y curl 
        && rm -rf /var/lib/apt/lists/*
    ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]

    这时候运行docker run myip -i就能够得到想要的结果,-i会作为参数传给ENTRYPOINT

    2)应用运行前的准备工作

    启动容器即启动主进程,在启动主进程前需要进行一些准备工作

    比如mysql类数据库需要进行一些数据库配置、初始化等工作;或者是希望避免使用root用户...

    这些是与CMD无关的。这种情况一般是写一个脚本放到ENTRYPOINT执行,脚本需要的参数会写到CMD中,如官方镜像redis:

    FROM alpine:3.4
    ...
    RUN addgroup -S redis && adduser -S -G redis redis
    ...
    ENTRYPOINT ["docker-entrypoint.sh"]
    EXPOSE 6379
    CMD [ "redis-server" ]

    可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT为docker-entrypoint.sh脚本

    #!/bin/sh
    ...
    # allow the container to be started with `--user`
    if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
        chown -R redis .
        exec su-exec redis "$0" "$@"
    fi
    exec "$@"

    $1即传入的第一个参数,在CMD中写着,即"redis-server"。如果是"redis-server"的话,就切换到redis用户身份启动服务器;否则仍使用root身份

     ⚠️⚠️除此之外,CMD和ENTERPOINT的区别还有:

    CMD:

    • 它是容器启动时默认执行的命令
    • 如果在运行docker run时指定了要运行的命令,那么CMD命令就会被忽略
    • 如果在Dockerfile文件中定义了多个CMD,只有最后一个有效
    • 因此可以使用ENTERPOINT来替代上面的CMD
    • 而且如果执行语句 docker run -it imageName /bin/bash,那么CMD命令就不会被执行

    ENTERPOINT:

    • 容器会以应用程序或者服务的形式来运行它,这也是上面的-i能直接作为参数传给ENTERPOINT命令的原因
    • 该命令是一定会执行的,不会像CMD可能被忽略

    5>ENV 设置环境变量

    格式:

    • ENV <key> <value>
    • ENV <key1>=<value1> <key2>=<value2>

    如:

    ENV VERSION=1.0 DEBUG=on 
        NAME="Happy Feet"

    ⚠️使用进行换行,带有空格的值要用双引号括起来

    ENV NODE_VERSION 7.2.0
    RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NOD
    E_VERSION-linux-x64.tar.xz" 
      && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS25
    6.txt.asc"

    从上面的例子可以看见,在RUN中多次使用$NODE_VERSION来操作定制。这样以后如果版本有所更改,只用改变ENV即可,维护更方便

    支持环境变量展开的指令还有:

    ADD、COPY 、ENV 、EXPOSR、LABEL 、USER 、WORKDIR 、VOLUME 、STOPSIGNAL 、ONBUILD

    6> ARG 构建参数

    格式:ARG <参数名>[=<默认值>]

    1)当实现与ENV的效果一样时,用来设置环境变量。不同在于——ARG所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因 此就使用 ARG保存密码之类的信息,因为 docker history还是可以看到所有 值的。

    2)可用于定义参数名称及其默认值。该默认值可以在构建命令docker build中用 --build-arg <参数名>=<值> 来覆盖默认值

    7> VOLUME 定义匿名卷

    格式:

    • VOLUME ["<路径1>","<路径2>"...]
    • VOLUME <路径>

    容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)

    为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据

     VOLUME /data

    /data目录就会在运行时自动挂载为匿名卷,任何向 中写入的 信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:

     docker run -d -v mydata:/data xxxx

    在这行命令中,就使用了 mydata这个命名卷挂载到了 /data这个位置,替代 Dockerfile中定义的匿名卷的挂载配置

    8> EXPOSE 声明端口

    格式:EXPOSE <端口1> [<端口2>...]

    声明运行时容器提供服务端口这只是一个声明,在运行时并不 会因为这个声明应用就会开启这个端口的服务

    声明好处:

    • 一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
    • 另一个用处则是在运行时使用随机端口映射时,也就docker run -p 时,会自动随机映射 EXPOSE 的端口

    ⚠️要将 EXPOSE和在运行时使用 -p <宿主端口>:<容器端口>区分开来

    -p是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问。

    而 EXPOSE仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行 端口映射。

    9> WORKDIR 指定工作目录

    格式:WORKDIR <工作目录路径>

    以后各层的当前目录就被改为指定的目录,该目录需要已经存在, WORKDIR并不会帮你建立目录。

    之前提到一些初学者常犯的错误是把 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

    RUN cd /app
    RUN echo "hello" > world.txt

    如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt文 件,或者其内容不是hello

    原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 命令的执行环境根本不同,是两个完全不同的容器

    之前说过每一个 RUN都是启动一个容器、执行命令、然后提交存储层文件变更。 RUN cd/app第一层 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。

    而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

    因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR指令。

    10> USER 指定当前用户

    格式 : USER <用户名>

    USER指令和 WORKDIR相似,都是改变环境状态并影响以后的层

    USER是改变之后层执行 RUN, CMD以及ENTRYPOINT这类命令的身份

    但是USER只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换,举例:

    RUN groupadd -r redis && useradd -r -g redis redis
    USER redis
    RUN [ "redis-server" ]

    如果以 root执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或sudo者 ,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用gosu,可以从其项目网站看到进一步的信息:https://github.com/tianon/gosu

    # 建立 redis 用户,并使用 gosu 换另一个用户执行命令
    RUN groupadd -r redis && useradd -r -g redis redis
    # 下载 gosu
    RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/ releases/download/1.7/gosu-amd64" 
        && chmod +x /usr/local/bin/gosu 
    && gosu nobody true
    # 设置 CMD,并以另外的用户执行
    CMD [ "exec", "gosu", "redis", "redis-server" ]

    11> HEALTHCHECK 健康检查

    格式:

    • HEALHTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
    • HEALHTHCHECK NONE :如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

    指令是告诉 Docker 应该如何进行判断容器的状态是否正常

    当在一个镜像指定了HEALHTHCHECK指令后,用其启动容器,初始状态会为starting, 在HEALHTHCHECK指令检查成功后变为healthy ;如果连续一定  次数失败,则会变为unhealthy

    支持下列选项:

    • --internal=<间隔> :两次健康检查的间隔,默认为 30 秒;
    • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
    • --retries=<次数>:当连续失败指定次数后,则将容器状态视为unhealthy,默认 3 次。

    和CMD , ENTRYPOINT一样, HEALHTHCHECK只可以出现一次,如果写了多个, 只有最后一个生效

    CMD <命令>中命令的返回值决定这次健康检查的成功与否:0为成功,1为失败,2为保留,不要使用这个返回值

    假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl来帮助判断,其Dockerfile HEALHTHCHECK可以这么写:

    FROM nginx
    RUN apt-get update && apt-get install -y curl && rm -rf /var/lib
    /apt/lists/*
    HEALTHCHECK --interval=5s --timeout=3s 
      CMD curl -fs http://localhost/ || exit 1

    这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit 1作为健康检查命令。

    然后构建镜像:

    docker build -t myweb:v1 .

    然后启动容器:

    docker run -d --name web -p 80:80 myweb:v1

    然后可以使用docker ps查看容器最初状态为(health:starting):

    CONTAINER ID    IMAGE            COMMAND           CREATED                        STATUS                  PORTS         NAMES
    03e28eb00bd0   myweb:v1  "nginx -g 'daemon off"   3 seconds ago    Up 2 seconds (health: starting)    80/tcp,443/tcp     web

    等待几秒后再查看就变成了healthy

    CONTAINER ID    IMAGE            COMMAND           CREATED                STATUS                  PORTS         NAMES
    03e28eb00bd0   myweb:v1  "nginx -g 'daemon off"   3 seconds ago    Up 16 seconds (healthy)    80/tcp,443/tcp     web

    如果连续失败超过重试次数,状态就会变成unhealthy

    为了帮助排障,健康检查命令的输出(包括 stdout以及 stderr)都会被存储于健康状态里,可以用docker inspect来查看

    $ docker inspect --format '{{json .State.Health}}' web | python -m json.tool
    {
        "FailingStreak": 0,
        "Log": [
            {
                "End": "2016-11-25T14:35:37.940957051Z",
                "ExitCode": 0,
                "Output": "<!DOCTYPE html>
    <html>
    <head>
    <title>W
    elcome to nginx!</title>
    <style>
        body {
             35
    em;
            margin: 0 auto;
            font-family: Tahoma, Verda
    na, Arial, sans-serif;
        }
    </style>
    </head>
    <body>
    <h1>We
    lcome to nginx!</h1>
    <p>If you see this page, the nginx web ser
    ver is successfully installed and
    working. Further configuratio
    n is required.</p>
    
    <p>For online documentation and support pl
    ease refer to
    <a href="http://nginx.org/">nginx.org</a>.<br/>
    
    Commercial support is available at
    <a href="http://nginx.com
    /">nginx.com</a>.</p>
    
    <p><em>Thank you for using nginx.</em>
    </p>
    </body>
    </html>
    ",
                "Start": "2016-11-25T14:35:37.780192565Z"
            }
    ],
        "Status": "healthy"
    }

    12> ONBUILD 为他人做嫁衣裳

    格式: ONBUILD <其他指令>

    ONBUILD是一个特殊的指令,它后面跟的是其它指令,比如RUN , COPY等, 而这些指令,在当前镜像构建时并不会被执行

    只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

    Dockerfile中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD是为了帮助别人定制自己而准备的

    假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm进行包管理,所有依赖、配置、启动信息等会放到 package.json文件里。在拿到程序代码后,需要先进行 npm install才可以获得所有需要的依赖。然后就可以通过 npm start来启动应用。

    因此,一般来说会这样写Dockerfile :

    FROM node:slim
    RUN "mkdir /app"
    WORKDIR /app
    COPY ./package.json /app
    RUN [ "npm", "install" ]
    COPY . /app/
    CMD [ "npm", "start" ]

    如果有相似的Node.js项目,就复制该Dockerfile。这会导致文件副本越来越多

    除此之外,如果开发中发现该Dockerfile存在问题,那么对第一个项目的Dockerfile进行了更改,那么其他相似项目的Dockerfile怎么办

    这些情况就可以用ONBUILD进行解决了:

    1)首先构建一个基础镜像,这样各个相似的项目就能够使用这个基础镜像,这样就可以对基础镜像进行更新,各个项目不用同步Dockerfile的变化,重新构建后就继承了基础镜像的更新:

    FROM node:slim
    RUN "mkdir /app"
    WORKDIR /app
    CMD [ "npm", "start" ]

    这里将项目相关的构建指令都拿了出来,放到相应子项目中去。假设上面构建的基础镜像名字为my-node,那么各个子项目中的Dockerfile就变成了:

    FROM my-node
    COPY ./package.json /app
    RUN [ "npm", "install" ]
    COPY . /app/

    基础镜像变化后,各个项目都用这个Dockerfile重新构建镜像,会继承基础镜像的更新。

    2)上面只解决了一般的问题,如果子项目中Dockerfile中有东西需要调整怎么办?

    比如可能npm install 突然需要添加一些参数,又该怎么办?是不可能将RUN [ "npm", "install" ]写入基础镜像的,难道又要一个个对子项目的Dockerfile进行修改吗?

    上面的问题能够使用ONBUILD解决,重新写一下基础镜像的Dockerfile

    FROM node:slim
    RUN "mkdir /app"
    WORKDIR /app
    ONBUILD COPY ./package.json /app
    ONBUILD RUN [ "npm", "install" ]
    ONBUILD COPY . /app/
    CMD [ "npm", "start" ]

    这样在构建基础镜像的时候,ONBUILD这三行并不会被执行。然后各个项目的Dockerfile就变成了简单地:

     FROM my-node

    当在各个项目目录中,用这个只有一行的 Dockerfile构建镜像时,之前基础镜像的那三行ONBUILD就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。

    这样,如果想要对RUN [ "npm", "install" ]添加参数 --save时,就不需要到一个个子项目的Dockerfile中去添加--save了,只用在基础镜像的Dockerfile中进行更改即可

    13)LABEL

    为镜像指定标签

    格式:

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

    可见一个Dockerfile是可以有多个标签的,如:

    LABEL multi.label1="value1" 
    multi.label2="value2" 
    other="value3"

     

  • 相关阅读:
    Android Studio 单刷《第一行代码》系列 05 —— Fragment 基础
    Android Studio 单刷《第一行代码》系列 04 —— Activity 相关
    Android Studio 单刷《第一行代码》系列 03 —— Activity 基础
    Android Studio 单刷《第一行代码》系列 02 —— 日志工具 LogCat
    Android Studio 单刷《第一行代码》系列 01 —— 第一战 HelloWorld
    IDEA 内网手动添加oracle,mysql等数据源,以及server returns invalid timezone错误配置
    eclipse maven设置
    IntelliJ IDE 常用配置
    eclipse maven 常见问题解决方案
    Maven 安装和配置
  • 原文地址:https://www.cnblogs.com/wanghui-garcia/p/10120835.html
Copyright © 2011-2022 走看看