在继续深入了解 Docker 的时候,有个先决条件。要先了解 Docker 镜像、容器、仓库三大核心概念。
文件系统
对于 Docker 来说,镜像是用来启动容器的基础,是容器的基础运行系统,包含了运行容器所需要的所有文件和配置,有了镜像才能启动容器。但是对于宿主机来说,镜像就是一个文件,一个文件系统的快照。
Docker 使用联合文件系统(UnionFS)来制作镜像文件,目前支持的联合文件系统有 AUFS、overlay、overlay2、DeviceMapper、VSF等。我们先以 overlay2 为例来看看镜像的文件系统,原理都是大致相同的。
可以看下自己当前的环境内使用的联合文件系统是哪个:
docker info | grep -i "storage driver"
overlay2存储文件的方式:将镜像层和容器层都放在单独的目录,并且有唯一 ID,每一层仅存储发生变化的文件,最终使用联合挂载技术将容器层和镜像层的所有文件统一挂载到容器中,使得容器中看到完整的系统文件。
使用 docker inspect 查看一下镜像的信息,删选出来 Data 字段。
docker inspect hello-world | grep -A 4 Data
从镜像中可以看到在 Data 下面分别有三个目录信息:
- MergedDir 实际上这一层目录对于镜像来说是空的。
- UpperDir 存储了容器层的文件,当容器内部对文件进行修改时,会在这一层进行修改。
- WorkDir UpperDir 的中间层,外部有操作时先在这一层进行操作,后续再合并到 UpperDir 中。
目录信息还包含了一个额外的信息,这三个文件夹都在一个共同的目录下,这个目录名称就是这个镜像的 ID。
之后我们启动一个名为 hello-world-container 的容器再来看下容器的文件系统。
docker run --name hello-world-container -d hello-world
docker inspect hello-world-container | grep -A 5 Data
这个时候可以发现容器的目录信息里多了 LowerDir。并且 LowerDir 的目录直接指向到镜像文件的 UpperDir。这里就要说下 LowerDir 这个目录了,在 Docker 的定义中 LowerDir 为只读的层,容器的 MergedDir 会先从 UpperDir 获取文件,如果 UpperDir 中没有文件,再从 LowerDir 中获取文件。
这里借用一张图片就可以很清楚看到这三个目录之间的关系,而 UpperDir 同样也只是作为一个中间目录,在前面进行理解的时候可以适当忽略掉。
在容器的文件系统中,LowerDir 直接引用的镜像的文件地址并且不可修改,其他三个目录则是挂载在容器镜像的 ID 对应的文件夹下面,所有的写入更改才做都在此进行,这样就实现了容器内部和镜像原本文件的隔离,保留了原始镜像文件的完整性。
镜像构建
Docker 使用 Dockerfile 来构建镜像,实际上来说 Dockerfile 就是一种被 Docker 来运行的脚本,每一行都是一个单独的命令。
先看一下 Dockerfile 内常用的一些内部指令。
- FROM:在整个dockfile文件中,除了解析器指令、注释和全局范围的ARG之后之外的第一行应该就是FROM,用于指定父镜像
- ADD:用于添加宿主机本地的文件、目录、压缩等资源到镜像里面去,会自动解压tar.gz格式的压缩包,不会自动解压zip
- COPY:用于添加宿主机本地的文件、目录、压缩等资源到镜像里面去,不会解压任何压缩包
- ENV:设置容器环境变量,该值将存在于构建阶段中所有后续指令的环境中,并且也可以在许多指令中内联替换该值将被解释为其他环境变量
- USER:指定运行操作的用户,之后容器启动时运行的程序将会使用设置的用户运行
- VOLUME:定义volume,也就是创建目录
- WORKDIR:用于定义工作目录,也就是容器镜像运行的目录,也是进入容器终端的默认目录
- RUN:执行shell命令,但是一定要以非交互式的方式执行
- MAINTAINER:镜像的作者信息
- LABEL:设置镜像的属性标签
- EXPOSE:设置容器在运行时监听指定的网络端口。您可以指定端口是侦听TCP还是UDP,如果不指定协议,则默认为TCP,实际上并未发布端口,他提供运维人员一个类似与标识的内容
- CMD:镜像启动为一个容器时候的默认命令,空格需要使用,隔开,如:CMD [“/bin/bash”,“sleep”,“30000”]
- ENTRYPOINT:也可以用于定义容器在启动时候默认执行的命令或者脚本,如果是和CMD命令混合使用的时候,会将CMD的命令当做参数传递给ENTRYPOINT后面的脚本,可以在脚本中对参数做判断并相应的容器初始化操作
下面直接列出一些代码片段来实际看下这些用途。
# 使用 Debian 基础镜像
FROM debian
# 实际使用的时候可以使用多个 FROM 指令,在后续的构建过程中 COPY from 甚至可以指定从哪个阶段的镜像中拷贝文件
FROM debian
<...some code>
FROM debian
COPY --from=0 /app/hello /app/hello
# COPY from 不光可以指定从哪个阶段的镜像中拷贝文件,还可以配合 as 指定阶段的别名,使用别名来进行引用。
FROM debian as stage1
<...some code>
FROM debian as stage2
COPY --from=stage1 /app/hello /app/hello
ADD 和 COPY 两个命令做的事非常的相似,使用上略微有些区别。
# COPY 和 ADD 都只能复制上下文包含的内容到镜像内
COPY test.file .
ADD test.flie .
# 可以使用通配符来匹配文件
COPY test* .
ADD test* .
# 但是只有 COPY 可以指定构建阶段来复制文件
COPY --from=0 /app/hello /app/hello
# 只有 ADD 可以复制远程在线文件,并且可以直接解压文件。例如下面得到的文件是解压之后的。
# 但是官方不推荐这种用法,会导致镜像分层变多。还有镜像内也会包含原始的压缩文件,导致镜像变大。
# 一般来说,如果需要远程文件,可以在 Dockerfile 中使用 wget 或者 curl 命令来下载文件。
ADD http://example.com/big.tar.xz /usr/src/things/
ENV 命令用于设置环境变量,这些变量将在构建阶段中所有后续指令的环境中存在,并且在许多指令中内联替换。
# 指定了 MY_NAME 的环境变量,在后面可以使用 $MY_NAME 来引用这个变量
ENV MY_NAME="John Doe"
USER 命令用于指定运行操作的用户,之后容器启动时运行的程序将会使用设置的用户运行。
# 指定了运行容器的用户为 nobody,这个用户必须是创建好的。实际的操作只是切换了用户,但是并不会创建用户。
USER nobody
VOLUME 可以指定容器启动的自动挂载一个匿名的存储券,只是一个默认缺省值,可以在容器启动的时候进行覆盖。默认挂载有一个好处,其他容器可以直接通过 volumes-from 来共享存储目录。
# 挂载外部存储券
VALUME ["/data/","/test/"]
VOLUME "/data"
MAINTAINER 以及 LABEL 就是单纯的用来声明镜像的作者信息和镜像的属性标签。
# 声明所有者
MAINTAINER ioiox
# LABEL 可以设置镜像的属性标签。设置多个标签的时候建议放在一行,构建的时候可以减少镜像分层。
LABEL version="1.0" description="This is a description" author="ioiox"
EXPOSE 命令用于设置容器在运行时监听指定的网络端口。您可以指定端口是侦听TCP还是UDP,如果不指定协议,则默认为TCP。EXPOSE 更多是作为一个标识,告诉使用者内部需要映射的端口。或者使用 docker -P 可以自动映射 EXPOSE 的端口。
# 指定容器监听的端口 tcp 端口
EXPOSE 80/tcp
# 指定容器监听的端口 udp 端口
EXPOSE 1443/udp
CMD 和 ENTRYPOINT 两个命令都是用来设置容器启动时候的默认命令,但是两者有一些区别。
# CMD 用于设置容器启动时候的默认命令,如果在 docker run 的时候指定了命令,那么 CMD 的命令会被覆盖。
CMD ["/bin/bash","sleep","30000"]
# ENTRYPOINT 也可以用于定义容器在启动时候默认执行的命令或者脚本,如果是和CMD命令混合使用的时候,会将CMD的命令当做参数传递给ENTRYPOINT后面的脚本。
ENTRYPOINT ["/bin/bash","-c"]
CMD ["sleep","30000"]
总结
这篇文章只是浅谈了 Docker 镜像的一些基础概念,以及如何使用 Dockerfile 来构建一个镜像。先给大家提供一个基础的概念,提供一个眼熟,在实际使用中可以针对性的继续深入了解。