> [!info] 章节导航
> [[docker学习路径|Docker 学习索引]] · [[01-认识 Docker|上一章]] · [[03-快速上手|下一章]]

## 2. 核心概念：Docker Objects

### 2.1 容器（Container）

就像app或者软件，实现方式略有不同，但是提供了类似的隔离。我们运行的是数据库、消息队列、网络服务器

每个容器化的进程都可以在完全独立于其它容器的隔离环境中运行，也独立于主机。

容器是镜像的可运行实例。您可以使用 Docker API 或 CLI 创建、启动、停止、移动或删除容器。

您可以将容器连接到一个或多个网络、为其附加存储，甚至可以根据其当前状态创建新的镜像。默认情况下，容器与其他容器及其主机之间具有相对良好的隔离。您可以控制容器的网络、存储或其他底层子系统与其他容器或主机之间的隔离程度。

特点：

- 自包含。每个容器都具备其运行所需的一切，无需依赖主机上任何预安装的依赖项。
- 隔离。由于容器是独立运行的，因此它们对主机和其他容器的影响极小，从而提高了应用程序的安全性。
- 
  独立。每个容器均独立管理。删除一个容器不会影响其他容器。
- 可移植。容器可以在任何地方运行！在您的开发机器上运行的容器在数据中心或云端的任何地方都能以相同的方式运行！

这样一来，Docker 就能把这些组件拼起来，并以各种方式连接它们，而不需要我在自己的电脑上一个个安装、配置环境变量，担心如果存在冲突。我的团队一部分人使用windows，一部分人使用Mac。

| 对比项     | 传统安装                 | Docker 方式               |
| ---------- | ------------------------ | ------------------------- |
| 环境隔离   | 全部在系统里混在一起     | 每个容器独立运行          |
| 配置复杂度 | 高，需要改环境变量、路径 | 简单，写在 Compose 文件里 |
| 迁移       | 换电脑要重新配置         | 拷贝 Compose 文件即可     |
| 回滚       | 麻烦                     | 一键删除重建容器          |
| 版本管理   | 容易冲突                 | 镜像固定版本号            |



<img src="https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20251016202543229.png" alt="image-20251016202543229" style="zoom:50%;" />

#### 容器与虚拟机（VM）



简单来说，虚拟机是一个完整的操作系统，拥有自己的内核、硬件驱动程序、程序和应用程序。仅仅为了隔离单个应用程序而启动虚拟机会带来很大的开销。


容器只是一个独立的进程，包含运行所需的所有文件。如果您运行多个容器，它们将共享同一个内核，从而允许您在更少的基础设施上运行更多应用程序。

### 2.2 镜像（Image）

我们可以将容器镜像视为一个单独的包，其中包含运行容器/  进程所需的所有内容。在本例中，它将包含 Node 环境、后端代码和已编译的 React 代码。

如果您不熟悉容器镜像，可以将其视为一个标准化软件包，其中包含运行应用程序所需的所有内容，包括其文件、配置和依赖项。这些软件包可以分发并与其他人共享。

如果将容器作为隔离的沙盒环境，它们仅使用环境的文件运行。 如何将文件放在环境里？如何共享这些文件？——镜像。

镜像就是这些包括文件、二进制文件、操作系统包和库，标准化的包。

镜像是一个只读模板，其中包含创建 Docker 容器的说明。一旦创建了镜像，无法修改或更改它。只能创建一个新的或者向其中添加额外的层。

镜像由层组成，这些层基本上代表文件系统的变化。每一层代表一组文件系统变更，包括添加、删除或修改文件。

这两个原则允许您扩展或添加现有镜像。例如，如果您正在构建一个 Python 应用，您可以从 [Python 镜像](https://hub.docker.com/_/python)开始，然后添加额外的层来安装应用的依赖项并添加代码。这样您就可以专注于应用本身，而不是 Python 本身。

也就是说，当你需要更新的时候，你不再需要进入生产环境来修改内容，而是构建一个新的镜像。

通常，一个镜像基于另一个镜像，并进行了一些额外的自定义。例如，您可以构建一个基于 `ubuntu` 镜像 图像，但会安装 Apache Web 服务器和您的应用程序，以及 使您的应用程序运行所需的配置详细信息。

您可以创建自己的镜像，也可以只使用其他人创建并发布在镜像仓库中的镜像。要构建自己的镜像，您需要创建一个Dockerfile，该文件使用简单的语法来定义创建和运行镜像所需的步骤。Dockerfile 中的每条指令都会在镜像中创建一个层。当您更改 Dockerfile 并重建镜像时，只有那些已更改的层会被重建。

任何使用该映像运行容器的机器都将能够按照构建的方式运行该应用程序，而无需在机器上预先安装任何其他东西。



**镜像 = 模板 + 配方 + 代码和依赖的打包快照**

- 它是一个 **只读** 的文件系统快照，包含程序运行所需的一切（代码、环境、库、配置等）。
- 镜像本身不会“运行”，就像一个蓝图。
- 你可以从同一个镜像创建多个容器，就像从同一个模板生成多个实例。

类比：

| 比喻                          | 含义              |
| ----------------------------- | ----------------- |
| 类（Class）                   | 镜像（Image）     |
| 对象（Object）                | 容器（Container） |
| 模板（Template）              | 镜像              |
| 模板实例（Template Instance） | 容器              |

**容器 = 镜像的运行实例**

- 当你执行 `docker run image_name`，Docker 会：
  1. 复制镜像的只读层；
  2. 在上面加一层“可写层”（容器层）；
  3. 启动运行环境（namespace + cgroup 隔离）。

每个容器的运行状态、日志、数据变更，都会存在它自己的可写层中。
 删除容器，这一层也就消失了；镜像本身不受影响。

-----

假设Docker Hub有一个镜像：

```
nginx:latest
```

它包含：

- nginx 程序
- 默认配置
- 基础 Linux 环境

当你运行：

```
docker run -d -p 8080:80 nginx
```

Docker 做了这些：

1. 以 `nginx` 镜像为模板；
2. 创建一个容器；
3. 给容器加上独立文件系统；
4. 启动 nginx 服务。

现在你就有了一个“活的 nginx 实例” —— 它在运行、可以接收请求。
 而镜像 `nginx:latest` 依然安静地躺在你的系统中，没动过。

------



```
镜像（Image）
 ├── 层1：基础系统（Ubuntu、Alpine）
 ├── 层2：依赖库（Node.js、Python 等）
 └── 层3：应用代码（你的项目）

容器（Container）
 ├── 继承镜像所有层（只读）
 └── + 容器自己的可写层（运行时变化）
```

| 概念             | 类比            | 特点                   |
| ---------------- | --------------- | ---------------------- |
| 镜像 (Image)     | 模板 / 类       | 只读、可重复使用、静态 |
| 容器 (Container) | 模板实例 / 对象 | 可写、可运行、动       |

#### 镜像分层（Image layers）

镜像中的每一层都包含一系列文件系统更改 - 添加、删除或修改。让我们来看一个理论上的镜像：

1. 第一层添加了基本命令和包管理器，例如 apt。
2. 第二层安装 Python 运行时和 pip 用于依赖项管理。
3. 
   第三层复制应用程序的特定 requirements.txt 文件。
4. 第四层安装该应用程序的特定依赖项。
5. 
   第五层复制应用程序的实际源代码。

<img src="https://docs.docker.com/get-started/docker-concepts/building-images/images/container_image_layers.webp" alt="screenshot of the flowchart showing the concept of the image layers" style="zoom:50%;" />


这非常有益，因为它允许在镜像之间重用层。例如，假设您想创建另一个 Python 应用程序。由于分层，您可以利用相同的 Python 基础。这将加快构建速度，并减少分发镜像所需的存储和带宽。镜像分层可能类似于以下内容：

<img src="https://docs.docker.com/get-started/docker-concepts/building-images/images/container_image_layer_reuse.webp" alt="screenshot of the flowchart showing the benefits of the image layering" style="zoom:40%;" />

通过图层，您可以通过重复使用其他图像的基础图层来扩展其他图像，从而允许您仅添加应用程序所需的数据。



#### 层的堆叠（Stacking the layers）


分层是通过内容可寻址存储和联合文件系统实现的。虽然这有点技术性，但其工作原理如下：

1. 下载每一层后，它会被提取到主机文件系统上自己的目录中。
2. 
   当您从图像运行容器时，将创建一个联合文件系统，其中各层相互堆叠，从而创建一个新的统一视图。
3. 当容器启动时，使用 `chroot` 将其根目录设置为此统一目录的位置。


创建联合文件系统时，除了镜像层之外，还会为正在运行的容器创建一个专用目录。这允许容器更改文件系统，同时保持原始镜像层不变。这样一来，您就可以从同一个底层镜像运行多个容器

![image-20251018185625525](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20251018185625525.png)



1. 在终端中，运行以下命令来启动新容器：

   

   ```console
    docker run --name=base-container -ti ubuntu
   ```

   下载镜像并启动容器后，您应该会看到一个新的 shell 提示符。它正在容器内运行。它看起来类似于以下内容（容器 ID 会有所不同）：

   ```console
   root@d8c5ca119fcd:/#
   ```

2. 在容器内，运行以下命令来安装 Node.js：

   

   ```console
    apt update && apt install -y nodejs
   ```

   
   此命令运行时，它会下载 Node 并将其安装到容器内。在联合文件系统的上下文中，这些文件系统的更改将发生在该容器独有的目录中。

3. 通过运行以下命令验证 Node 是否已安装：

   

   ```console
    node -e 'console.log("Hello world!")'
   ```

   然后您应该会看到控制台中出现“Hello world！”。

4. 现在您已经安装了 Node，可以将所做的更改保存为新的**镜像层**，以便从中启动新容器或构建新镜像。为此，您将使用 [`docker container commit`](https://docs.docker.com/reference/cli/docker/container/commit/) 命令。在新终端中运行以下命令：

   ```console
    docker container commit -m "Add node" base-container node-base
   ```
	这会创建一个新的镜像 node-base，它等于「base-image + Node 已安装」。  
	  所以 node-base 就是一个带 Node 的自定义镜像。
5. 
   
   使用 `docker image history` 命令查看图像的层：

   ```console
    docker image history node-base
   ```

​	此时只是创建了镜像，如果需要打包成一个容器

6. 为了证明您的镜像已安装 Node，您可以使用这个新镜像启动一个新容器：

```console
 docker run node-base node -e "console.log('Hello again')"
```

7. 现在您已经完成了基础镜像的创建，您可以删除该容器：

```console
 docker rm -f base-container
```



`commit` 是把容器的修改固化下来变成一个新的镜像；
 `run` 是用镜像启动一个新的容器（容器是镜像的“运行实例”）。



基础镜像是构建其他镜像的基础。任何镜像都可以用作基础镜像。不过，有些镜像是专门设计为构建块的，为应用程序提供基础或起点。

在这个例子中，你可能不会部署这个 `node-base` 镜像，因为它实际上还没有做任何事情。但它可以作为你进行其他构建的基础。

现在您有了基础图像，您可以扩展该图像来构建其他图像。

1. 使用新创建的节点基础映像启动一个新容器：

   ```console
    docker run --name=app-container -ti node-base
   ```

2. 在此容器内，运行以下命令来创建一个 Node 程序：

   ```console
    echo 'console.log("Hello from an app")' > app.js
   ```

   要运行这个 Node 程序，您可以使用以下命令并查看屏幕上打印的消息：

   ```console
    node app.js
   ```

3. 在另一个终端中，运行以下命令将此容器的更改保存为新图像：

   ```console
    docker container commit -c "CMD node app.js" -m "Add app" app-container sample-app
   ```

   此命令不仅会创建一个名为 `sample-app` 新镜像，还会向该镜像添加其他配置，以设置启动容器时的默认命令。在本例中，您将它设置为自动运行 `node app.js` 。

4. 
   在容器外的终端中，运行以下命令来查看更新的层：

   

   ```console
    docker image history sample-app
   ```

5. 最后，使用全新的镜像启动一个新容器。由于您指定了默认命令，因此可以使用以下命令：



```console
 docker run sample-app
```

您应该会看到来自 Node 程序的问候语出现在终端中。

现在您已经完成了容器的操作，您可以使用以下命令删除它们：

```console
 docker rm -f app-container
```



- 如果你运行容器时执行的是

  ```
  docker run --name app-container sample-app
  ```

  那么容器的名字就是 **app-container**，此时你删除它的命令确实是：

  ```
  docker rm -f app-container
  ```

- 但如果你只是执行了：

  ```
  docker run sample-app
  ```

  没有指定 `--name`，那么 Docker 会自动生成一个随机容器名。
   这时你需要先查出容器的名字或 ID，然后再删除，比如：

  ```
  docker ps -a
  ```

  找到那个容器（可能叫 `frosty_mclean` 之类），然后：

  ```
  docker rm -f <容器名或ID>
  ```

------

⚠️ 注意

- `docker rm` 删除的是 **容器**。

- 如果你还想删除镜像（例如 `sample-app`），可以用：

  ```
  docker rmi sample-app
  ```

### 2.3 镜像注册表（Registry）

镜像存储在哪里？

容器镜像存储在计算机系统上，但是如果您想于朋友共享它们或者在另一台机器上使用它们，就是需要镜像注册表。

镜像注册表（Image registry）是用于存储和共享容器镜像的集中位置。它可以是公共的，也可以是私有的。Docker Hub是一个任何人都可以使用的公共镜像注册表，也是默认的镜像注册表。

注册表就是您可以向其推送图像和从中拉取图像的地方。

Docker Hub实现了开放容器计划（OCI）分发规范。

#### Registry 与 Repository

在使用注册表时，您可能会听到 *“注册表”* 和 *“存储库”* 这两个术语，好像它们可以互换。尽管它们相关，但它们并不完全相同。

*镜像仓库 (Registry)* 是一个集中存储和管理容器镜像的位置，而*仓库 (Repository)* 是镜像仓库中相关容器镜像的集合。您可以将其视为一个文件夹，用于根据项目组织镜像。每个仓库都包含一个或多个容器镜像。



#### Docker Hub


要共享 Docker 镜像，您需要一个地方来存储它们。这时，镜像仓库就派上用场了。虽然镜像仓库有很多，但 Docker Hub 是默认且首选的镜像仓库。Docker Hub 不仅为您提供了存储自己镜像的地方，还为您提供了查找其他镜像的地方，方便您运行或作为自己镜像的基础
