Docker 知识汇总

291 阅读16分钟

基于容器介绍 Docker

Docker 是一个开源的容器化平台,它的诞生是为了简化构建、传输和运行应用程序的过程。Docker 使用了容器技术来封装应用程序,但 Docker 并不是容器技术的发明者,容器技术可以被更早地追溯到2001年的 VServer,但 Docker 是目前最受欢迎的容器化平台,容易产生容器等同于 Docker 的印象。

容器是一种轻量级、可执行的软件包装技术,它允许软件或应用程序与其环境隔离,但与宿主机共用一个操作系统内核。容器内包含了运行程序所需的一切:代码、运行时、系统工具、系统库、设置,以便在任何计算环境中一致运行。

因此,相比于虚拟机,容器更轻量,同时拥有更好的隔离性和移植性。

基于容器的优点,使用容器在应用程序开发中的优势在于,当部署这些应用程序时,可以更紧密地打包,从而节省硬件资源。从开发和测试生命周期来看,容器使得能够在开发机器上运行生产代码,无需复杂的设置;它还允许在不需要安装相同数据库的不同实例的情况下创建环境,以试验新软件。

Docker 由许多部分组成,其核心是 Docker 引擎,这是一个具有编排、调度网络和安全功能的轻量级应用程序运行时。Docker 引擎可以安装在物理或虚拟主机上的任何位置,并且支持 Windows 和 Linux。同时它也是一个客户端-服务器类型的应用程序,包括客户端和服务端:

  • Server:Docker 守护进程 (daemon),称为 dockerd,负责创建和管理 Docker 容器。
  • Client:Docker 客户端,提供了一种命令行接口,用户通过执行各种 Docker 命令(如 docker rundocker build 等)来操作容器和镜像。

除此以外,Docker 还包括两个重要概念,Docker 镜像(Images)和 Docker 容器(Containers)

Docker 镜像是容器运行的基础,它包含了容器运行所需要的所有文件和依赖。镜像是只读的模板,可以用来创建 Docker 容器。Docker 用户可以通过 Dockerfile 自定义镜像,或者从 Docker Hub 等容器镜像仓库下载现成的镜像。

Docker 容器是由镜像创建的不可变实例,数据卷默认是非持久的。容器封装了应用和其运行环境,确保应用在不同环境间可一致运行。容器可以启动、开始、停止、移动和删除。

运行第一个 Docker 容器

Docker 的官方提供了一个 Hello World 镜像用于简单的测试,可以用于演示 Docker 的基本功能

打开终端或命令提示符,输入以下命令来运行一个 Hello World 容器:

docker run hello-world

这个命令的含义是:

  • docker run 命令 Docker 运行一个新容器。
  • hello-world 是容器所使用的镜像。如果这个镜像不在本地系统上,Docker 会从 Docker Hub(Docker 的默认公共镜像库)自动下载它。

运行上述命令后,如果一切正常,将看到以下输出:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:8e3114318a6db8bd4eec35f29683b90bb99f4a38cfa7c8cb6e35f7cbb2e4e5de
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
...

这表示 Docker 客户端成功从 Docker Hub 下载了 hello-world 镜像,并创建了一个容器来运行这个镜像。镜像内的程序输出了一些文本,证明容器正确运行。

接下来可以使用 docker ps -a 来查看所有容器的列表(包括已停止的容器)或使用 docker images 查看本地存储的镜像列表。 以下是一些常用的容器管理命令和镜像管理命令:

基础管理命令

  • docker run:创建一个新的容器并运行一个命令
  • docker start:启动一个或多个已经停止运行的容器
  • docker stop:停止一个运行中的容器
  • docker restart:重启容器
  • docker rm:删除一个或多个容器
  • docker ps:列出容器
  • docker images:列出镜像

镜像管理命令

  • docker pull:从镜像仓库拉取一个镜像或仓库
  • docker push:将一个镜像或仓库推送到注册中心
  • docker build:使用 Dockerfile 创建镜像
  • docker tag:标记本地镜像,将其归入某一仓库
  • docker rmi:删除一个或多个镜像

Docker 卷

Docker 容器是由镜像创建的不可变实例,数据卷默认是非持久的。可以通过启动一个新容器做出一些修改,然后重新启动容器并检查文件是否存在验证。

但在某些情况下,如配置管理和日志记录时,需要将数据持久化。因此 Docker 引入了卷(volumes)的概念,用于在容器和宿主机之间管理和存储数据。它主要用于实现数据的持久化和数据共享,以确保容器中的数据不会随着容器的删除而丢失,并且可以在多个容器之间共享数据。

Docker volumes 主要有三种类型:

  1. Volumes:由 Docker 管理的存储空间,位于宿主机的某个目录下,通常位于 /var/lib/docker/volumes。这是最常用的 volume 类型,因为它完全由 Docker 管理,相对更加安全和稳定。
  2. Bind mounts:将宿主机上的任意目录或文件系统挂载到容器中。这种方式可以提供对宿主机文件系统更直接的访问权限,但相对来说,灵活性和安全性略低。
  3. tmpfs mounts:将数据存储在宿主机的内存中,而不是磁盘上。适用于对数据持久性没有要求的情况,例如存储敏感信息或临时数据。

Docker 提供了一系列命令来管理 volumes:

  • docker volume create:创建一个新的 volume。
  • docker volume ls:列出所有的 volume。
  • docker volume inspect:查看某个 volume 的详细信息。
  • docker volume rm:删除一个 volume。

为了保持镜像的高效和紧凑,Docker 使用了联合文件系统(Union File System)的概念。联合文件系统允许通过组合不同的目录或文件来表示一个逻辑文件系统。它采用写时复制(Copy on Write)技术,当修改文件系统时复制层,这样在创建新镜像时只需使用大约1MB的空间。当数据被写入文件系统时,Docker 复制该层并将其放置在堆栈的顶部。在构建镜像和扩展现有镜像时,我们利用这种技术,同时当启动一个镜像并创建一个容器时,唯一的区别是这个可写层,这意味着不需要每次都复制所有层并填满我们的磁盘。

Docker 端口

在容器内运行 Web 应用时,通常需要将一些端口暴露给外界。默认情况下,Docker 容器是完全隔离的,需要从 Docker 容器外部访问容器内部的服务时,需要设置端口映射。可以通过运行容器时使用 -p--publish 选项来完成。

格式为:

docker run -p <宿主机端口>:<容器端口> ...

例如,如果容器应用在端口 80 上提供服务,而希望通过宿主机的端口 8080 来访问它,可以这样启动容器:

docker run -p 8080:80 my-web-app

Docker 也提供了查看端口映射的命令,会列出容器的所有端口映射。

docker port <容器名或ID>

Docker 网络

Docker 网络用于管理容器之间的通信以及容器与外部世界的连接,提供了多种网络模式和配置方式,帮助用户控制容器的访问、隔离和路由规则。

Docker 支持以下几种网络模式:

  • bridge (桥接)
  • host (主机)
  • none (无网络)
  • overlay (覆盖网络)

桥接网络

桥接网络模式是 Docker 容器默认的网络类型,它为容器提供了一个隔离的网络环境,并通过一个虚拟的桥接设备在容器和宿主机之间建立通信。同一桥接网络中的容器可以直接通过 IP 地址相互通信,无需端口映射。同时,也可以配置容器使用不同的桥接网络来进一步隔离通信。

在桥接模式中,每个容器都会被分配到一个私有的子网中,通常情况下容器会获得一个私有 IP 地址,增加了网络层面的隔离性。

为了实现这一功能,Docker 使用了 Linux 的一些核心功能,如网络命名空间和虚拟以太网接口(或 veth 接口)。当 Docker 引擎启动时,它会在主机上创建 docker0 虚拟接口。docker0 是一个虚拟以太网桥,它会自动转发连接到它的其他网络接口之间的数据包。当一个容器启动时,Docker 会创建一个 veth 对,其中一个接口分配给容器,这个接口成为容器的 eth0,而另一个接口连接到 docker0 桥接网络。

主机网络

主机网络(Host Networking)模式是 Docker 网络驱动的一种,允许 Docker 容器共享宿主机的网络命名空间。这意味着在主机网络模式下运行的容器不会有自己独立的虚拟网络接口,而是直接使用宿主机的 IP 地址和网络端口。由于容器直接使用宿主的网络,不存在网络虚拟化的开销,这可以提供更接近原生的网络性能。 虽然这似乎很方便,但 Docker 一直被设计为能够在引擎上运行同一容器的多个实例,而在 Linux 中只能将一个套接字绑定到一个端口,使用主机网络限制了这一特性。

主机网络也可能对容器构成安全风险,因为它不再受到“不信任原则”的保护,并且也不再能够明确控制端口是否暴露。由于主机网络的高效性,如果容器会大量使用网络,连接容器到主机网络可能是合适的。API 网关可能就是这样一个例子,这样的容器仍然可以将请求路由到位于桥接网络中的其他 API 容器。

无网络

无网络模式为容器提供了一个没有任何网络接口的环境,这意味着在这种模式下运行的容器将不会有网络栈,因此不能进行任何形式的网络通信。这种网络模式主要用于需要完全隔离容器的网络或在特定的安全环境中运行容器的场景。

覆盖网络

覆盖网络(Overlay Network)模式是专门设计用来支持 Docker Swarm 集群中多主机之间的容器通信。这种网络类型允许在不同的物理或虚拟机上运行的容器之间进行通信,就好像它们在同一主机上一样。

与开发环境不同,在生产环境中运行代码时通常会运行多个主机,每个主机上运行多个容器,以确保高可用性。这些容器之间仍然需要相互通信,虽然可以将所有流量通过企业服务总线(ESB)进行路由,但在微服务架构中这被认为是一种反模式。

推荐的方式是让服务负责自身的发现和客户端调用的负载均衡。Docker 的覆盖网络解决了这个问题,它实际上在机器之间创建了一个网络隧道,使流量在物理网络上传输时不被修改。覆盖网络的问题在于,你不能再依赖 Docker 自动更新 /etc/hosts 文件,而是必须依赖动态服务注册表。

写一个 Dockerfile

基础指令

Dockerfile 是一个文本文件,用于自动构建 Docker 容器镜像。它包含了一系列的指令和参数,这些指令描述了从基础镜像开始,如何一步步构建最终的镜像。每一个指令通常都会在镜像中添加一个新层。下面是一些常用的 Dockerfile 指令:

  • FROM: 指定基础镜像。所有Dockerfile都必须从一个基础镜像开始,这可以是一个已存在的镜像,或者从零开始的空白镜像。
  • RUN: 执行命令并创建新的镜像层。例如安装软件包。
  • CMD: 容器启动时运行的命令。一个Dockerfile中只能有一个CMD指令,如果指定了多个,则只有最后一个会被执行。
  • ENTRYPOINT: 配置容器启动时运行的命令,允许将容器当作命令行工具使用。
  • COPY: 从构建上下文目录复制文件或目录到容器里指定路径。
  • ADD: 类似于COPY,但是如果源文件是个压缩格式的文件,ADD指令将自动解压缩。
  • ENV: 设置环境变量。
  • ARG: 定义构建时的变量,可用于传递版本号、路径等信息给构建运行时。
  • EXPOSE: 声明容器运行时监听的端口。
  • VOLUME: 声明容器内的一个或多个挂载点。
  • WORKDIR: 设置工作目录,Dockerfile 中的 RUN、CMD、ENTRYPOINT、COPY 和 ADD 命令可以使用这个路径。
  • USER: 设置运行容器时的用户名或UID。
  • HEALTHCHECK: 告诉Docker如何测试容器以检查其是否还在正常运行。

层与缓存

在 Docker 中,(Layers)和缓存(Cache)是构建和运行容器镜像时的核心概念,它们大大提高了构建效率和存储效率。这里详细解释这两个概念及其在 Docker 使用中的重要性。

层(Layers)

Docker 镜像是由多个只读层构成的。每个层代表 Dockerfile 中的一条指令,例如 RUN, COPY, ADD 等。当你构建镜像时,每执行一条指令,Docker 就会创建一个新的层。这些层是只读的,并且当再次构建镜像时,如果 Dockerfile 中的指令没有改变,已存在的层会被重用。

为什么使用层?

  • 重用:层可以被不同的镜像共享,只要它们是相同的层,就无需重复存储或创建,减少了存储空间的使用和加速了镜像的下载。
  • 效率:层的使用使得更新镜像变得非常高效。如果你修改了 Dockerfile,只有从修改点开始之后的层会被重新构建。
  • 缓存利用:层的结构让 Docker 很容易实现缓存机制,加快连续的构建过程。

缓存(Cache)

当使用 Dockerfile 构建镜像时,Docker 会查看每条指令并尝试使用现有的缓存层(如果可用)。如果 Docker 判断当前指令和之前构建中的指令完全一样(包括所有前面的指令和环境),它就会使用缓存的层,而不是重新创建一个新的层。

缓存的工作方式

  • 顺序依赖:缓存的检查是从 Dockerfile 的顶部开始,按顺序进行。如果一条指令的缓存失效(因为指令或其上下文改变了),则这条指令以及后续所有指令的缓存都会失效。
  • 指令敏感性ADDCOPY 指令对文件的修改特别敏感。即使是小的更改,也会导致缓存失效,因为 Docker 会检查文件的内容。
  • 运行命令:对于 RUN 指令,如果命令文本没有改变,Docker 将尝试使用缓存。但是,如果它依赖于被修改了的文件,缓存也会失效。

编写 Dockerfile 的良好实践

每当在 Dockerfile 中执行一条命令时,Docker 都会创建一个新的层。当你修改其中一条命令时,该层必须被完全重建,且可能会影响后续的所有层,这可能会显著减慢构建速度。因此,推荐的最佳实践是尽可能地将相关命令组合在一起,以减少这种情况的发生。

你经常会看到一些 Dockerfile 中不是每个命令都用一个单独的 RUN 指令,而是使用标准的 bash 格式将多个命令串联在一起。比如,考虑以下示例,它使用包管理器来安装软件:

不良实践:

RUN apt-get update
RUN apt-get install -y wget
RUN apt-get install -y curl
RUN apt-get install -y nginx

良好实践:

RUN apt-get update && \
    apt-get install -y wget curl nginx

第二个示例只会创建一个层,从而生成一个更小更紧凑的镜像。此外,将 Dockerfile 中 COPY 语句按变动频率排序也是一种良好实践,尽量将变动最少的语句放在前面,这样即使发生变动也不会影响后续的层。

使用 Docker Compose

Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具。它允许用户使用一个 YAML 文件来配置应用程序的所有服务,然后通过简单的命令来启动和管理这些服务。Docker Compose 非常适合于开发和测试环境中需要多容器协作的应用,尤其是在微服务架构中。

使用 Docker Compose 的步骤

  1. 编写 docker-compose.yml 文件
    docker-compose.yml 是 Compose 文件的默认名称。在这个文件中,可以定义服务、网络和卷等内容。

    yaml
    复制代码
    version: '3'
    services:
      web:
        image: nginx
        ports:
          - "80:80"
      db:
        image: mysql
        environment:
          MYSQL_ROOT_PASSWORD: example
    

    在这个示例中,定义了两个服务 webdbweb 服务使用 nginx 镜像,并将宿主机的 80 端口映射到容器的 80 端口;db 服务使用 mysql 镜像,并设置 MySQL 的根密码。

  2. 运行 Compose 命令
    编写好 Compose 文件后,可以使用 docker-compose up 命令来启动应用。Docker Compose 会根据文件内容,下载所需的镜像,创建并启动容器。

    bash
    复制代码
    docker-compose up
    

    -d 选项可以让容器在后台运行:

    bash
    复制代码
    docker-compose up -d
    
  3. 管理和操作

    • docker-compose ps:查看运行中的服务。
    • docker-compose stop:停止所有服务。
    • docker-compose down:停止并删除所有容器、网络和卷(如果在文件中定义了卷)。
  4. 查看日志
    可以使用 docker-compose logs 查看所有服务的日志:

    docker-compose logs
    
  5. 扩展服务
    如果需要同时启动多个相同的服务实例,可以使用 --scale 参数进行扩展,例如启动 3 个 web 服务实例:

    docker-compose up --scale web=3