概述

容器技术对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此称为容器。Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。

主要目标:通过对应用组件的封装、分发、部署、运行等生命周期的管理,达到应用级别的一次封装,到处运行。

现在基本上所有的开发人员都需要与 docker 打交道,交付方式大多也都是交付 docker 镜像,现代化的开发流程大致如下

image-20220118101753400

可以说,现在还不会 docker 的开发,不是一个好开发

基本概念

组成

Docker 的世界里主要由三部分组成:

  • 镜像:Image
  • 容器:Container
  • 仓库:Repository

这三个概念贯穿了 Docker 的生命周期,理解了这三个概念,就更容器理清 Docker 的生命周期

镜像

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

Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。 镜像实际是由多层文件系统联合组成。

镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。

比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。

在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。

image-20220122141833113

容器

镜像和容器的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以通过 Docker API 创建、启动、停止、删除、暂停等。

在默认情况下,容器与其它容器及其主机是隔离的,拥有自己的独立进程空间、网络配置。

容器由其镜像以及在创建或启动容器时提供的任何配置选项定义。当容器被删除时,对其状态的任何未存储在持久存储中的更改都会消失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。

容器 = 镜像 + 读写层

  • 容器用来运行程序,是读写层
  • 镜像用来安装程序,是制度层

仓库

仓库是集中存放镜像文件的场所。

有时候狐白仓库和仓库注册服务器(Registry)混为一谈,并不严格区分。实际上,仓库注册服务器上往往存放着多个仓库,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag)。

架构

image-20220122142041973

中间部位为我们进行 Docker 操作的宿主机,其运行了一个 Docker daemon 的核心守护程序,负责构建、运行和分发 Docker 容器。

左边为 Docker 客户端,其与 Docker 守护进程进行通信,客户端会将 build、pull、run 命令发送到 Docker 守护进程进行执行。

右边为 Docler 注册表存储 Docker 镜像,是一个所有 Docker 用户共享 Docker 镜像的服务,Docker daemon 与之进行交互。

镜像操作

docker 的镜像来源大致有这几种:仓库拉取、本地构建、导入。

镜像拉取

docker 的仓库拉取命令格式如下

$ docker pull [仓库地址[:端口]/]仓库名[tag]
  • 仓库地址可以省略,默认地址是 docker hub;
  • 仓库名分为两部分,用户名+镜像名,用户名可以省略,默认为library,也就是官方镜像;
  • 标签默认为 latest,即最新版本,可以根据需要指定哪个版本

查看镜像

  • 使用docker images 查看全部镜像,docker image ls 效果相同,;

  • 使用 docker inspect <image-name|image-id>查看镜像数据;

  • 可以使用-f 参数查看镜像的指定属性,例如

    $ docker inspect -f {{.Metadata}} nginx
    

    双大括号之内的属性要在信息中存在,注意.不要忽略;

    image-20220206143218086

  • 使用 docker system df 查看镜像占用的磁盘空间;

    image-20220206143852358

  • 使用docker image ls -f dangling=true查看所有的虚悬镜像,由于虚悬镜像没有用处,因此可以随意删除docker image prune

虚悬镜像:由于新旧镜像重名,旧镜像名称被取消,从而出现仓库名、标签名均为 <none> 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image)

删除镜像

使用 docker rmi [选项] <image-id> <image-id>来删除镜像,image-id 可以是长 id、短 id、镜像摘要或者镜像名

image-20220208103315497

制作镜像

使用 docker build 命令手动制作镜像,docker 会根据指定的路径中的 dockerfile 来制作镜像

常用构建命令:docker build -t image-name:version ..表示当前路径下,也可以手动指定 Dockerfile 文件,docker build -f /path/to/Dockerfile

传输镜像

制作完镜像之后可以推送到仓库,也可已通过 tar 包来进行传输

仓库

向仓库推送之前需要将镜像打 tag

$ docker tag image-name username/image-name:version

# 如果没有登录 docker 需要先登录
$ docker login

# 推送镜像
$ docker push username/image-name:version

手动传输

先将镜像保存为 tar 包(或者其他包类型),然后通过 scp 等工具将 tar 包传输到其他设备,然后加载 tar 包

PS:save 和 load 操作都是相对于当前目录

# 导出镜像
$ docker save image-name > filename
# or
$ docker save image-name -o filename

# 传输镜像包
$ scp filename user@address[:path]
# 例如
$ scp nginx.tar root@10.10.10.10:/ssd
# 然后输入目标终端对应用户的密码即可

# 加载镜像
$ docker load < nginx.tar

Dockerfile 脚本

Dockerfile 分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

部分 指令
基础镜像信息 FROM
维护者信息 MAINTAINER
镜像操作指令 RUN、COPY、ADD、EXPOSE 等
容器启动时执行指令 CMD、ENTRYPOINT

注意,每个指令独立运行,并导致创建一个新的 Image,因此 RUN cd / tmp 对下一个指令不会有任何影响,如果想要移动目录可以使用&连接命令,RUN cd /tmp & ls

环境变量

环境变量(使用 ENV 语句声明)也可以在某些指令中用作要由 Dockerfile 解释的变量。还可以处理转义,以将类似变量包含在语句中。

环境变量在 Dockefile 中用 $variable_name${variable_name} 表示。它们被等同对待,并且括号语法通常用于解决不带空格的变量名的问题,例如 ${foo}_bar

${variable_name} 语法还支持以下指定的一些标准 bash 修饰符:

  • ${variable:-word} 表示如果设置了 variable,则结果将是该值。如果 variable 未设置,那么 word 将是结果。
  • ${variable:+word} 表示如果设置了 variable,那么 word 将是结果,否则结果是空字符串

在所有情况下,word 可以是任何字符串,包括额外的环境变量。

可以通过在变量之前添加 \ 来转义:\$foo\${foo},分别转换为 $foo${foo}

FROM busybox
ENV foo /bar
WORKDIR ${foo}    # WORKDIR /bar
ADD . $foo        # ADD . /bar
COPY \$foo /quux  # COPY $foo /quux

支持环境变量的指令有:ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL

FROM

构建镜像的基础源镜像(Base Image)。因此,有效的 Dockerfile 必须具有 FROM 作为其第一条指令。

FROM <image>
# 或者
FROM <image>:<tag>
# 或者
FROM <image>@<digest>
  • FROM 必须是 Dockerfile 中的第一个非注释指令。
  • FROM 可以在单个 Dockerfile 中多次出现,以创建多个图像。只需记下在每个新的 FROM 命令之前由提交输出的最后一个 Image ID
  • tagdigest 是可选的。如果省略其中任何一个,构建器将默认使用 latest。如果构建器与 tag 值不匹配,则构建器将返回错误。

MAINTAINER

设置生成的 Images 的作者字段

WORKDIR

为 Dockerfile 中的任何 RUNCMDENTRYPOINTCOPYADD 指令设置工作目录。可以在一个 Dockerfile 中多次使用,如果提供了相对路径,它将相对于先前 WORKDIR 指令的路径。

WORKDIR /foo
WORKDIR bar
WORKDIR baz
RUN pwd
# /foo/bar/baz

WORKDIR 指令可以解析先前使用 ENV 设置的环境变量

ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd
# /path/$DIRNAME

ENV

ENV 指令将环境变量 <key> 设置为值 <value>

ENV 指令有两种形式。

  • 第一种形式,ENV <key> <value>,将单个变量设置为一个值。第一个空格后面的整个字符串将被视为 <value> - 包括空格和引号等字符。
  • 第二种形式,ENV <key>=<value> ...,允许一次设置多个变量。

第二种形式在语法中使用等号 =,而第一种形式不使用。与命令行解析类似,引号和反斜杠可用于在值内包含空格。

ARG

ARG 指令定义一个变量,用户可以使用 docker build 命令使用 --build-arg <varname>=<value> 标志,在构建时将其传递给构建器。如果用户指定了一个未在 Dockerfile 中定义的构建参数,构建将输出错误。

Dockerfile 作者可以通过指定 ARG 一个或多个变量,通过多次指定 ARG 来定义单个变量。例如,一个有效的 Dockerfile:

FROM myapp
ARG foo
ARG bar

也可以可选地指定 ARG 指令的默认值:

FROm myapp
ARG foo=xyz
ARG bar=123

如果 ARG 值具有缺省值,并且如果在构建时没有传递值,则构建器使用缺省值。

使用上述示例,但使用不同的 ENV 规范,可以在 ARGENV 指令之间创建更有用的交互:

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0}
RUN echo $CONT_IMG_VER

ARG 变量不会持久化到构建的 Image 中,而 ENV 变量则会。但是,ARG 变量会以类似的方式影响构建缓存。如果一个 Dockerfile 定义一个 ARG 变量,它的值不同于以前的版本,那么在它的第一次使用时会出现一个 “cache miss”,而不是它的定义。特别地,在 ARG 指令之后的所有 RUN 指令都隐式地使用 ARG 变量(作为环境变量),因此可能导致高速缓存未命中。

RUN

RUN:后面跟的是在容器中要执行的命令。有两种形式:

  • RUN <command> shell 形式,命令在 Shell 中运行,Linux 上为 /bin/sh/ -c,Windows 上为 cmd /S/C
  • RUN ["executable", "param1", "param2"] exec 形式

每一个 RUN 指令都会新建立一层,在其上执行这些命令,当运行多个指令时,会产生一些非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错因此,在很多情况下,我们可以合并指令并运行,例如 RUN apt-get update && apt-get install -y libgdiplus。在命令过多时,一定要注意格式,比如换行、缩进、注释等,会让维护、排障更为容易。除此之外,Union FS 是有最大层数限制的,不能超过 127 层,而且我们应该把每一层中我用文件清除,比如一些没用的依赖,来防止镜像臃肿。

COPY

拷贝文件至容器的工作目录下,.dockerignore 指定的文件不会拷贝。

COPY <src> .. <dest>

COPY ["<src>", ..., "<dest>"]

与 ADD 类似,不过 COPY<src> 不能为 URL。

如果源或目标包含空格,请将路径括在方括号和双引号中。

ADD

ADD 指令与 COPY 指令非常类似,但它包含更多功能。除了将文件从主机复制到容器映像,ADD 指令还可以使用 URL 规范从远程位置复制文件

EXPOSE

EXPOSE <port> [<port>...]

EXPOSE 指令通知 Docker 容器在运行时侦听指定的网络端口。EXPOSE 不使主机的容器的端口可访问。为此,必须使用 -p 标志发布一系列端口,或者使用 -P 标志发布所有暴露的端口。您可以公开一个端口号,并用另一个端口号在外部发布。

ENTRYPOINT

ENTRYPOINT 允许您配置容器,运行执行的可执行文件。

# 使用 exec 执行
ENTRYPOINT ["executable", "param1", "param2"]

# 使用 shell 执行
ENTRYPOINT command param1 param2

每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个起效。

CMD

CMD 指令有三种形式:

# 使用 exec 执行,推荐方式
# 这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号
CMD ["executable", "param1", "param2"]

# 在 /bin/sh 中执行,提供给需要交互的应用
# 实际命令会被包装为 sh -c 的参数形式进行执行
CMD command param1 param2

# 提供给 ENTRYPOINT 的默认参数
CMD ["param1", "param2"]

指定启动容器时执行命令,每个 Dockerfile 只能有一个 CMD 指令。如果指定了多条命令,则只有最后一条会被执行。如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD 指定的命令

与 ENTRYPOINT 对比

共同点:

  1. 都可以指定 Shell 或 exec 函数调用的方式执行命令
  2. 当存在多个 CMD 指令或 ENTRYPOINT 指令时,只有最后一个生效

差异:

  1. CMD 指令指定的容器启动时命令可以被 docker run 指定的命令覆盖,而 ENTRYPOINT 指令指定的命令不能被覆盖,而是将 docker run 指定的参数当做 ENTRYPOINT 指定命令的参数
  2. CMD 指令可以为 ENTRYPOINT 指令设置默认参数,而且可以被 docker run 指定的参数覆盖

总结:

  • 如果 ENTRYPOINT 使用了 shell 模式,CMD 指令会被忽略
  • 如果 ENTRYPOINT 使用了 exec 模式,CMD 指定的内容被追加为 ENTRYPOINT 指定命令的参数
  • 如果 ENTRYPOINT 使用了 exec 模式,CMD 也应该使用 exec 模式

exec 模式是建议的使用模式,因为当运行任务的进程作为容器中的 1 号进程时,我们可以通过 docker 的 stop 命令优雅地结束容器。

VOLUME

VOLUME 指令创建具有指定名称的挂载点,并将其标记为从本机主机或其他容器保留外部挂载的卷。该值可以是 JSON 数组 VOLUME ["/var/log"] 或具有多个参数的纯字符串,例如 VOLUME /var/logVOLUME /var/log /var/db

USER

USER 指令设置运行 Image 时使用的用户名或 UID,以及 Dockerfile 中的任何 RUNCMDENTRYPOINT 指令。

LABEL

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

LABEL 指令向 Image 添加元数据。LABEL 是键值对。要在 LABEL 值中包含空格,使用引号和反斜杠,就像在命令行解析中一样:

Image 可以有多个 Label。要指定多个 Label,Docker 建议在可能的情况下将标签合并到单个 LABEL 指令中。每个 LABEL 指令产生一个新层,如果使用许多标签,可能会导致效率低下的镜像。

容器操作

image-20220212103504735

容器的几个核心状态,也就是图中色块表示的:CreatedRunningPausedStoppedDeleted

启动容器

$ docker run [args] <image-name>[:tag]

常见的启动参数:

  • -p:向宿主机暴露端口,格式 宿主机端口:容器端口
  • -P:将容器端口映射为宿主机的随机端口
  • -t:让 Docker 分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上
  • -i:让容器的标准输入保持打开
  • -v:映射数据卷,例如 /home/project:/usr/src,宿主机 /home/project 映射容器 /usr/src
  • -d:将容器放在后台运行
  • --rm:容器推出后清除资源
  • --privileged:最高权限

当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:

  1. 检查本地是否存在制定的镜像,不存在就从公有仓库下载
  2. 利用本地镜像创建并启动一个容器
  3. 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  4. 从宿主机配置的网桥接口桥接一个虚拟接口到容器中去
  5. 从地址池配置一个 IP 地址给容器
  6. 执行用户的指定的用户程序
  7. 执行完毕后容器被终止

重启容器

$ docker restart <container-name>

停止容器

$ docker stop <container-name>
# 或者
$ docker container stop <container-name>

使用 stop 停止的容器可以使用 start 再次启动

进入容器

$ docker exec  -it <container-name> /bin/bash

原理实际上是启动了容器内的 /bin/bash,此时你就可以通过 bash shell 与容器内交互了。就像远程连接了 SSH 一样。

  • -i:只有该参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回
  • -it:当合并使用时,则可以看到我们熟悉的 Linux 命令提示符

如果从这个 stdin 中 exit,不会导致容器的停止。

如果启动容器时使用-dit后台启动了终端,可以通过 attach 连接

$ docker attach <container-name>

导入、导出容器

# 导出容器
$ docker export  <container-name> > <file-name>.<file-suffix>
# 导入容器
$ cat <import-file-name> | docker import - <image-name> # 格式
$ cat ubuntu.tar | docker import - test/ubuntu:v1.0 # 示例

# 也可以通过 URL 导入
$ docker import <url> # 格式
$ docker import http://example.com/exampleimage.tgz example/imagerepo # 示例

删除容器

# 删除处于终止状态的容器
$ docker rm <container-name>

# 删除运行状态的容器
$ docker rm -f <container-name>

# 清理掉所有处于终止状态的容器
$ docker container prune

网络

要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)来收发数据包;此外,如果不同子网之间要进行通信,需要路由机制。

Docker 中的网络接口默认都是虚拟的接口。虚拟接口的优势之一是转发效率较高。 Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发,发送接口的发送缓存中的数据包被直接复制到接收接口的接收缓存中。对于本地系统和容器内系统看来就像是一个正常的以太网卡,只是它不需要真正同外部网络设备通信,速度要快很多。

Docker 容器网络就利用了这项技术。它在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做 veth pair)。

Docker 中使用 Network 来管理容器之间的通信,只要两个 Conteiner 处于同一个 Network 之中,就可以通过容器名去互相通信。

网络模式

在 docker 的世界中有四种网络模式:

  • Bridge 模式
  • Host 模式
  • Container 模式
  • None 模式

Bridge 模式

Bridge 模式(--net=bridge)为 Docker 容器默认创建后的网络模式,这中模式创建后即加入 docker0 网桥。其特点如下:

  • 使用一个 Linux bridge,默认为 docker0
  • 使用 veth 对,一头在容器的网络 namespace 中,一头在 docker0
  • 该模式下 Docker Container 不具有一个公有 IP,因为宿主机的 IP 地址与 veth pair 的 IP 地址不在同一个网段内
  • Docker 采用 NAT 方式,将容器内部的服务监听的端口与宿主机的某一个端口 port 进行绑定,使得宿主机以外的世界可以主动将网络报文发送至容器内部
  • 外界访问容器内的服务时,需要访问宿主机的 IP 以及宿主机的端口 port
  • NAT 模式由于是在三层网络上的实现手段,故肯定会影响网络的传输效率
  • 容器拥有独立、隔离的网络栈;让容器和宿主机以外的世界通过 NAT 建立通信

Host 模式

Host 模式(--net=host)并没有为容器创建一个隔离的网络环境(network namespace),这就意味着容器不会有自己的网卡信息,而是与宿主机共用网络环境,亦即拥有完全的本地主机接口的访问权限。

容器进程可以跟宿主机其他 root 进程一样打开低范围的端口,可以访问本地网络服务比如 D-Bus,还可以让容器做一些影响整个主机系统的事情。这种情况下,容器除了网络,其他都是隔离的。

此时容器内获取 IP 为宿主机 IP,端口绑定直接绑定在宿主机网卡上,有点是网络传输不用经过 NAT 转换,效率更高速度更快。

其特点包括:

  • 这种模式下的容器没有隔离的 network namespace
  • 容器的 IP 地址同 Docker host(容器的宿主机)的 IP 地址
  • 需要注意容器中服务的端口号不能与 Docker host 上已经使用的端口号相冲突
  • host 模式能够和其它模式共存

Container 模式

Container 模式(--net=container:<NAME_OR_ID>)是 Docker 中一种较为特别的网络的模式。处于这个模式下的 Docker 容器会共享其他容器的网络环境(共享 network namespace),因此,至少这两个容器之间不存在网络隔离,而这两个容器又与宿主机以及除此之外其他的容器存在网络隔离。

两个容器有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享 IP 地址和端口等网络资源,两者进程可以直接通过 I/O 环回接口通信。

None 模式

None 模式(--net=none),即容器获取独立的 network namespace,但不为容器进行任何网络配置,需要手动配置。一旦 Docker 容器采用了 none 网络模式,那么容器内部就只能使用 loopback 网络设备,不会再有其他的网络资源。Docker Container 的 none 网络模式意味着不给该容器创建任何网络环境,容器只能使用 127.0.0.1 的本机网络。

容器互联

# 通过link指令建立连接
 $ docker run --name <Name> -d -p <path1>:<path2> --link <containerName>:<alias> <containerName:tag/imageID>

常用指令

$ docker network create
# 将容器 container-name 连接到新建网络 network-name
$ docker network connect <network-name> <contaienr-name>
$ docker network ls
# 将 container-name 从 network-name 网络中移除连接
$ docker network disconnect <network-name> <container-name>
# 与 disconnect 相似,但是要求容器关闭或断开与此网络的连接
$ docker network rm <network-name> <container-name>
# 查看容器的网络情况
$ docker network inspect <container-name>

前端小白