jannal(无名小宝)

没有失败,只有缓慢的成功

导航

Docker之镜像

镜像简介

  1. 基础镜像(base):不依赖其他镜像,从scratch构建,其他镜像可以从它扩展。比如Centos7的镜像

    1. 地址https://github.com/CentOS/sig-cloud-instance-images/tree/b2d195220e1c5b181427c3172829c23ab9cd27eb/docker
    
    # 基于scratch,scratch是个空镜像,没有东西,不能当成镜像下载(即无法docker pull scratch )
    FROM scratch
    # CentOS 7的rootfs。在制作镜像时,这个tar包会自动解压到/目录下,生成/dev、/proc、/bin等目录。
    ADD centos-7-x86_64-docker.tar.xz /
    
    LABEL \
        org.label-schema.schema-version="1.0" \
        org.label-schema.name="CentOS Base Image" \
        org.label-schema.vendor="CentOS" \
        org.label-schema.license="GPLv2" \
        org.label-schema.build-date="20201113" \
        org.opencontainers.image.title="CentOS Base Image" \
        org.opencontainers.image.vendor="CentOS" \
        org.opencontainers.image.licenses="GPL-2.0-only" \
        org.opencontainers.image.created="2020-11-13 00:00:00+00:00"
    
    CMD ["/bin/bash"]
    
  2. 为什么基础镜像发行版(比如Centos)相对较小?

    • Linux操作系统由内核空间和用户空间组成,内核空间是kernel, Linux刚启动时会加载bootfs文件系统,之后bootfs会被卸载掉。用户空间的文件系统是rootfs,包含我们熟悉的 /dev、/proc、/bin等目录。

    • 对于基础镜像来说,底层直接用Host的kernel,自己只需要提供rootfs。基础镜像提供了最小安装的Linux发行版

    • 不同Linux发行版的区别主要就是rootfs,比如Ubuntu 14.04使用upstart管理服务,apt管理软件包;而CentOS 7使用systemd和yum。这些都是用户空间上的区别,Linux kernel差别不大

  3. 基础镜像(base)只是在用户空间与发行版一致,kernel版本与发行版是不同的。所有容器都共用Docker host(宿主机)的kernel,在容器中没办法对kernel升级。如果容器对kernel版本有要求(比如应用只能在某个kernel版本下运行),则不建议用容器,这种场景虚拟机可能更合适

    cda6e44ae222        jenkins/jenkins:2.233-centos  
    663268bd2fef        mysql:5.7.29    
    
    两个不同的镜像,查看内核发现是一样的(这是在Mac环境下,Docker是在虚拟机中运行)
    也就是说容器只能使用Docker Host(宿主机)的内核,并且不能修改
    $ docker exec -it cda6e44ae222 uname -r
    4.19.76-linuxkit
    $ docker exec -it 663268bd2fef uname -r
    4.19.76-linuxkit
    
    查看Docker for Mac的虚拟机 
    方法:https://gist.github.com/BretFisher/5e1a0c7bcca4c735e716abf62afad389
    $ screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty
    关闭
    $ Ctrl-A k (then y to confirm).
    

镜像分层

  1. Docker的镜像是从基础镜像一层层叠加生成的。分层最大的好处就是共享资源

    • 有多个镜像都从相同的base镜像构建而来,那么Docker Host只需在磁盘上保存一份base镜像;同时内存中也只需加载一份base镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享
    • 当某个容器修改了基础镜像的内容,比如 /etc下的文件,这时其他容器的 /etc是否不会被修改。修改只会被限制在单个容器中
  2. 当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作容器层容器层之下的都叫镜像层。所有对容器的改动,无论添加、删除,还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的

  3. 所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /etc,上层的 /etc会覆盖下层的 /etc,也就是说用户只能访问到上层中的文件/etc。在容器层中,用户看到的是一个叠加之后的文件系统。

  4. 容器层(Copy-on-Write特性):保存的是镜像变化的部分,不会对镜像做任何修改

    • 添加文件。在容器中创建文件时,新文件被添加到容器层中。
    • 读取文件。在容器中读取某个文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。
    • 修改文件。在容器中修改已存在的文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。
    • 删除文件。在容器中删除文件时,Docker也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。
  5. 镜像层,每一层都有一个唯一ID(64个十六进制字符组成)

  6. 查看镜像分层docker history <image name>

    1. 过长的命令会被截断,可以通过--no-trunc选项输出完整内容
    $ docker history  registry.cn-beijing.aliyuncs.com/jannal/redis:3.2.11  
    IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
    ca38546c0a34        15 months ago       /bin/sh -c #(nop)  ENTRYPOINT ["/usr/local/r…   0B                  
    9e0e95755600        15 months ago       /bin/sh -c #(nop)  EXPOSE 6379                  0B                  
    1c2134a5ba1c        15 months ago       /bin/sh -c #(nop) COPY dir:eb15290162d71ca65…   46.8kB              
    42d9364d31e3        15 months ago       /bin/sh -c rpm --rebuilddb && yum install -y…   327MB               
    a060b36326fa        15 months ago       /bin/sh -c #(nop) COPY file:dacb9220d8a6df7d…   145B                
    f6f092ab0d22        15 months ago       /bin/sh -c #(nop) COPY file:b39aa050b18d2600…   8.76MB              
    2fa5ec20e350        15 months ago       /bin/sh -c #(nop) COPY file:0b0b73e2e83666e4…   1.55MB              
    46d83096cab0        15 months ago       /bin/sh -c #(nop)  MAINTAINER jannal <jannal…   0B                  
    f7dcf0d2d225        15 months ago       /bin/sh -c #(nop)  CMD ["/usr/sbin/sshd" "-D…   0B                  
    b28273dbdbf0        15 months ago       /bin/sh -c #(nop)  EXPOSE 22                    0B                  
    15dc202ffc27        15 months ago       /bin/sh -c yum makecache     && yum install …   19.3MB              
    260b40d7cdd0        15 months ago       /bin/sh -c #(nop)  MAINTAINER jannal <jannal…   0B                  
    368c96d786ae        2 years ago         /bin/sh -c #(nop) ADD file:323d96a96f4ecb68e…   203MB               
    <missing>           2 years ago         /bin/sh -c #(nop)  MAINTAINER The CentOS Pro…   0B 
    

构建镜像

  1. 创建镜像的主要方式
    • 基于已有的镜像的容器创建(不推荐,无法审计,无法知道如何创建的)
    • 基于本地模板导入
    • 基于Dockerfile创建

基于已有的容器

  1. 使用docker commit创建新镜像的步骤

    • 运行容器
    • 修改容器
    • 提交修改,保存为新镜像
  2. 命令格式

    $ docker commit [选项] [容器ID或者名称] [仓库[:TAG]]
    	-a,--author=""    作者信息
    	-m,--message=""   提价信息
    	-p,--pause=true   提交时暂停容器运行
    
  3. 实例

    1. 查看镜像
    $ docker images
    REPOSITORY      TAG    IMAGE ID       CREATED     SIZE
    redis        latest  b77605993f64   6 days ago  105.9 MB
    
    2. 启动并进入一个容器
    $	docker run -it redis:b77605993f64 /bin/bash
    root@59334364d705:/data# touch test.xt    创建一个测试文件(记录容器的ID 59334364d705) 
    root@59334364d705:/data# exit             退出容器
    
    3. 提交镜像,如果成功会返回一个新创建镜像ID
    $ docker commit -m "增加一个test.xt文件"  -a "jannal" 59334364d705 redis-jannal 
    557913e17f65da58bc7f7fd91d13281343147d939d31057582fb56211c9144f0
    
    4. 查看本地镜像列表
    $ docker images
    REPOSITORY      TAG         IMAGE ID       CREATED         SIZE
    redis-jannal    latest      557913e17f65   2 minutes ago   105.9 MB
    redis           latest      b77605993f64   6 days ago      105.9 MB
    

基于本地模板导入

  1. 模板下载地址 https://openvz.org/Download/templates/precreated
  2. cat 模板 | docker import - 名字

基于Dockerfile

  1. Dockerfile 是一个文本格式的配置文件,用户可以使用Dockerfile快速创建自定义的镜像

  2. Dockerfile 由一行行命令语句组成,并#开头注释行

  3. Dockerfile 分为四个部分

    • 基础镜像信息:以哪个镜像作为基础进行制作,FROM 基础镜像名称
    • 维护者信息:需要写下该Dockerfile编写人的姓名或邮箱,用法是 MANITAINER 名字/邮箱
    • 镜像操作指令: 对基础镜像要进行的改造命令,比如安装新的软件,进行哪些特殊配置等,常见的是RUN 命令
    • 容器启动命令: 当基于该镜像的容器启动时需要执行哪些命令,常见的是CMD 命令或ENTRYPOINT
  4. 命令

    $ docker build -t <镜像名> -f <dockerfile文件名> . 
    	-t:将镜像命名为xxx
      -f:指定Dockerfile的位置,Docker默认会从build context中查找Dockerfile文件
      .: 指明build context为当前目录。
      --no-cache:不使用缓存
    
  5. 构建过程:Docker将build context中的所有文件发送给Docker daemon(所以不要将无关的文件或目录放置到build context中,否则可能构建非常缓慢)。build context为镜像构建提供所需要的文件或目录。Dockerfile中的ADDCOPY等命令可以将build context中的文件添加到镜像

    • 从base镜像运行一个容器
    • 执行一条指令,对容器做修改
    • 执行类似docker commit的操作,生成一个新的镜像层
    • Docker再基于刚刚提交的镜像运行一个新容器。
    • 重复2~4步,直到Dockerfile中的所有指令执行完毕
  6. Docker会缓存已有镜像的镜像层,构建新镜像或者下载镜像,如果某镜像层已经存在,就直接使用,无须重新创建。如果希望在构建镜像时不使用缓存,可以在docker build命令中加上--no-cache参数。Dockerfile中每一个指令都会创建一个镜像层,上层是依赖于下层的。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。即如果改变Dockerfile指令的执行顺序,或者修改或添加指令,都会使缓存失效。

  7. .dockerignore文件来让Docker忽略路径下的目录和文件,在创建镜像时候不将无关数据发送到服务端。

    dockerignore文件中模式语法支持Golang风格的路径正则格式:
    *表示任意多个字符;
    ?代表单个字符;
    !表示不匹配(即不忽略指定的路径或文件)。
    

Dockerfile 详解

  1. 模板,这里是自定义zk的Dockerfile

    # DOCKER-VERSION     18.03.1-ce-mac65
    # 第一行必须指定基于的基础镜像
    FROM registry.cn-beijing.aliyuncs.com/jannal/centos6.6-jdk8:1.0.0
    # 维护者信息
    MAINTAINER jannal <jannals@126.com>
    # 环境变量
    ENV ZK_VERISON 3.4.6
    ENV ZOOKEEPER_HOME /usr/local/zookeeper-${ZK_VERISON}
    # 复制文件
    COPY zookeeper-${ZK_VERISON}.tar.gz /root/install/
    # 工作目录
    WORKDIR  /root/install/
    
    RUN tar -zxvf zookeeper-${ZK_VERISON}.tar.gz -C /usr/local/ \
        && rm -rf zookeeper-${ZK_VERISON}.tar.gz  \
        && rm -rf $ZOOKEEPER_HOME/bin/*.cmd  \
        && rm -rf $ZOOKEEPER_HOME/dist-maven  \
        && rm -rf $ZOOKEEPER_HOME/docs  \
        && rm -rf $ZOOKEEPER_HOME/src \
        && rm -rf /root/install/* \
        && mkdir $ZOOKEEPER_HOME/data $ZOOKEEPER_HOME/logs \
        && mv /usr/local/zookeeper-${ZK_VERISON}/conf/zoo_sample.cfg /usr/local/zookeeper-${ZK_VERISON}/conf/zoo.cfg \
        && /usr/local/zookeeper-${ZK_VERISON}/bin/zkServer.sh start /usr/local/zookeeper-${ZK_VERISON}/conf/zoo.cfg
    # 端口暴露
    EXPOSE 22 2181 2888 3888
    # 覆盖上层执行命令
    CMD ["/usr/local/zookeeper-3.4.6/bin/zkServer.sh","start-foreground","/usr/local/zookeeper-3.4.6/conf/zoo.cfg"]
    

常用指令

  1. 指令说明

    image-20210919224358493
  2. ARG:定义创建镜像过程中使用的变量。在执行docker build时,可以通过-build-arg[=]来为变量赋值。当镜像编译成功后,ARG指定的变量将不再存在。Docker内置了一些镜像创建变量,用户可以直接使用而无须声明,包括(不区分大小写)HTTP_PROXYHTTPS_PROXYFTP_PROXYNO_PROXY

  3. FROM:格式为FROM <image> 或者FROM <image>:<tag>。如果在同一个dockerfile中创建多个镜像时,可以使用多个FROM指令(每个镜像一次)

  4. MAINTAINER:MAINTAINER 指定维护者信息

  5. RUN

    • RUN <command> 将在shell终端中运行命令,即/bin/sh -c
    • RUN ["executable" ,"param1","param2"] 使用exec执行。指定使用其他终端可以通过RUN ["/bin/bash","-c","echo hello"]
    • 每条RUN指令将在当前镜像基础上执行指定的命令,并提交为新的镜像,当命令较长时可以使用\来执行
    • 每次在Dockerfile中执行RUN命令的时候系统都会在镜像中新建一个层,每个镜像层都会占用一定的磁盘空间。因此为了尽量减少镜像的层数,最好把移动、提取、删除等所有文件操作都写在同一行RUN命令下
  6. CMD:允许用户指定容器默认的执行命令,此命令会在容器启动且docker run没有指定其他命令时运行

    • CMD ["exec","param1","param2"]使用exec格式执行。当指令执行时,会直接调用 [command],不会被shell解析。CMD和ENTRYPOINT推荐使用Exec格式,因为指令可读性更强,更容易理解

      ENV name Jannal
      ENTRYPOINT ["/bin/echo", "Hello, $name"]
      
      输出:Hello, $name,即环境变量name没有被替换。如果想让环境变量被替换
      
      ENV name Jannal
      ENTRYPOINT ["/bin/sh", "-c", "echo Hello, $name"]
      输出:Hello, Jannal
      
    • CMD command param1 param2 使用shell格式。shell格式底层会调用 /bin/sh -c [command]

      ENV name Jannal
      ENTRYPOINT echo "Hello, $name"
      
      输出:Hello, Jannal。即环境变量被替换了
      
    • CMD ["param1","param2"]提供给ENTRYPOINT的默认参数。此时ENTRYPOINT必须使用Exec格式。

    • 指定启动容器时执行的命令,每个Dockerfile只能有一条CMD命令,如果指定多条命令,只有最后一条会被执行

    • 如果用户启动容器时候指定了运行的命令,则会覆盖CMD指定的命令

  7. ENTRYPOINT:设置容器启动时运行的命令。可让容器以应用程序或者服务的形式运行

    • 可以有多个ENTRYPOINT指令,但只有最后一个生效

    • CMD或docker run之后的参数会被当作参数传递给ENTRYPOINT。

    • ENTRYPOINT看上去与CMD很像,它们都可以指定要执行的命令及其参数。不同的地方在于ENTRYPOINT不会被忽略,一定会被执行,即使运行docker run时指定了其他命令。

    • ENTRYPOINT ["executable", "param1", "param2"]这是Exec格式。ENTRYPOINT的Exec格式用于设置要执行的命令及其参数,同时可通过CMD提供额外的参数。ENTRYPOINT中的参数始终会被使用,而CMD的额外参数可以在容器启动时动态替换掉。

      ENTRYPOINT ["/bin/echo", "Hello"] 
      CMD ["world"]
      当容器通过docker run -it [image]启动时,输出为:
      Hello world
      通过docker run -it [image] Jannal启动,则输出为:
      Hello Jannal
      
    • ENTRYPOINT command param1 param2这是shell格式。ENTRYPOINT的Shell格式会忽略任何CMD或docker run提供的参数

  8. ADD:将文件从build context复制到镜像。

    • ADD <src> <dest>
    • 该命令将复制指定的<src>到容器中的<dest>.其中<src>可以是Dockerfile所在目录的一个相对路径(文件或目录)。也可以是URL,还可以是一个tar(自动解压为目录)
  9. COPY:将文件从build context复制到镜像。

    • COPY <src> <dest> 或者COPY ["src","dest"]src只能指定build context中的文件或目录。
    • 复制本地主机<src>(为Dockerfile所在目录的相对路径,文件或者目录)为容器中的<dest>目标路径不存在时会自动创建
    • 当使用本地目录为源目录时,推荐使用COPY
  10. EXPOSE

    • EXPOSE <port> [<port> ...]
    • 告诉docker 服务器端容器暴露的端口号
  11. ENV:ENV <key> <value> 指定一个环境变量,可以被后续RUN指令使用,并在容器运行时保持

  12. VOLUME:将文件或目录声明为volume

  13. WORKDIR:为后面的RUN、CMD、ENTRYPOINT、ADD或COPY指令设置镜像中的当前工作目录

最佳实践

  1. 使用RUN指令安装应用和软件包,构建镜像。多个RUN使用&&连接,以节省空间
  2. 如果Docker镜像的用途是运行应用程序或服务,比如运行一个MySQL,应该优先使用Exec格式的ENTRYPOINT指令。CMD可为ENTRYPOINT提供额外的默认参数,同时可利用docker run命令行替换默认参数。
  3. 如果想为容器设置默认的启动命令,可使用CMD指令。用户可在docker run命令行中替换此默认命令。
  4. 恰当使用多步骤构建:通过多步骤创建,可以将编译和运行等过程分开,保证最终生成的镜像只包括运行应用所需要的最小化环境。
  5. 调整合理的指令顺序:在开启cache的情况下,内容不变的指令尽量放在前面,这样可以尽量复用;

Dockerfile调试

  1. 从构建过程可以看出,即使Dockfile由于某种原因某个指令失败了,也将能够得到前一个指令成功执行构建出的镜像

方式一

  1. 测试Dockerfile

    FROM alpine
    RUN touch test.txt
    RUN sh -c echo "hello"
    RUN java -version
    
  2. 运行

    $ docker build -t test-debug .
    
     docker build -t test-debug .
    Sending build context to Docker daemon  2.048kB
    Step 1/4 : FROM alpine
     ---> a24bb4013296
    Step 2/4 : RUN touch test.txt
     ---> Using cache
     ---> 1173fde4f4ac
    Step 3/4 : RUN sh -c echo "hello"
     ---> Using cache
     ---> 22fc2249c339
    Step 4/4 : RUN java -version
     ---> Running in f57e9a6683d0
    /bin/sh: java: not found
    The command '/bin/sh -c java -version' returned a non-zero code: 127
    
  3. 运行上一步生成的镜像(22fc2249c339)的容器进行调试

    $ docker run -it 22fc2249c339
    / # java -version
    /bin/sh: java: not found
    

方式二

  1. https://jeremyxu2010.github.io/2019/04/巧妙调试docker容器/

多步骤构建

  1. 自17.05版本开始,Docker支持多步骤镜像创建(Multi-stage build)特性,可以精简最终生成的镜像大小。
  2. 对于需要编译的应用(如C、Go或Java语言等)来说,通常情况下至少需要准备两个环境的Docker镜像:
    • 编译环境镜像:包括完整的编译引擎、依赖库等,往往比较庞大。作用是编译应用为二进制文件
    • 运行环境镜像:利用编译好的二进制文件,运行应用,由于不需要编译环境,体积比较小

镜像导出与导入

  1. 存出
    • 如果导出镜像到本地文件,可以使用docker save
    • docker save -o redis.tar redis:latest
    • 可以在当前目录ls查看镜像tar
  2. 存入
    • docker load从存出的本地文件中导入本地镜像库
    • docker load --input redis.tar或者 docker load < redis.tar

常见镜像

  1. Busybox:集成一百多个最常用的linux命令的精简工具箱,只有不到2 MB大小,被誉为Linux系统的瑞士军刀https://busybox.net/
  2. Alpine:Alpine操作系统是一个面向安全的轻型Linux发行版,关注安全,性能和资源效能。不同于其他发行版,Alpine采用了musl libc和BusyBox以减小系统的体积和运行时资源消耗,比BusyBox功能上更完善。在保持瘦身的同时,Alpine还提供了包管理工具apk查询和安装软件包。相比于其他镜像,它的容量非常小,仅仅只有5 MB左右

posted on 2021-12-26 11:12  jannal  阅读(77)  评论(0编辑  收藏  举报