Docker之镜像
镜像简介
-
基础镜像(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"]
-
为什么基础镜像发行版(比如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差别不大
-
-
基础镜像(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).
镜像分层
-
Docker的镜像是从基础镜像一层层叠加生成的。分层最大的好处就是共享资源
- 有多个镜像都从相同的base镜像构建而来,那么Docker Host只需在磁盘上保存一份base镜像;同时内存中也只需加载一份base镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享
- 当某个容器修改了基础镜像的内容,比如 /etc下的文件,这时其他容器的 /etc是否不会被修改。修改只会被限制在单个容器中
-
当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作容器层,容器层之下的都叫镜像层。所有对容器的改动,无论添加、删除,还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的
-
所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如
/etc
,上层的/etc
会覆盖下层的/etc
,也就是说用户只能访问到上层中的文件/etc
。在容器层中,用户看到的是一个叠加之后的文件系统。 -
容器层(Copy-on-Write特性):保存的是镜像变化的部分,不会对镜像做任何修改
- 添加文件。在容器中创建文件时,新文件被添加到容器层中。
- 读取文件。在容器中读取某个文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。
- 修改文件。在容器中修改已存在的文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。
- 删除文件。在容器中删除文件时,Docker也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。
-
镜像层,每一层都有一个唯一ID(64个十六进制字符组成)
-
查看镜像分层
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
构建镜像
- 创建镜像的主要方式
- 基于已有的镜像的容器创建(不推荐,无法审计,无法知道如何创建的)
- 基于本地模板导入
- 基于Dockerfile创建
基于已有的容器
-
使用
docker commit
创建新镜像的步骤- 运行容器
- 修改容器
- 提交修改,保存为新镜像
-
命令格式
$ docker commit [选项] [容器ID或者名称] [仓库[:TAG]] -a,--author="" 作者信息 -m,--message="" 提价信息 -p,--pause=true 提交时暂停容器运行
-
实例
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
基于本地模板导入
- 模板下载地址 https://openvz.org/Download/templates/precreated
cat 模板 | docker import - 名字
基于Dockerfile
-
Dockerfile 是一个文本格式的配置文件,用户可以使用Dockerfile快速创建自定义的镜像
-
Dockerfile 由一行行命令语句组成,并
#
开头注释行 -
Dockerfile 分为四个部分
- 基础镜像信息:以哪个镜像作为基础进行制作,
FROM 基础镜像名称
- 维护者信息:需要写下该Dockerfile编写人的姓名或邮箱,用法是
MANITAINER 名字/邮箱
- 镜像操作指令: 对基础镜像要进行的改造命令,比如安装新的软件,进行哪些特殊配置等,常见的是RUN 命令
- 容器启动命令: 当基于该镜像的容器启动时需要执行哪些命令,常见的是
CMD
命令或ENTRYPOINT
- 基础镜像信息:以哪个镜像作为基础进行制作,
-
命令
$ docker build -t <镜像名> -f <dockerfile文件名> . -t:将镜像命名为xxx -f:指定Dockerfile的位置,Docker默认会从build context中查找Dockerfile文件 .: 指明build context为当前目录。 --no-cache:不使用缓存
-
构建过程:Docker将build context中的所有文件发送给Docker daemon(所以不要将无关的文件或目录放置到build context中,否则可能构建非常缓慢)。build context为镜像构建提供所需要的文件或目录。Dockerfile中的
ADD
、COPY
等命令可以将build context中的文件添加到镜像- 从base镜像运行一个容器
- 执行一条指令,对容器做修改
- 执行类似docker commit的操作,生成一个新的镜像层
- Docker再基于刚刚提交的镜像运行一个新容器。
- 重复2~4步,直到Dockerfile中的所有指令执行完毕
-
Docker会缓存已有镜像的镜像层,构建新镜像或者下载镜像,如果某镜像层已经存在,就直接使用,无须重新创建。如果希望在构建镜像时不使用缓存,可以在docker build命令中加上
--no-cache
参数。Dockerfile中每一个指令都会创建一个镜像层,上层是依赖于下层的。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。即如果改变Dockerfile指令的执行顺序,或者修改或添加指令,都会使缓存失效。 -
.dockerignore
文件来让Docker忽略路径下的目录和文件,在创建镜像时候不将无关数据发送到服务端。dockerignore文件中模式语法支持Golang风格的路径正则格式: *表示任意多个字符; ?代表单个字符; !表示不匹配(即不忽略指定的路径或文件)。
Dockerfile 详解
-
模板,这里是自定义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"]
常用指令
-
指令说明
-
ARG:定义创建镜像过程中使用的变量。在执行docker build时,可以通过-build-arg[=]来为变量赋值。当镜像编译成功后,ARG指定的变量将不再存在。Docker内置了一些镜像创建变量,用户可以直接使用而无须声明,包括(不区分大小写)
HTTP_PROXY
、HTTPS_PROXY
、FTP_PROXY
、NO_PROXY
。 -
FROM
:格式为FROM <image>
或者FROM <image>:<tag>
。如果在同一个dockerfile中创建多个镜像时,可以使用多个FROM指令(每个镜像一次) -
MAINTAINER
:MAINTAINER指定维护者信息 -
RUN
RUN <command>
将在shell终端中运行命令,即/bin/sh -c
RUN ["executable" ,"param1","param2"]
使用exec执行。指定使用其他终端可以通过RUN ["/bin/bash","-c","echo hello"]
- 每条RUN指令将在当前镜像基础上执行指定的命令,并提交为新的镜像,当命令较长时可以使用
\
来执行 - 每次在Dockerfile中执行RUN命令的时候系统都会在镜像中新建一个层,每个镜像层都会占用一定的磁盘空间。因此为了尽量减少镜像的层数,最好把移动、提取、删除等所有文件操作都写在同一行RUN命令下
-
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指定的命令
-
-
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提供的参数
-
-
ADD
:将文件从build context复制到镜像。ADD <src> <dest>
- 该命令将复制指定的
<src>
到容器中的<dest>
.其中<src>
可以是Dockerfile所在目录的一个相对路径(文件或目录)。也可以是URL,还可以是一个tar(自动解压为目录)
-
COPY
:将文件从build context复制到镜像。COPY <src> <dest>
或者COPY ["src","dest"]
。src
只能指定build context中的文件或目录。- 复制本地主机
<src>
(为Dockerfile所在目录的相对路径,文件或者目录)为容器中的<dest>
。目标路径不存在时会自动创建 - 当使用本地目录为源目录时,推荐使用COPY
-
EXPOSE
EXPOSE <port> [<port> ...]
- 告诉docker 服务器端容器暴露的端口号
-
ENV
:ENV <key> <value>
指定一个环境变量,可以被后续RUN指令使用,并在容器运行时保持 -
VOLUME
:将文件或目录声明为volume -
WORKDIR
:为后面的RUN、CMD、ENTRYPOINT、ADD或COPY指令设置镜像中的当前工作目录
最佳实践
- 使用RUN指令安装应用和软件包,构建镜像。多个RUN使用
&&
连接,以节省空间 - 如果Docker镜像的用途是运行应用程序或服务,比如运行一个MySQL,应该优先使用Exec格式的ENTRYPOINT指令。CMD可为ENTRYPOINT提供额外的默认参数,同时可利用docker run命令行替换默认参数。
- 如果想为容器设置默认的启动命令,可使用CMD指令。用户可在docker run命令行中替换此默认命令。
- 恰当使用多步骤构建:通过多步骤创建,可以将编译和运行等过程分开,保证最终生成的镜像只包括运行应用所需要的最小化环境。
- 调整合理的指令顺序:在开启cache的情况下,内容不变的指令尽量放在前面,这样可以尽量复用;
Dockerfile调试
- 从构建过程可以看出,即使Dockfile由于某种原因某个指令失败了,也将能够得到前一个指令成功执行构建出的镜像
方式一
-
测试Dockerfile
FROM alpine RUN touch test.txt RUN sh -c echo "hello" RUN java -version
-
运行
$ 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
-
运行上一步生成的镜像(
22fc2249c339
)的容器进行调试$ docker run -it 22fc2249c339 / # java -version /bin/sh: java: not found
方式二
多步骤构建
- 自17.05版本开始,Docker支持多步骤镜像创建(Multi-stage build)特性,可以精简最终生成的镜像大小。
- 对于需要编译的应用(如C、Go或Java语言等)来说,通常情况下至少需要准备两个环境的Docker镜像:
- 编译环境镜像:包括完整的编译引擎、依赖库等,往往比较庞大。作用是编译应用为二进制文件
- 运行环境镜像:利用编译好的二进制文件,运行应用,由于不需要编译环境,体积比较小
镜像导出与导入
- 存出
- 如果导出镜像到本地文件,可以使用
docker save
docker save -o redis.tar redis:latest
- 可以在当前目录ls查看镜像tar
- 如果导出镜像到本地文件,可以使用
- 存入
docker load
从存出的本地文件中导入本地镜像库docker load --input redis.tar
或者docker load < redis.tar
常见镜像
- Busybox:集成一百多个最常用的linux命令的精简工具箱,只有不到2 MB大小,被誉为Linux系统的瑞士军刀。https://busybox.net/
- Alpine:Alpine操作系统是一个面向安全的轻型Linux发行版,关注安全,性能和资源效能。不同于其他发行版,Alpine采用了musl libc和BusyBox以减小系统的体积和运行时资源消耗,比BusyBox功能上更完善。在保持瘦身的同时,Alpine还提供了包管理工具apk查询和安装软件包。相比于其他镜像,它的容量非常小,仅仅只有5 MB左右