• 8、Dockerfile介绍和最佳实践


    一、Dockerfile 概念

    1、Dockerfile是什么

    Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

    镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

    Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。有了 Dockerfile,当我们需要定制自己额外的需求时,只需在 Dockerfile 上添加或者修改指令,重新生成 image 即可,省去了敲命令的麻烦。

    二、Dockerfile构建方式

    Docker通过对Dockerfile中的一系列指令的顺序解析实现自动镜像构建,构建方式:
    1. 通过使用build命令,根据Dockerfie的命令来构建镜像,默认加载当前目录下的Dockerfile文件
    2. 通过源代码路径的方式,即指定Dockerfile文件位置,比如Git仓库位置
    3. 通过标准输入流的方式

    通过源代码路径方式

    • Dockerfile需要放置在项目的根目录位置
    • 在构建的时候,Dockerfile client会把整个context打包发送到Docker Server端,然后由server端负责build镜像,在构建成功后,会删除context目录
    • docker build -t {镜像名字} {项目路径可以是相对路径}

      

    通过标准输入流方式

    • 通过标准输入流的方式获取Dockerfile的内容
    • client不会打包上传context目录,因此对于一些ADD、COPY等涉及host本地文件复制的操作不能够支持
    • docker build -t {镜像名字} - < Dockerfile路径
     

    通过build命令

    • 这是最常用的方式,docker build -t {镜像名字} {项目路径可以是相对路径,也可以是网络文件}
    • docker build -t="xuequn/nginx:v1" git@github:loveliuli/custom_dockerfile
    • 注意:custom_dockerfile目录下必须存在Dockerfile文件才行!

    三、Dockerfile构建缓存

    • Dockerfile中的每一个指令执行完毕后,都会提交为一个image,这样保证了指令之间不会有影响
    • Dockerfile会尽可能尝试重用之前已经构建的镜像
    • 可以通过在build命令中增加--no-cache的方式来禁用这个cache

     

    四、Dockerfile最佳实践

    Docker镜像由只读层组成,每个层都代表一个Dockerfile指令。这些层是堆叠的,每一层都是前一层变化的增量。

    1、了解构建上下文

    发出docker build命令时,当前工作目录称为构建上下文。默认情况下,假定Dockerfile位于此处,但您可以使用文件flag(-f)指定其他位置。无论Dockerfile实际存在的位置如何,当前目录中的所有文件和目录的递归内容都将作为构建上下文发送到Docker守护程序。

    构建上下文示例

    为构建上下文创建一个目录并cd进入该目录。将“hello”写入名为的文本文件,hello并创建一个cat在其上运行的Dockerfile 。从构建上下文(.)中构建镜像:

    mkdir myproject && cd myproject
    echo "hello" > hello
    echo -e "FROM busybox
    COPY /hello /
    RUN cat /hello" > Dockerfile
    docker build -t helloapp:v1 .
    

      

    把Dockerfile文件移动到dockerfile文件夹,hello文件移动到context文件夹,构建第二个版本(不依赖于上一个版本的缓存)。使用-f 指向Dockerfile并指定构建上下文的目录:

    mkdir -p dockerfiles context
    mv Dockerfile dockerfiles && mv hello context
    docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context
    

      

    2、管道Dockerfile通过stdin

    Docker17.05增加了Dockerfile通过stdin使用本地或远程构建上下文进行管道来构建镜像的功能。在早期版本中,使用Dockerfilefrom构建镜像stdin并未发送构建上下文。

    Docker17.04及更低版本

    
    
    docker build -t foo -<<EOF
    FROM busybox
    RUN echo "hello world"
    EOF

    Docker 17.05及更高版本(本地构建上下文)

    
    
    docker build -t foo . -f-<<EOF
    FROM busybox
    RUN echo "hello world"
    COPY . /my-copied-files
    EOF

    Docker 17.05及更高版本(远程构建上下文)

    
    
    docker build -t foo https://github.com/thajeztah/pgadmin4-docker.git -f-<<EOF
    FROM busybox
    COPY LICENSE config_local.py /usr/local/lib/python2.7/site-packages/pgadmin4/
    EOF 

    3、使用.dockerignore

    要排除与构建无关的文件(不重构源存储库),请使用.dockerignore文件。此文件支持与.gitignore文件类似的排除模式。有关创建一个的信息,请参阅 .dockerignore文件

    4、使用多阶段构建

    多阶段构建(在Docker 17.05或更高版本中)允许您大幅减小最终镜像的大小,而不必费力地减少中间层和文件的数量。

    由于镜像是在构建过程的最后阶段构建的,因此可以通过利用构建缓存来最小化镜像层。

    例如,如果您的构建包含多个镜像层,则可以从较不频繁更改(以确保构建缓存可重用)到更频繁更改的顺序对它们进行排序:

    • 安装构建应用程序所需的工具  #yum install gcc-c++等

    • 安装或更新库依赖项 #yum install nginx

    • 生成您的应用程序  #COPY ./* /var/www/

    例如Go应用程序的Dockerfile可能如下所示:

    FROM golang:1.9.2-alpine3.6 AS build
    
    # Install tools required for project#安装项目必须的工具
    # Run `docker build --no-cache .` to update dependencies #使用nocache更新依赖
    RUN apk add --no-cache git
    RUN go get github.com/golang/dep/cmd/dep
    
    # List project dependencies with Gopkg.toml and Gopkg.lock  #列出项目依赖
    # These layers are only re-built when Gopkg files are updated #这些层只会在文件有更新时才会被重构
    COPY Gopkg.lock Gopkg.toml /go/src/project/
    WORKDIR /go/src/project/
    # Install library dependencies
    RUN dep ensure -vendor-only
    
    # Copy the entire project and build it  #拷贝整个项目并构建
    # This layer is rebuilt when a file changes in the project directory  #当项目目录里有文件变化时,这一层将会被重新构建
    COPY . /go/src/project/
    RUN go build -o /bin/project
    
    # This results in a single layer image  #这样会导致一个独立的镜像层
    FROM scratch
    COPY --from=build /bin/project /bin/project
    ENTRYPOINT ["/bin/project"]
    CMD ["--help"]

    5、不要安装不必要的包

    为了降低复杂性,依赖性,文件大小和构建时间,避免安装额外的或不必要的软件包,因为它们可能“很好”。例如,您不需要在数据库镜像中包含文本编辑器。

    6、解耦应用程序

    每个容器应该只承担一个功能。

    将应用程序分散到多个容器中可以更容易地水平扩展和重用容器。例如,Web应用程序可能包含三个独立的容器,每个容器都有自己独特的镜像,以分离的方式管理Web应用程序,数据库和内存缓存。

    将每个容器限制为一个进程是一个很好的经验法则,但它并不是一个严格的规则。例如,不仅可以使用init进程生成容器 ,而且某些程序可能会自行生成其他进程。例如,Celery可以生成多个工作进程,Apache可以为每个请求创建一个进程。

    使用您的最佳判断,尽可能保持容器简单和模块化。如果容器彼此依赖,则可以使用Docker容器网络来确保这些容器可以进行通信。

    一个容器就是一个进程,承担一个功能,这个是最重要的!

    7、最小化层数

    在旧版本的Docker中,最大限度地减少镜像中的层数以确保它们具有高性能非常重要。

    添加了以下功能以减少此限制:

    • 在Docker 1.10和更高,只有指令RUNCOPYADD创建镜像。其他指令创建临时中间镜像,而不是直接增加构建的大小。

    • 在Docker 17.05及更高版本中,您可以执行多阶段构建, 并仅将所需的东西复制到最终镜像中。这允许您在中间构建阶段中包含工具和调试信息,而不会增加最终镜像的大小。

    8、对多行参数进行排序

    只要有可能,通过按字母数字方式对多行参数进行排序,可以缓解以后的更改。这有助于避免重复包并使列表更容易更新。这也使PR更容易阅​​读和审查。在反斜杠()之前添加空格也有帮助。

    例如:

    RUN apt-get update && apt-get install -y 
      bzr 
      cvs 
      git 
      mercurial 
      subversion

    9、利用构建缓存

    构建镜像时,Docker会逐步执行Dockerfile中的指令,按指定的顺序执行每个指令。在检查每条指令时,Docker会在其缓存中查找可以重用的现有镜像,而不是创建新的(重复)镜像。

    如果您根本不想使用缓存,可以使用命令中的--no-cache=true选项来docker build。但是,如果你让Docker使用它的缓存,重要的是要了解它何时可以找到匹配的镜像。

    Docker遵循的基本规则概述如下:

    • 从已经在高速缓存中的父镜像开始,将下一条指令与从该基本镜像导出的所有子镜像进行比较,以查看它们中的一个是否使用完全相同的指令构建。如果不是,则缓存无效。

    • 在大多数情况下,只需将Dockerfile其中一个子镜像中的指令进行比较就足够了。但是,某些说明需要更多的检查和解释。

    • 对于ADDCOPY指令,检查镜像中文件的内容,并计算每个文件的校验和。在这些校验和中不考虑文件的最后修改时间和最后访问时间。在高速缓存查找期间,将校验和与现有镜像中的校验和进行比较。如果文件中的任何内容(例如内容和元数据)发生了任何变化,则缓存无效。

    • 除了ADDCOPY命令之外,缓存检查不会查看容器中的文件来确定缓存匹配。例如,在处理RUN apt-get -y update命令时,不检查容器中更新的文件以确定是否存在缓存命中。在这种情况下,只需使用命令字符串本身来查找匹配项。

    一旦高速缓存失效,所有后续Dockerfile命令都会生成新镜像,并且不使用高速缓存。

     

    五、Dockerfile常用指令和最佳实践

    • 只支持Docker自己定义的一套指令,不支持自定义
    • 大小写不敏感,但是建议全部使用大写
    • 根据Dockerfile的内容顺序执行
    Dockerfile常用指令如下:
     

    1、FROM

     

    FROM {base镜像}
    必须放在Dockerfile的第一行,表示从哪个baseimage开始构建
    

     

      

     

    FROM最佳实践

    尽可能使用当前的官方存储库作为镜像的基础。
    我们推荐Alpine图像,因为它受到严格控制并且尺寸较小(目前小于5 MB),同时仍然是完整的Linux发行版。
    

      

    2、LABLE最佳实践

    您可以为镜像添加标签,以帮助按项目组织镜像,记录许可信息,帮助实现自动化或出于其他原因。

    对于每个标签,添加LABEL以一个或多个键值对开头的行。以下示例显示了不同的可接受格式。内容包括解释性意见。

    必须引用带空格的字符串或必须转义空格。内引号字符(")也必须进行转义。

    镜像可以有多个标签。

    # 设置一个或多个标签
    LABEL com.example.version="0.0.1-beta"
    LABEL vendor1="ACME Incorporated"
    LABEL vendor2=ZENITH Incorporated
    LABEL com.example.release-date="2015-02-12"
    LABEL com.example.version.is-production=""
    

     

    3、MAINTAINER

    可选的,用来标识image作者的地方
    

      

    4、RUN

    每一个RUN指令都会是在一个新的container里面运行,并提交为一个image作为下一个RUN的base
    一个Dockerfile中可以包含多个RUN,按定义顺序执行
    RUN支持两种运行方式:
     RUN <cmd> 这个会当作/bin/sh -c “cmd” 运行
     RUN [“executable”,“arg1”,。。],Docker把他当作json的顺序来解析,因此必须使用双引号,而且executable需要是完整路径
      RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN command1 的执行仅仅是当前进程,一个内存上的变化而已,其结果不会造成任何文件。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。而如果需要将两条命令或者多条命令联合起来执行需要加上&&。如:cd /usr/local/src && wget xxxxxxx
     

    RUN最佳实践

    RUN在使用反斜杠分隔的多行上拆分长或复杂语句,以使您Dockerfile更具可读性,可理解性和可维护性。
     例如:
     RUN apt-get update && apt-get install -y 
            package-bar 
            package-baz 
            package-foo
    

      使用管道:RUN wget -O - https://some.site | wc -l > /number

     

    5、CMD

    CMD的作用是作为执行container时候的默认行为(容器默认的启动命令)
    当运行container的时候声明了command,则不再用image中的CMD默认所定义的命令
    一个Dockerfile中只能有一个有效的CMD,当定义多个CMD的时候,只有最后一个才会起作用,即会被覆盖。
    CMD和ENTRPOINT之间的相互关系需要理解,ENTRPOINT不容易被覆盖,而且docker run中指定的任何参数都会当做参数再次传递给ENTRPOINT。
    CMD定义的三种方式:
      CMD <cmd> 这个会当作/bin/sh -c "cmd"来执行
      CMD ["executable","arg1",....]
      CMD ["arg1","arg2"],这个时候CMD作为ENTRYPOINT的参数
     

    CMD最佳实践

    在大多数其他情况下,CMD应该给出一个交互式shell,例如bash,python和perl。例如,CMD ["perl", "-de0"]CMD ["python"],或CMD [“php”, “-a”]
    使用此表单意味着当您执行类似的操作时 docker run -it python,您将被放入可用的shell中,随时可以使用。 
    CMD应该很少的方式使用CMD [“param”, “param”]会配合ENTRYPOINT,除非你和你预期的用户已经非常熟悉ENTRYPOINT是如何工作的。

    6、EXPOSE声明端口

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

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

    在 Dockerfile 中写入这样的声明有两个好处,

      1、帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;

      2、在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

    EXPOSE最佳实践:

    尽量使用常规端口,比如Mysql的3306,Mongo的27017。
     

    7、ENTRPOINT

    entrypoint的作用是,把整个container变成了一个可执行的文件,
    这样不能够通过替换CMD的方法来改变创建container的方式。
    但是可以通过参数传递的方法影响到container内部
    每个Dockerfile只能够包含一个entrypoint,多个entrypoint只有最后一个有效
    当定义了entrypoint以后,CMD只能够作为参数进行传递

     

    entrypoint定义方式:
    entrypoint ["executable","arg1","arg2"],这种定义方式下,CMD可以通过json的方式来定义entrypoint的参数,可以通过在运行container的时候通过指定command的方式传递参数
    entrypoint <cmd>,当作/bin/bash -c "cmd"运行命令
     

    ENTRPOINT最佳实践

    1、最好的用法是把ENTRYPOINT设置为镜像的主命令,允许该镜像和该命令一样运行(然后CMD用作默认标志)。
    例如:
    ENTRYPOINT ["s3cmd"]
    CMD ["--help"]
    2、ENTRYPOINT指令还可以与辅助脚本结合使用,使其能够以与上述命令类似的方式运行,即使启动该工具可能需要多个步骤。
    docker-entrpoint.sh文件:
    #!/bin/bash
    set -e
    
    if [ "$1" = 'postgres' ]; then
        chown -R postgres "$PGDATA"
    
        if [ -z "$(ls -A "$PGDATA")" ]; then
            gosu postgres initdb
        fi
    
        exec gosu postgres "$@"
    fi
    
    exec "$@"
    

      

    COPY ./docker-entrypoint.sh /
    ENTRYPOINT ["/docker-entrypoint.sh"]
    CMD ["postgres"]

    8、ADD & COPY

    当在源代码构建的方式下,可以通过ADD和COPY的方式,把host上的文件或者目录复制到image中
    ADD和COPY的源必须在context路径下
    当src为网络URL的情况下,ADD指令可以把它下载到dest的指定位置,这个在任何build的方式下都可以work
    ADD相对COPY还有一个多的功能,能够进行自动解压压缩包。
    ADD latest.tar.gz /var/www/wordpress 他会自动将tar包解压到wordpress目录下。

    ADD & COPY最佳实践

    一般而言,虽然ADD并且COPY在功能上类似,但是COPY 是优选的。因为COPY相对ADD来说,是更透明的,比如ADD在添加tar包时,会自动解压。
    如果您的Dockerfile有多个步骤使用上下文中的不同文件,则COPY它们是单独的,而不是一次性完成。这可确保每个步骤的构建缓存仅在特定所需文件更改时失效(强制重新执行该步骤)。
    COPY requirements.txt /tmp/
    RUN pip install --requirement /tmp/requirements.txt
    COPY . /tmp/
    

      尽量减小镜像文件大小,不是一次性的拷贝文件,而是只拷贝需要的文件,这样镜像文件会更小。

    由于镜像大小很重要,ADD因此强烈建议不要使用从远程URL获取包。你应该使用curlwget代替。这样,您可以删除提取后不再需要的文件,也不必在图像中添加其他图层。例如,你应该避免以下做法:
    ADD http://example.com/big.tar.xz /usr/src/things/
    RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
    RUN make -C /usr/src/things all

    而是要这样做:

    RUN mkdir -p /usr/src/things 
        && curl -SL http://example.com/big.tar.xz 
        | tar -xJC /usr/src/things 
        && make -C /usr/src/things all

    9、ENV

    ENV key value
    用来设置环境变量,后续的RUN可以使用它所创建的环境变量
    当创建基于该镜像的container的时候,会自动拥有设置的环境变量

    ENV最佳实践

    要使新软件更易于运行,您可以使用ENV更新PATH容器安装的软件的 环境变量。例如,ENV PATH /usr/local/nginx/bin:$PATH确保CMD [“nginx”] 正常工作。

    ENV指令还可用于提供特定于您希望容纳的服务的必需环境变量,例如Postgres PGDATA

    最后,ENV还可以用来设置常用的版本号,以便更容易维护版本的变化,如下例所示:

    ENV PG_MAJOR 9.3
    ENV PG_VERSION 9.3.4
    RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
    ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

    每一行ENV就会创建一个新的中间层,就像RUN命令一样。这意味着即使您在将来的镜像中取消设置环境变量,它仍然会在此图层中保留,并且可以转储其值。

    您可以通过创建如下所示的Dockerfile来测试它,然后构建它。

    FROM alpine
    ENV ADMIN_USER="mark"
    RUN echo $ADMIN_USER > ./mark
    RUN unset ADMIN_USER
    CMD sh
    $ docker run --rm -it test sh echo $ADMIN_USER
    
    mark

    要防止这种情况发生,并且确实取消了之前设置的环境变量,请使用RUN带有shell命令的命令,在单个镜像中设置,使用和取消设置变量all。您可以使用;或分隔命令&&

    如果您使用第二种方法,并且如果其中一个命令失败,则docker build也会失败。这通常是一个好主意。使用作为行继续符可以提高可读性。

    您还可以将所有命令放入shell脚本中,并让RUN命令运行该shell脚本。 

    10、WORKDIR

    用来指定当前工作目录(或者称为当前目录)
    当使用相对目录的情况下,采用上一个WORKDIR指定的目录作为基准 

    WORKDIR最佳实践

    为了清晰和可靠,您应该始终使用绝对路径的 WORKDIR。此外,您应该使用WORKDIR,而不是使用难以阅读,排除故障和维护指令RUN cd … && do-something

    11、USER

     

    指定UID或者username,来决定运行RUN指令的用户
    

     

      

     

    USER最佳实践
    如果服务可以在没有权限的情况下运行,请把USER更改为非root用户。
    首先在Dockerfile中创建用户和组RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres。
    镜像中的用户和组被分配了不确定的UID/GID,因为无论镜像如何构建,都会分配一个UID/GID。因此,如果它很重要,您应该分配一个显式的UID / GID。
    由于Go归档/tar包在处理松散文件时有一个未解决的错误,尝试在Docker容器内创建具有非常大的UID的用户可能导致磁盘耗尽,因为/var/log/faillog在容器层中填充了NULL( 0)字符。
    解决方法是将--no-log-init标志传递给useradd。Debian / Ubuntu adduser装饰器不支持此标志。

    避免安装或使用sudo,因为它具有可能导致不可预测的TTY和信号转发行为的问题。如果您绝对需要类似的功能sudo,例如将守护程序初始化root为非运行它root,请考虑使用“gosu”

    最后,为了减少层次和复杂性,避免USER频繁地来回切换。

    12、ONBUILD

    ONBUILD作为一个trigger的标记,可以用来trigger任何Dockerfile中的指令
    可以定义多个ONBUILD指令
    当下一个镜像B使用镜像A作为base的时候,在FROM A指令前,会先按照顺序执行在构建A时候定义的ONBUILD指令
    ONBUILD <DOCKERFILE 指令> <content>

    ONBUILD最佳实践

    ONBUILD指令能为镜像添加触发器,当一个镜像被用作其他镜像的基础镜像时,改镜像中的触发器将会被执行。

    ONBUILD指令是紧跟在FROM之后指定的

    Docker构建ONBUILD在子代中的任何命令之前执行命令Dockerfile

    把时要小心,ADDCOPYONBUILD。如果新构建的上下文缺少正在添加的资源,则“onbuild”映像将发生灾难性故障。

    例如:

    FROM ubuntu:14.04
    MAINTAINER xuequn "xuequn@kingsoft.com"
    RUN apt-get update
    RUN apt-get install -y apache2
    ENV APACHE_RUN_USER www-data
    ENV APACHE_RUN_GROUP www-data
    ENV APACHE_LOG_DIR /var/log/apache2
    ONBUILD ADD . /var/www/
    EXPOSE 80
    ENTRPOINT ["/usr/sbin/apache2"]
    CMD ["-D","FOREGOUND"]
    

     

      在新构建的镜像包含一条ONBUILD指令,该指令会使用ADD 指令将构建环境所在的目录下的所有文件拷贝到/var/www/下面。

    当我们使用上面的镜像作为基础镜像,再构建一个新的镜像时:

    FROM xuequn/apache2
    MAINTAINER xuequn 'xuequn@kingsoft.com'
    ENV APPLICATION_NAME webapp01
    

      当执行完FROM时,就进入了构建阶段,此时会出发基础镜像中的ONBUILD指令,会将当期目录下的所有文件拷贝到/var/www/下面,这样就完成了个性化镜像制作功能,这就是ONBUILD的绝妙之处!!

    注意:

    • ONBUILD指令只会被继承一次,也就是在子镜像制作时会出发ONBUILD指令,而孙子镜像构建时不会再触发此指令!
    • ONBUILD指令中有几条指令是不能使用的:FROM/MAINTAINER/ONBUILD。因为这样会进入递归调用而进入死循环!

    13、VOLUME

    用来创建一个在image之外的mount point,用来在多个container之间实现数据共享
    运行使用json array的方式定义多个volume
    VOLUME ["/var/data1","/var/data2"]
    或者plain text的情况下定义多个VOLUME指令

    VOLUME最佳实践

    该VOLUME指令应用于公开由docker容器创建的任何数据库存储区域,配置存储或文件/文件夹。
    

     

      

     

  • 相关阅读:
    html 之input标签height设置问题
    jquery 练习笔记
    JavaScript
    css记录
    python 反射器
    python 之 前端初识 html
    [mysql]修改
    [mysql]约束
    [mysql]select
    python_exercise_能在当前目录以及当前目录的所有子目录下查找文件名包含指定字符串的文件,并打印出相对路径
  • 原文地址:https://www.cnblogs.com/skyflask/p/10048207.html
Copyright © 2020-2023  润新知