# Docker 容器 ## 一句话理解容器 > **容器是镜像的运行实例。如果把镜像比作程序,那么容器就是进程。** 用面向对象编程的术语来说:**镜像是类(Class),容器是对象(Instance)**。 - 一个镜像可以创建多个容器 - 每个容器相互独立,互不影响 - 容器可以被创建、启动、停止、删除、暂停 ## 容器的本质 > 💡 **笔者认为,理解这一点是理解 Docker 的关键** **容器的本质是一个特殊的进程。** ``` ┌─────────────────────────────────────────────────────────────┐ │ 普通进程 │ │ • 与其他进程共享系统资源 │ │ • 可以看到其他进程 │ │ • 共享网络和文件系统 │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ 容器进程 │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ • 有自己的进程空间(看不到宿主机上的其他进程) │ │ │ │ • 有自己的网络(独立 IP、端口) │ │ │ │ • 有自己的文件系统(独立的 root 目录) │ │ │ │ • 有自己的用户(容器内的 root ≠ 宿主机的 root) │ │ │ └───────────────────────────────────────────────────────┘ │ │ 但仍然运行在宿主机的内核上 │ └─────────────────────────────────────────────────────────────┘ ``` 这种隔离是通过 Linux 内核的 **Namespace** 技术实现的。 ## 容器 vs 虚拟机:核心区别 很多初学者会混淆容器和虚拟机。笔者用一张图来说明: ``` 虚拟机 容器 ┌───────────────────────┐ ┌───────────────────────┐ │ App A │ App B │ │ App A │ App B │ ├────────────┼──────────┤ ├────────────┼──────────┤ │ Guest OS │ Guest OS │ │ Container │ Container│ │ (完整系统) │ (完整系统)│ │ (仅应用) │ (仅应用) │ ├────────────┴──────────┤ └────────────┴──────────┤ │ Hypervisor │ │ Docker Engine │ ├───────────────────────┤ ├───────────────────────┤ │ Host OS │ │ Host OS │ ├───────────────────────┤ ├───────────────────────┤ │ Hardware │ │ Hardware │ └───────────────────────┘ └───────────────────────┘ 每个 VM 运行完整 OS 所有容器共享宿主机内核 ``` | 特性 | 容器 | 虚拟机 | |------|------|--------| | **隔离级别** | 进程级(Namespace) | 硬件级(Hypervisor) | | **启动时间** | 秒级(甚至毫秒) | 分钟级 | | **资源占用** | MB 级别 | GB 级别 | | **性能损耗** | 几乎为零 | 5-20% | | **内核** | 共享宿主机内核 | 各自独立内核 | ## 容器的存储层 ### 镜像层 + 容器层 当容器运行时,Docker 会在镜像的只读层之上创建一个**可写层**(容器存储层): ``` ┌─────────────────────────────────────────────┐ │ 容器存储层(可读写) │ ← 容器运行时创建 │ 运行时产生的文件变化记录在这里 │ ├─────────────────────────────────────────────┤ │ 镜像第 N 层(只读) │ ├─────────────────────────────────────────────┤ │ 镜像第 N-1 层(只读) │ ├─────────────────────────────────────────────┤ │ ... │ ├─────────────────────────────────────────────┤ │ 镜像第 1 层(只读) │ ← 基础镜像层 └─────────────────────────────────────────────┘ ``` ### Copy-on-Write(写时复制) 当容器需要修改镜像层中的文件时: 1. Docker 将该文件**复制**到容器存储层 2. 在容器层中进行修改 3. 原始镜像层保持不变 ``` 读取文件:直接从镜像层读取(共享,高效) 修改文件:复制到容器层,然后修改(只有这个容器能看到修改) ``` ### ⚠️ 容器存储层的生命周期 > **笔者特别强调**:这是新手最容易踩的坑! **容器存储层与容器生命周期绑定。容器删除,数据就没了!** ```bash # 创建容器,写入数据 $ docker run -it ubuntu bash root@abc123:/# echo "important data" > /data.txt root@abc123:/# exit # 删除容器 $ docker rm abc123 # 数据丢了!没有任何办法恢复! ``` ### 正确的数据持久化方式 按照 Docker 最佳实践,容器存储层应该保持**无状态**。需要持久化的数据应该使用: | 方式 | 说明 | 适用场景 | |------|------|---------| | **[数据卷(Volume)](../07_data_network/data/volume.md)** | Docker 管理的存储 | 数据库、应用数据 | | **[绑定挂载(Bind Mount)](../07_data_network/data/bind-mounts.md)** | 挂载宿主机目录 | 开发时共享代码 | ```bash # 使用数据卷(推荐) $ docker run -v mydata:/var/lib/mysql mysql # 使用绑定挂载 $ docker run -v /host/path:/container/path nginx ``` 这些位置的读写**会跳过容器存储层**,直接写入宿主机,性能更好,也不会随容器删除而丢失。 ## 容器的生命周期 ``` ┌──────────────────────────────────────────────────┐ │ 容器生命周期 │ └──────────────────────────────────────────────────┘ docker create docker start docker stop │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Created │───────────▶│ Running │───────────▶│ Stopped │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ docker pause │ │ ▼ │ │ ┌─────────┐ │ │ │ Paused │ │ │ └─────────┘ │ │ │ │ │ docker rm │ docker rm │ └───────────────────────┴──────────────────────┘ │ ▼ ┌──────────┐ │ Deleted │ └──────────┘ ``` ### 常用生命周期命令 ```bash # 创建并启动容器(最常用) $ docker run nginx # 分步操作 $ docker create nginx # 创建容器(不启动) $ docker start abc123 # 启动容器 # 停止容器 $ docker stop abc123 # 优雅停止(发送 SIGTERM,等待后发送 SIGKILL) $ docker kill abc123 # 强制停止(直接发送 SIGKILL) # 暂停/恢复(不常用,但有时有用) $ docker pause abc123 # 暂停容器内所有进程 $ docker unpause abc123 # 恢复 # 删除容器 $ docker rm abc123 # 删除已停止的容器 $ docker rm -f abc123 # 强制删除运行中的容器 ``` ## 容器与进程的关系 > **核心概念**:容器的生命周期 = 主进程(PID 1)的生命周期 ```bash # 主进程运行,容器运行 # 主进程退出,容器停止 ``` 这就是为什么: ```bash # 这个容器会立即退出(bash 没有输入就退出了) $ docker run ubuntu # 这个容器会持续运行(nginx 作为守护进程持续运行) $ docker run nginx ``` 详细解释请参考[后台运行](../05_container/daemon.md)章节。 ## 容器的隔离性 Docker 容器通过以下 Namespace 实现隔离: | Namespace | 隔离内容 | 效果 | |-----------|---------|------| | **PID** | 进程 ID | 容器内 PID 1 是应用进程,看不到宿主机其他进程 | | **NET** | 网络 | 独立的网络栈、IP 地址、端口 | | **MNT** | 文件系统 | 独立的根目录和挂载点 | | **UTS** | 主机名 | 独立的主机名和域名 | | **IPC** | 进程间通信 | 独立的信号量、消息队列 | | **USER** | 用户 | 独立的用户和组 ID | > 想深入了解?请阅读[底层实现 - 命名空间](../13_implementation/namespace.md)。 ## 本章小结 | 概念 | 要点 | |------|------| | **容器是什么** | 镜像的运行实例,本质是隔离的进程 | | **容器 vs 虚拟机** | 共享内核,更轻量,但隔离性较弱 | | **存储层** | 可写层随容器删除而消失 | | **数据持久化** | 使用 Volume 或 Bind Mount | | **生命周期** | 与主进程(PID 1)绑定 | 理解了镜像和容器,接下来让我们学习[仓库](repository.md)——存储和分发镜像的服务。 ## 延伸阅读 - [启动容器](../05_container/run.md):详细的容器启动选项 - [后台运行](../05_container/daemon.md):理解容器为什么会"立即退出" - [进入容器](../05_container/attach_exec.md):如何操作运行中的容器 - [数据管理](../07_data_network/README.md):Volume 和数据持久化详解