diff --git a/README.md b/README.md index 7d1ae82..9d35957 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![](https://img.shields.io/github/stars/yeasy/docker_practice.svg?style=social&label=Stars)](https://github.com/yeasy/docker_practice) [![](https://img.shields.io/github/release/yeasy/docker_practice/all.svg)](https://github.com/yeasy/docker_practice/releases) [![](https://img.shields.io/badge/Based-Docker%20CE%20v29.x-blue.svg)](https://github.com/docker/docker-ce) [![](https://img.shields.io/badge/Docker%20%E6%8A%80%E6%9C%AF%E5%85%A5%E9%97%A8%E4%B8%8E%E5%AE%9E%E6%88%98-jd.com-red.svg)][1] -**v1.4.3** +**v1.4.4** [Docker](https://www.docker.com) 是个划时代的开源项目,它彻底释放了计算虚拟化的威力,极大提高了应用的维护效率,降低了云计算应用开发的成本!使用 Docker,可以让应用的部署、测试和分发都变得前所未有的高效和轻松! diff --git a/basic_concept/container.md b/basic_concept/container.md index 2ae8273..c7fc3c8 100644 --- a/basic_concept/container.md +++ b/basic_concept/container.md @@ -1,13 +1,246 @@ # Docker 容器 -镜像(`Image`)和容器(`Container`)的关系,就像是面向对象程序设计中的 `类` 和 `实例` 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。 +## 一句话理解容器 -容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 [命名空间](https://en.wikipedia.org/wiki/Linux_namespaces)。因此容器可以拥有自己的 `root` 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。 +> **容器是镜像的运行实例。如果把镜像比作程序,那么容器就是进程。** -前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 **容器存储层**。 +用面向对象编程的术语来说:**镜像是类(Class),容器是对象(Instance)**。 -容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。 +- 一个镜像可以创建多个容器 +- 每个容器相互独立,互不影响 +- 容器可以被创建、启动、停止、删除、暂停 -按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 [数据卷(Volume)](../data_management/volume.md)、或者 [绑定宿主目录](../data_management/bind-mounts.md),在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。 +## 容器的本质 -数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。 +> 💡 **笔者认为,理解这一点是理解 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)](../data_management/volume.md)** | Docker 管理的存储 | 数据库、应用数据 | +| **[绑定挂载(Bind Mount)](../data_management/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 +``` + +详细解释请参考[后台运行](../container/daemon.md)章节。 + +## 容器的隔离性 + +Docker 容器通过以下 Namespace 实现隔离: + +| Namespace | 隔离内容 | 效果 | +|-----------|---------|------| +| **PID** | 进程 ID | 容器内 PID 1 是应用进程,看不到宿主机其他进程 | +| **NET** | 网络 | 独立的网络栈、IP 地址、端口 | +| **MNT** | 文件系统 | 独立的根目录和挂载点 | +| **UTS** | 主机名 | 独立的主机名和域名 | +| **IPC** | 进程间通信 | 独立的信号量、消息队列 | +| **USER** | 用户 | 独立的用户和组 ID | + +> 想深入了解?请阅读[底层实现 - 命名空间](../underly/namespace.md)。 + +## 本章小结 + +| 概念 | 要点 | +|------|------| +| **容器是什么** | 镜像的运行实例,本质是隔离的进程 | +| **容器 vs 虚拟机** | 共享内核,更轻量,但隔离性较弱 | +| **存储层** | 可写层随容器删除而消失 | +| **数据持久化** | 使用 Volume 或 Bind Mount | +| **生命周期** | 与主进程(PID 1)绑定 | + +理解了镜像和容器,接下来让我们学习[仓库](repository.md)——存储和分发镜像的服务。 + +## 延伸阅读 + +- [启动容器](../container/run.md):详细的容器启动选项 +- [后台运行](../container/daemon.md):理解容器为什么会"立即退出" +- [进入容器](../container/attach_exec.md):如何操作运行中的容器 +- [数据管理](../data_management/README.md):Volume 和数据持久化详解 diff --git a/basic_concept/image.md b/basic_concept/image.md index 740f885..69c9635 100644 --- a/basic_concept/image.md +++ b/basic_concept/image.md @@ -1,15 +1,222 @@ # Docker 镜像 -我们都知道,操作系统分为 **内核** 和 **用户空间**。对于 `Linux` 而言,内核启动后,会挂载 `root` 文件系统为其提供用户空间支持。而 **Docker 镜像**(`Image`),就相当于是一个 `root` 文件系统。比如官方镜像 `ubuntu:24.04` 就包含了完整的一套 Ubuntu 24.04 最小系统的 `root` 文件系统。 +## 一句话理解镜像 -**Docker 镜像** 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 **不包含** 任何动态数据,其内容在构建之后也不会被改变。 +> **Docker 镜像是一个只读的模板,包含了运行应用所需的一切:代码、运行时、库、环境变量和配置文件。** -## 分层存储 +如果用一个类比:**镜像就像是一张光盘或 ISO 文件**。你可以用同一张光盘在不同电脑上安装系统,而光盘本身不会被修改。同样,一个镜像可以创建多个容器,而镜像本身保持不变。 -因为镜像包含操作系统完整的 `root` 文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 [Union FS](https://en.wikipedia.org/wiki/Union_mount) 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 `ISO` 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。 +## 镜像与操作系统的关系 -镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。 +我们都知道,操作系统分为**内核**和**用户空间**: -分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。 +``` +┌─────────────────────────────────────────────────────────────┐ +│ 用户空间 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 应用程序、工具、库、配置文件... │ │ +│ │ (这部分被打包成 Docker 镜像) │ │ +│ └─────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ Linux 内核 │ +│ (容器共享宿主机的内核) │ +└─────────────────────────────────────────────────────────────┘ +``` -关于镜像构建,将会在后续相关章节中做进一步的讲解。 +对于 Linux 而言,内核启动后会挂载 `root` 文件系统来提供用户空间支持。**Docker 镜像**本质上就是一个 `root` 文件系统。 + +例如,官方镜像 `ubuntu:24.04` 包含了一套完整的 Ubuntu 24.04 最小系统的 root 文件系统——但**不包含 Linux 内核**(因为容器共享宿主机的内核)。 + +## 镜像包含什么? + +Docker 镜像是一个特殊的文件系统,包含: + +| 内容类型 | 示例 | +|---------|------| +| **程序文件** | 应用二进制文件、Python/Node 解释器 | +| **库文件** | libc、OpenSSL、各种依赖库 | +| **配置文件** | nginx.conf、my.cnf 等 | +| **环境变量** | PATH、LANG 等预设值 | +| **元数据** | 启动命令、暴露端口、数据卷定义 | + +**关键特性**: +- ✅ 镜像是**只读**的 +- ✅ 镜像**不包含**动态数据 +- ✅ 镜像构建后**内容不会改变** + +## 分层存储:镜像的核心设计 + +### 为什么需要分层? + +笔者认为,分层存储是 Docker 最巧妙的设计之一。 + +假设你有三个应用,都基于 Ubuntu 运行: + +``` +传统方式(不分层): +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ App A │ │ App B │ │ App C │ +│ Ubuntu │ │ Ubuntu │ │ Ubuntu │ +│ 500MB │ │ 500MB │ │ 500MB │ +└─────────────┘ └─────────────┘ └─────────────┘ + 总计:1.5GB ❌ + +Docker 分层方式: +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ App A │ │ App B │ │ App C │ +│ 50MB │ │ 30MB │ │ 40MB │ +└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + └────────────────┼────────────────┘ + ▼ + ┌─────────────────┐ + │ Ubuntu │ + │ (共享)500MB │ + └─────────────────┘ + 总计:620MB ✅ +``` + +### 分层是如何工作的? + +笔者用一个实际的 Dockerfile 来解释分层: + +```docker +FROM ubuntu:24.04 # 第 1 层:基础系统(约 78MB) +RUN apt-get update # 第 2 层:更新包索引 +RUN apt-get install nginx # 第 3 层:安装 nginx +COPY app.conf /etc/nginx/ # 第 4 层:复制配置文件 +``` + +构建后的镜像结构: + +``` +┌─────────────────────────────────────┐ +│ 第 4 层: COPY app.conf (只读) │ ← 最新添加的层 +├─────────────────────────────────────┤ +│ 第 3 层: nginx 安装文件 (只读) │ +├─────────────────────────────────────┤ +│ 第 2 层: apt 缓存更新 (只读) │ +├─────────────────────────────────────┤ +│ 第 1 层: Ubuntu 基础系统 (只读) │ ← 基础镜像层 +└─────────────────────────────────────┘ +``` + +每一层的特点: +- **只读**:构建完成后不可修改 +- **可共享**:多个镜像可以共享相同的层 +- **有缓存**:未变化的层不会重新构建 + +### 分层存储的"陷阱" + +> ⚠️ **笔者特别提醒**:理解这一点可以帮你避免构建出臃肿的镜像。 + +**关键原理**:每一层的文件变化会被记录,但**删除操作只是标记,不会真正减小镜像体积**。 + +```docker +# 错误示范 ❌ +FROM ubuntu:24.04 +RUN apt-get update +RUN apt-get install -y build-essential # 安装编译工具(约 200MB) +RUN make && make install # 编译应用 +RUN apt-get remove build-essential # 试图删除编译工具 +# 结果:镜像仍然包含 200MB 的编译工具! +``` + +```docker +# 正确做法 ✅ +FROM ubuntu:24.04 +RUN apt-get update && \ + apt-get install -y build-essential && \ + make && make install && \ + apt-get remove -y build-essential && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* +# 在同一层完成安装、使用、清理 +``` + +### 查看镜像的分层 + +```bash +# 查看镜像的历史(每层的构建记录) +$ docker history nginx:latest + +IMAGE CREATED CREATED BY SIZE +a6bd71f48f68 2 weeks ago CMD ["nginx" "-g" "daemon off;"] 0B + 2 weeks ago STOPSIGNAL SIGQUIT 0B + 2 weeks ago EXPOSE map[80/tcp:{}] 0B + 2 weeks ago ENTRYPOINT ["/docker-entrypoint.sh"] 0B + 2 weeks ago COPY 30-tune-worker-processes.sh /docker-ent… 4.62kB +... +``` + +## 镜像的标识 + +Docker 镜像有多种标识方式: + +### 1. 镜像名称和标签 + +格式:`[仓库地址/]仓库名[:标签]` + +```bash +# 完整格式 +registry.example.com/myproject/myapp:v1.2.3 + +# 简写(使用 Docker Hub) +nginx:1.25 +ubuntu:24.04 + +# 省略标签(默认使用 latest) +nginx # 等同于 nginx:latest +``` + +### 2. 镜像 ID(Content-Addressable) + +每个镜像有一个基于内容计算的唯一 ID: + +```bash +$ docker images +REPOSITORY TAG IMAGE ID CREATED SIZE +nginx latest a6bd71f48f68 2 weeks ago 187MB +ubuntu 24.04 ca2b0f26964c 3 weeks ago 78.1MB +``` + +### 3. 镜像摘要(Digest) + +更精确的标识,基于镜像内容的 SHA256 哈希: + +```bash +$ docker images --digests +REPOSITORY TAG DIGEST IMAGE ID +nginx latest sha256:6db391d1c0cfb30588ba0bf72ea999404f2764184d8b8d10d89e8a9c6... a6bd71f48f68 +``` + +> 💡 笔者建议:在生产环境使用镜像摘要而非标签,因为标签可以被覆盖,但摘要是不可变的。 + +## 镜像的来源 + +Docker 镜像可以通过以下方式获取: + +| 方式 | 说明 | 示例 | +|------|------|------| +| **从 Registry 拉取** | 最常用的方式 | `docker pull nginx` | +| **从 Dockerfile 构建** | 自定义镜像 | `docker build -t myapp .` | +| **从容器提交** | 保存容器状态(不推荐) | `docker commit` | +| **从文件导入** | 离线传输 | `docker load < image.tar` | + +## 本章小结 + +| 概念 | 要点 | +|------|------| +| **镜像是什么** | 只读的应用模板,包含运行所需的一切 | +| **分层存储** | 多层叠加,共享基础层,节省空间 | +| **只读特性** | 构建后不可修改,保证一致性 | +| **层的陷阱** | 删除操作只是标记,不减小体积 | + +理解了镜像,接下来让我们学习[容器](container.md)——镜像的运行实例。 + +## 延伸阅读 + +- [获取镜像](../image/pull.md):从 Registry 下载镜像 +- [使用 Dockerfile 定制镜像](../image/build.md):创建自己的镜像 +- [Dockerfile 最佳实践](../appendix/best_practices.md):构建高质量镜像的技巧 +- [底层实现 - 联合文件系统](../underly/ufs.md):深入理解分层存储的技术原理 diff --git a/basic_concept/repository.md b/basic_concept/repository.md index d0fcc3c..7e748e4 100644 --- a/basic_concept/repository.md +++ b/basic_concept/repository.md @@ -1,29 +1,250 @@ # Docker Registry -镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,[Docker Registry](../repository/registry.md) 就是这样的服务。 +## 一句话理解 Registry -一个 **Docker Registry** 中可以包含多个 **仓库**(`Repository`);每个仓库可以包含多个 **标签**(`Tag`);每个标签对应一个镜像。 +> **Docker Registry 是存储和分发 Docker 镜像的服务,类似于代码的 GitHub 或包管理的 npm。** -通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 `<仓库名>:<标签>` 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 `latest` 作为默认标签。 +镜像构建完成后,可以在当前机器上运行。但如果需要在其他服务器上使用这个镜像,就需要一个集中的存储和分发服务——这就是 Docker Registry。 -以 [Ubuntu 镜像](https://hub.docker.com/_/ubuntu) 为例,`ubuntu` 是仓库的名字,其内包含有不同的版本标签,如,`22.04`, `24.04`。我们可以通过 `ubuntu:22.04`,或者 `ubuntu:24.04` 来具体指定所需哪个版本的镜像。如果忽略了标签,比如 `ubuntu`,那将视为 `ubuntu:latest`。 +## 核心概念 -仓库名经常以 *两段式路径* 形式出现,比如 `jwilder/nginx-proxy`,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。 +### Registry、仓库、标签的关系 -## Docker Registry 公开服务 +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Docker Registry │ +│ (如 Docker Hub) │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Repository(仓库): nginx │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ :latest │ │ :1.25 │ │ :1.24 │ │ :alpine │ ... │ │ +│ │ │ (tag) │ │ (tag) │ │ (tag) │ │ (tag) │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Repository(仓库): mysql │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ :latest │ │ :8.0 │ │ :5.7 │ ... │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` -Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。 +| 概念 | 说明 | 示例 | +|------|------|------| +| **Registry** | 存储镜像的服务 | Docker Hub、ghcr.io | +| **Repository(仓库)** | 同一软件的镜像集合 | `nginx`、`mysql`、`mycompany/myapp` | +| **Tag(标签)** | 仓库内的版本标识 | `latest`、`1.25`、`alpine` | -最常使用的 Registry 公开服务是官方的 [Docker Hub](https://hub.docker.com/),这也是默认的 Registry,并拥有大量的高质量的 [官方镜像](https://hub.docker.com/search?q=&type=image&image_filter=official)。除此以外,还有 Red Hat 的 [Quay.io](https://quay.io/repository/);Google 的 [Google Container Registry](https://cloud.google.com/container-registry/),[Kubernetes](https://kubernetes.io/) 的镜像使用的就是这个服务;代码托管平台 [GitHub](https://github.com) 推出的 [ghcr.io](https://docs.github.com/cn/packages/working-with-a-github-packages-registry/working-with-the-container-registry)。 +### 镜像的完整名称 -由于某些原因,在国内访问这些服务可能会比较慢。国内的一些云服务商提供了针对 Docker Hub 的镜像服务(`Registry Mirror`),这些镜像服务被称为 **加速器**。常见的有 [阿里云加速器](https://www.aliyun.com/product/acr?source=5176.11533457&userCode=8lx5zmtu)、[DaoCloud 加速器](https://www.daocloud.io/mirror#accelerator-doc) 等。使用加速器会直接从国内的地址下载 Docker Hub 的镜像,比直接从 Docker Hub 下载速度会提高很多。在 [安装 Docker](../install/mirror.md) 一节中有详细的配置方法。 +``` +[registry地址/][用户名/]仓库名[:标签] +``` -国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 [网易云镜像服务](https://c.163.com/hub#/m/library/)、[DaoCloud 镜像市场](https://hub.daocloud.io/)、[阿里云镜像库](https://www.aliyun.com/product/acr?source=5176.11533457&userCode=8lx5zmtu) 等。 +示例: -## 私有 Docker Registry +```bash +# 完整格式 +registry.example.com/mycompany/myapp:v1.2.3 +│ │ │ │ +│ │ │ └── 标签 +│ │ └── 仓库名 +│ └── 用户名/组织名 +└── Registry 地址 -除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了 [Docker Registry](https://hub.docker.com/_/registry/) 镜像,可以直接使用做为私有 Registry 服务。在 [私有仓库](../repository/registry.md) 一节中,会有进一步的搭建私有 Registry 服务的讲解。 +# Docker Hub 官方镜像(省略 registry 和用户名) +nginx:1.25 +ubuntu:24.04 -开源的 Docker Registry 镜像只提供了 [Docker Registry API](https://docs.docker.com/registry/spec/api/) 的服务端实现,足以支持 `docker` 命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。 +# Docker Hub 用户镜像 +jwilder/nginx-proxy:latest -除了官方的 Docker Registry 外,还有第三方软件实现了 Docker Registry API,甚至提供了用户界面以及一些高级功能。比如,[Harbor](https://github.com/goharbor/harbor) 和 [Sonatype Nexus](../repository/nexus3_registry.md)。 +# 其他 Registry +ghcr.io/username/myapp:v1.0 +gcr.io/google-containers/pause:3.6 +``` + +> 💡 **笔者提示**:如果不指定 Registry 地址,默认使用 Docker Hub。如果不指定标签,默认使用 `latest`。 + +## 公共 Registry 服务 + +### Docker Hub(默认) + +[Docker Hub](https://hub.docker.com/) 是最大的公共 Registry,也是 Docker 的默认 Registry。 + +**特点**: +- 拥有大量[官方镜像](https://hub.docker.com/search?q=&type=image&image_filter=official)(nginx、mysql、redis 等) +- 免费账户可以创建公开仓库 +- 付费账户支持私有仓库 + +```bash +# 从 Docker Hub 拉取镜像 +$ docker pull nginx # 官方镜像 +$ docker pull bitnami/redis # 第三方镜像 + +# 推送镜像到 Docker Hub +$ docker login +$ docker push username/myapp:v1.0 +``` + +### 其他公共 Registry + +| Registry | 地址 | 说明 | +|----------|------|------| +| **GitHub Container Registry** | ghcr.io | GitHub 提供,与 GitHub Actions 集成好 | +| **Google Container Registry** | gcr.io | Google Cloud 提供,Kubernetes 镜像常用 | +| **Quay.io** | quay.io | Red Hat 提供 | +| **阿里云容器镜像服务** | registry.cn-*.aliyuncs.com | 国内访问快 | +| **腾讯云容器镜像服务** | ccr.ccs.tencentyun.com | 国内访问快 | + +## 镜像加速器 + +由于网络原因,在国内直接访问 Docker Hub 可能会很慢。可以配置**镜像加速器**(Registry Mirror)来加速下载。 + +```json +// /etc/docker/daemon.json +{ + "registry-mirrors": [ + "https://your-accelerator-url" + ] +} +``` + +详细配置方法请参考[镜像加速器](../install/mirror.md)章节。 + +> ⚠️ **笔者提醒**:镜像加速器的可用性经常变化,使用前建议先测试是否可用。 + +## 私有 Registry + +对于企业用户,通常需要搭建私有 Registry 来存储内部镜像。 + +### 官方 Registry 镜像 + +Docker 官方提供了 [registry](https://hub.docker.com/_/registry/) 镜像,可以快速搭建私有 Registry: + +```bash +# 启动一个本地 Registry +$ docker run -d -p 5000:5000 --name registry registry:2 + +# 推送镜像到本地 Registry +$ docker tag myapp:v1.0 localhost:5000/myapp:v1.0 +$ docker push localhost:5000/myapp:v1.0 + +# 从本地 Registry 拉取 +$ docker pull localhost:5000/myapp:v1.0 +``` + +### 企业级解决方案 + +官方 Registry 功能较为基础,企业环境常用以下方案: + +| 方案 | 特点 | +|------|------| +| **[Harbor](https://goharbor.io/)** | CNCF 项目,功能全面(用户管理、漏洞扫描、镜像签名) | +| **[Nexus Repository](../repository/nexus3_registry.md)** | 支持多种制品类型(Docker、Maven、npm 等) | +| **云厂商服务** | 阿里云 ACR、腾讯云 TCR、AWS ECR 等 | + +笔者建议: +- 小团队:可以先用官方 Registry,够用即可 +- 中大型团队:推荐 Harbor,功能完善且开源免费 +- 已使用云服务:直接用云厂商的 Registry 服务更省心 + +## 镜像的推送和拉取 + +### 完整工作流程 + +``` +开发者机器 Registry 生产服务器 + │ │ │ + │ docker build │ │ + │ 构建镜像 │ │ + │ │ │ + │ docker push ─────────────▶ │ + │ 推送镜像 │ 存储镜像 │ + │ │ │ + │ │ ◀───────────── docker pull │ + │ │ 拉取镜像 │ + │ │ │ + │ │ docker run │ + │ │ 运行容器 │ +``` + +### 常用命令 + +```bash +# 登录 Registry +$ docker login # 登录 Docker Hub +$ docker login registry.example.com # 登录其他 Registry + +# 拉取镜像 +$ docker pull nginx:1.25 + +# 标记镜像(准备推送) +$ docker tag myapp:latest registry.example.com/myteam/myapp:v1.0 + +# 推送镜像 +$ docker push registry.example.com/myteam/myapp:v1.0 + +# 登出 +$ docker logout +``` + +## 镜像的安全性 + +### 使用官方镜像 + +Docker Hub 的[官方镜像](https://hub.docker.com/search?q=&type=image&image_filter=official)(标有 "Official Image" 标识)经过 Docker 团队审核,相对更安全。 + +```bash +# 官方镜像示例 +nginx # ✅ 官方 +mysql # ✅ 官方 +redis # ✅ 官方 + +# 第三方镜像(需要自行评估可信度) +bitnami/redis # ⚠️ 需要评估 +someuser/myapp # ⚠️ 需要评估 +``` + +### 镜像签名 + +使用 Docker Content Trust (DCT) 验证镜像来源: + +```bash +# 启用镜像签名验证 +$ export DOCKER_CONTENT_TRUST=1 + +# 此后的 pull/push 会验证签名 +$ docker pull nginx:latest +``` + +### 漏洞扫描 + +```bash +# 使用 Docker Scout 扫描镜像漏洞 +$ docker scout cves nginx:latest + +# 使用 Trivy(开源工具) +$ trivy image nginx:latest +``` + +## 本章小结 + +| 概念 | 要点 | +|------|------| +| **Registry** | 存储和分发镜像的服务 | +| **仓库(Repository)** | 同一软件的镜像集合 | +| **标签(Tag)** | 版本标识,默认为 latest | +| **Docker Hub** | 默认的公共 Registry | +| **私有 Registry** | 企业内部使用,推荐 Harbor | + +现在你已经了解了 Docker 的三个核心概念:[镜像](image.md)、[容器](container.md)和仓库。接下来,让我们开始[安装 Docker](../install/README.md),动手实践! + +## 延伸阅读 + +- [Docker Hub](../repository/dockerhub.md):Docker Hub 的详细使用 +- [私有仓库](../repository/registry.md):搭建私有 Registry +- [私有仓库高级配置](../repository/registry_auth.md):认证、TLS 配置 +- [镜像加速器](../install/mirror.md):配置镜像加速 diff --git a/compose/django.md b/compose/django.md index d65c941..d7a4cb8 100644 --- a/compose/django.md +++ b/compose/django.md @@ -2,41 +2,114 @@ > 本小节内容适合 `Python` 开发人员阅读。 -我们现在将使用 `Docker Compose` 配置并运行一个 `Django/PostgreSQL` 应用。 +本节将使用 Docker Compose 配置并运行一个 **Django + PostgreSQL** 应用。笔者不仅会介绍具体步骤,还会解释每个配置项的作用,以及开发环境和生产环境的差异。 -在一切工作开始前,需要先编辑好三个必要的文件。 +## 架构概览 -第一步,因为应用将要运行在一个满足所有环境依赖的 Docker 容器里面,那么我们可以通过编辑 `Dockerfile` 文件来指定 Docker 容器要安装内容。内容如下: +在开始之前,让我们先理解我们要构建的架构: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Docker Compose 网络 │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ web 服务 │ │ db 服务 │ │ +│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ +│ │ │ Django │ │──────│ │ PostgreSQL │ │ │ +│ │ │ 应用 │ │ :5432│ │ 数据库 │ │ │ +│ │ └───────────────┘ │ │ └───────────────┘ │ │ +│ │ :8000 │ │ │ │ +│ └──────────┬──────────┘ └─────────────────────┘ │ +│ │ │ +└─────────────┼───────────────────────────────────────────────┘ + │ + ▼ + localhost:8000 + (浏览器访问) +``` + +**关键点**: +- `web` 服务运行 Django 应用,对外暴露 8000 端口 +- `db` 服务运行 PostgreSQL 数据库,只在内部网络可访问 +- 两个服务通过 Docker Compose 自动创建的网络相互通信 +- `web` 服务可以通过服务名 `db` 访问数据库(Docker 内置 DNS) + +## 准备工作 + +创建一个项目目录并进入: + +```bash +$ mkdir django-docker && cd django-docker +``` + +我们需要创建三个文件:`Dockerfile`、`requirements.txt` 和 `docker-compose.yml`。 + +## Step 1: 创建 Dockerfile ```docker -FROM python:3 -ENV PYTHONUNBUFFERED 1 -RUN mkdir /code +FROM python:3.12-slim + +# 防止 Python 缓冲 stdout/stderr,让日志实时输出 +ENV PYTHONUNBUFFERED=1 + +# 设置工作目录 WORKDIR /code + +# 先复制依赖文件,利用 Docker 缓存加速构建 COPY requirements.txt /code/ -RUN pip install -r requirements.txt + +# 安装依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 复制项目代码 COPY . /code/ ``` -以上内容指定应用将使用安装了 Python 以及必要依赖包的镜像。更多关于如何编写 `Dockerfile` 文件的信息可以查看 [ Dockerfile 使用](../image/dockerfile/README.md)。 +**逐行解释**: -第二步,在 `requirements.txt` 文件里面写明需要安装的具体依赖包名。 +| 指令 | 作用 | 为什么这样写 | +|------|------|-------------| +| `FROM python:3.12-slim` | 基础镜像 | `slim` 版本比完整版小很多,但包含运行 Python 所需的一切 | +| `ENV PYTHONUNBUFFERED=1` | 关闭输出缓冲 | 让 `print()` 和日志立即显示,便于调试 | +| `WORKDIR /code` | 设置工作目录 | 后续命令都在此目录执行 | +| `COPY requirements.txt` 在前 | 分层复制 | 只有 requirements.txt 变化时才重新安装依赖,加速构建 | +| `--no-cache-dir` | 不缓存 pip 下载 | 减小镜像体积 | -```bash -Django>=4.0,<5.0 -psycopg2-binary>=2.9,<3.0 +> 💡 **笔者建议**:总是将变化频率低的文件先复制,变化频率高的后复制。这样可以最大化利用 Docker 的构建缓存。 + +## Step 2: 创建 requirements.txt + +```txt +Django>=5.0,<6.0 +psycopg[binary]>=3.1,<4.0 +gunicorn>=21.0,<22.0 ``` -第三步,`docker-compose.yml` 文件将把所有的东西关联起来。它描述了应用的构成(一个 web 服务和一个数据库)、使用的 Docker 镜像、镜像之间的连接、挂载到容器的卷,以及服务开放的端口。 +**依赖说明**: + +| 包名 | 作用 | +|------|------| +| `Django` | Web 框架 | +| `psycopg[binary]` | PostgreSQL 数据库驱动(推荐使用 psycopg 3) | +| `gunicorn` | 生产环境 WSGI 服务器(可选,开发时可不用) | + +## Step 3: 创建 docker-compose.yml ```yaml - services: - db: - image: postgres + image: postgres:16 environment: - POSTGRES_PASSWORD: 'postgres' + POSTGRES_DB: django_db + POSTGRES_USER: django_user + POSTGRES_PASSWORD: django_password + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U django_user -d django_db"] + interval: 5s + timeout: 5s + retries: 5 web: build: . @@ -45,76 +118,222 @@ services: - .:/code ports: - "8000:8000" + depends_on: + db: + condition: service_healthy + environment: + DATABASE_URL: postgres://django_user:django_password@db:5432/django_db + +volumes: + postgres_data: ``` -查看 [`docker-compose.yml` 章节](compose_file.md) 了解更多详细的工作机制。 +**配置详解**: -现在我们就可以使用 `docker compose run` 命令启动一个 `Django` 应用了。 +### db 服务 + +```yaml +db: + image: postgres:16 # 使用官方 PostgreSQL 16 镜像 + environment: + POSTGRES_DB: django_db # 创建的数据库名 + POSTGRES_USER: django_user # 数据库用户 + POSTGRES_PASSWORD: django_password # 数据库密码 + volumes: + - postgres_data:/var/lib/postgresql/data # 持久化数据 + healthcheck: # 健康检查,确保数据库就绪 + test: ["CMD-SHELL", "pg_isready -U django_user -d django_db"] + interval: 5s +``` + +> ⚠️ **笔者提醒**:`volumes` 配置很重要!没有它,每次容器重启数据都会丢失。笔者见过不少新手因为忘记这一步,导致开发数据全部丢失。 + +### web 服务 + +```yaml +web: + build: . # 从当前目录的 Dockerfile 构建 + command: python manage.py runserver # 启动 Django 开发服务器 + volumes: + - .:/code # 挂载代码目录,支持热更新 + ports: + - "8000:8000" # 映射端口 + depends_on: + db: + condition: service_healthy # 等待数据库健康后再启动 +``` + +**关键配置说明**: + +| 配置项 | 作用 | 笔者建议 | +|--------|------|---------| +| `volumes: .:/code` | 代码挂载 | 开发时必备,修改代码无需重新构建镜像 | +| `depends_on` + `healthcheck` | 启动顺序 | 确保数据库就绪后 Django 才启动,避免连接错误 | +| `environment` | 环境变量 | 推荐用环境变量管理配置,避免硬编码 | + +## Step 4: 创建 Django 项目 + +运行以下命令创建新的 Django 项目: ```bash -$ docker compose run web django-admin startproject django_example . +$ docker compose run --rm web django-admin startproject mysite . ``` -由于 web 服务所使用的镜像并不存在,所以 Compose 会首先使用 `Dockerfile` 为 web 服务构建一个镜像,接着使用这个镜像在容器里运行 `django-admin startproject django_example` 指令。 +**命令解释**: +- `docker compose run`:运行一次性命令 +- `--rm`:命令执行后删除临时容器 +- `web`:在 web 服务环境中执行 +- `django-admin startproject mysite .`:在当前目录创建 Django 项目 -这将在当前目录生成一个 `Django` 应用。 +生成的目录结构: -```bash -$ ls -Dockerfile docker-compose.yml django_example manage.py requirements.txt +``` +django-docker/ +├── docker-compose.yml +├── Dockerfile +├── requirements.txt +├── manage.py +└── mysite/ + ├── __init__.py + ├── settings.py + ├── urls.py + ├── asgi.py + └── wsgi.py ``` -如果你的系统是 Linux,记得更改文件权限。 +> 💡 **Linux 用户注意**:如果遇到权限问题,执行 `sudo chown -R $USER:$USER .` -```bash -$ sudo chown -R $USER:$USER . -``` +## Step 5: 配置数据库连接 -首先,我们要为应用设置好数据库的连接信息。用以下内容替换 `django_example/settings.py` 文件中 `DATABASES = ...` 定义的节点内容。 +修改 `mysite/settings.py`,配置数据库连接: ```python +import os + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'postgres', - 'USER': 'postgres', - 'HOST': 'db', + 'NAME': os.environ.get('POSTGRES_DB', 'django_db'), + 'USER': os.environ.get('POSTGRES_USER', 'django_user'), + 'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'django_password'), + 'HOST': 'db', # Docker Compose 服务名 'PORT': 5432, - 'PASSWORD': 'postgres', } } + +# 允许的主机(开发环境) +ALLOWED_HOSTS = ['*'] ``` -这些信息是在 [postgres](https://hub.docker.com/_/postgres/) 镜像固定设置好的。然后,运行 `docker compose up` : +**为什么 HOST 是 `db` 而不是 `localhost`?** + +在 Docker Compose 中,各服务通过服务名相互访问。Docker 内置的 DNS 会将 `db` 解析为 db 服务容器的 IP 地址。这是 Docker Compose 的核心功能之一。 + +## Step 6: 启动应用 ```bash $ docker compose up - -django_db_1 is up-to-date -Creating django_web_1 ... -Creating django_web_1 ... done -Attaching to django_db_1, django_web_1 -db_1 | The files belonging to this database system will be owned by user "postgres". -db_1 | This user must also own the server process. -db_1 | -db_1 | The database cluster will be initialized with locale "en_US.utf8". -db_1 | The default database encoding has accordingly been set to "UTF8". -db_1 | The default text search configuration will be set to "english". - -web_1 | Performing system checks... -web_1 | -web_1 | System check identified no issues (0 silenced). -web_1 | -web_1 | November 23, 2024 - 06:21:19 -web_1 | Django version 4.2, using settings 'django_example.settings' -web_1 | Starting development server at http://0.0.0.0:8000/ -web_1 | Quit the server with CONTROL-C. ``` -这个 `Django` 应用已经开始在你的 Docker 守护进程里监听着 `8000` 端口了。打开 `127.0.0.1:8000` 即可看到 `Django` 欢迎页面。 +你会看到: +1. 首先构建 web 镜像(第一次运行) +2. 启动 db 服务,等待健康检查通过 +3. 启动 web 服务 -你还可以在 Docker 上运行其它的管理命令,例如对于同步数据库结构这种事,在运行完 `docker compose up` 后,在另外一个终端进入文件夹运行以下命令即可: +``` +db-1 | PostgreSQL init process complete; ready for start up. +db-1 | LOG: database system is ready to accept connections +web-1 | Watching for file changes with StatReloader +web-1 | Starting development server at http://0.0.0.0:8000/ +``` + +打开浏览器访问 http://localhost:8000,可以看到 Django 欢迎页面! + +## 常用开发命令 + +在另一个终端窗口执行: ```bash -$ docker compose run web python manage.py migrate +# 执行数据库迁移 +$ docker compose exec web python manage.py migrate + +# 创建超级用户 +$ docker compose exec web python manage.py createsuperuser + +# 进入 Django shell +$ docker compose exec web python manage.py shell + +# 进入 PostgreSQL 命令行 +$ docker compose exec db psql -U django_user -d django_db ``` + +> 💡 笔者建议使用 `exec` 而不是 `run`。`exec` 在已运行的容器中执行命令,`run` 会创建新容器。 + +## 常见问题排查 + +### Q1: 数据库连接失败 + +**错误信息**:`django.db.utils.OperationalError: could not connect to server` + +**可能原因与解决方案**: + +| 原因 | 解决方案 | +|------|---------| +| 数据库还没启动完成 | 使用 `depends_on` + `healthcheck` | +| HOST 配置错误 | 确保使用服务名 `db` 而不是 `localhost` | +| 网络未创建 | 运行 `docker compose down` 后重新 `up` | + +```bash +# 调试:检查数据库是否正常运行 +$ docker compose ps +$ docker compose logs db +``` + +### Q2: 代码修改没有生效 + +**可能原因**: + +1. **开发服务器没有自动重载**:确保使用 `runserver` 而不是 `gunicorn` +2. **Volume 挂载问题**:检查 `docker-compose.yml` 中的 volumes 配置 +3. **缓存问题**:尝试 `docker compose restart web` + +### Q3: 权限问题(Linux) + +```bash +# 如果容器内创建的文件 root 用户所有 +$ sudo chown -R $USER:$USER . +``` + +## 开发 vs 生产:关键差异 + +笔者特别提醒,本节的配置是**开发环境**配置。生产环境需要以下调整: + +| 配置项 | 开发环境 | 生产环境 | +|--------|---------|---------| +| **Web 服务器** | `runserver` | `gunicorn` + Nginx | +| **DEBUG** | `True` | `False` | +| **密码管理** | 明文写在配置 | 使用 Docker Secrets 或环境变量 | +| **Volume** | 挂载代码目录 | 代码直接 COPY 进镜像 | +| **ALLOWED_HOSTS** | `['*']` | 具体域名 | + +**生产环境 docker-compose.yml 示例**: + +```yaml +# docker-compose.prod.yml +services: + web: + build: . + command: gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 + # 不挂载代码,使用镜像内的代码 + environment: + DEBUG: 'False' + ALLOWED_HOSTS: 'example.com,www.example.com' + # ... +``` + +## 延伸阅读 + +- [Compose 模板文件详解](compose_file.md):深入理解 docker-compose.yml 的所有配置项 +- [使用 WordPress](wordpress.md):另一个 Compose 实战案例 +- [Dockerfile 最佳实践](../appendix/best_practices.md):构建更小、更安全的镜像 +- [数据管理](../data_management/README.md):Volume 和数据持久化详解 diff --git a/compose/rails.md b/compose/rails.md index 763930a..9e39e34 100644 --- a/compose/rails.md +++ b/compose/rails.md @@ -1,117 +1,268 @@ # 使用 Rails -> 本小节内容适合 `Ruby` 开发人员阅读。 +> 本小节内容适合 Ruby 开发人员阅读。 -我们现在将使用 `Compose` 配置并运行一个 `Rails/PostgreSQL` 应用。 +本节使用 Docker Compose 配置并运行一个 **Rails + PostgreSQL** 应用。 -在一切工作开始前,需要先设置好三个必要的文件。 +## 架构概览 -首先,因为应用将要运行在一个满足所有环境依赖的 Docker 容器里面,那么我们可以通过编辑 `Dockerfile` 文件来指定 Docker 容器要安装内容。内容如下: - -```docker -FROM ruby -RUN apt-get update -qq && apt-get install -y build-essential libpq-dev -RUN mkdir /myapp -WORKDIR /myapp -ADD Gemfile /myapp/Gemfile -RUN bundle install -ADD . /myapp +``` +┌─────────────────────────────────────────────────────────────┐ +│ Docker Compose 网络 │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ web 服务 │ │ db 服务 │ │ +│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ +│ │ │ Rails │ │──────│ │ PostgreSQL │ │ │ +│ │ │ 应用 │ │ :5432│ │ 数据库 │ │ │ +│ │ └───────────────┘ │ │ └───────────────┘ │ │ +│ │ :3000 │ │ │ │ +│ └──────────┬──────────┘ └─────────────────────┘ │ +│ │ │ +└─────────────┼───────────────────────────────────────────────┘ + │ + ▼ + localhost:3000 ``` -以上内容指定应用将使用安装了 Ruby、Bundler 以及其依赖件的镜像。更多关于如何编写 Dockerfile 文件的信息可以查看 [Dockerfile 使用](../image/dockerfile/README.md)。 +## 准备工作 -下一步,我们需要一个引导加载 Rails 的文件 `Gemfile` 。 等一会儿它还会被 `rails new` 命令覆盖重写。 +创建项目目录: ```bash -source 'https://rubygems.org' -gem 'rails', '4.0.2' +$ mkdir rails-docker && cd rails-docker ``` -最后,`docker-compose.yml` 文件才是最神奇的地方。 `docker-compose.yml` 文件将把所有的东西关联起来。它描述了应用的构成(一个 web 服务和一个数据库)、每个镜像的来源(数据库运行在使用预定义的 PostgreSQL 镜像,web 应用侧将从本地目录创建)、镜像之间的连接,以及服务开放的端口。 +需要创建三个文件:`Dockerfile`、`Gemfile` 和 `docker-compose.yml`。 + +## Step 1: 创建 Dockerfile + +```docker +FROM ruby:3.2 + +# 安装系统依赖 +RUN apt-get update -qq && \ + apt-get install -y build-essential libpq-dev nodejs && \ + rm -rf /var/lib/apt/lists/* + +# 设置工作目录 +WORKDIR /myapp + +# 先复制 Gemfile,利用缓存加速构建 +COPY Gemfile /myapp/Gemfile +COPY Gemfile.lock /myapp/Gemfile.lock +RUN bundle install + +# 复制应用代码 +COPY . /myapp +``` + +**配置说明**: + +| 指令 | 作用 | +|------|------| +| `build-essential` | 编译原生扩展所需 | +| `libpq-dev` | PostgreSQL 客户端库 | +| `nodejs` | Rails Asset Pipeline 需要 | +| 先复制 Gemfile | 只有依赖变化时才重新 `bundle install` | + +## Step 2: 创建 Gemfile + +创建一个初始的 `Gemfile`,稍后会被 `rails new` 覆盖: + +```ruby +source 'https://rubygems.org' +gem 'rails', '~> 7.1' +``` + +创建空的 `Gemfile.lock`: + +```bash +$ touch Gemfile.lock +``` + +## Step 3: 创建 docker-compose.yml ```yaml - services: - db: - image: postgres - ports: - - "5432" + image: postgres:16 + environment: + POSTGRES_PASSWORD: password + volumes: + - postgres_data:/var/lib/postgresql/data web: build: . - command: bundle exec rackup -p 3000 + command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp ports: - "3000:3000" + depends_on: + - db + environment: + DATABASE_URL: postgres://postgres:password@db:5432/myapp_development + +volumes: + postgres_data: ``` -所有文件就绪后,我们就可以通过使用 `docker compose run` 命令生成应用的骨架了。 +**配置详解**: + +| 配置项 | 说明 | +|--------|------| +| `rm -f tmp/pids/server.pid` | 清理上次异常退出留下的 PID 文件 | +| `volumes: .:/myapp` | 挂载代码目录,支持热更新 | +| `depends_on: db` | 确保数据库先启动 | +| `DATABASE_URL` | Rails 12-factor 风格的数据库配置 | + +## Step 4: 生成 Rails 项目 + +使用 `docker compose run` 生成项目骨架: ```bash -$ docker compose run web rails new . --force --database=postgresql --skip-bundle +$ docker compose run --rm web rails new . --force --database=postgresql --skip-bundle ``` -`Compose` 会先使用 `Dockerfile` 为 web 服务创建一个镜像,接着使用这个镜像在容器里运行 `rails new ` 和它之后的命令。一旦这个命令运行完后,应该就可以看一个崭新的应用已经生成了。 +**命令解释**: +- `--rm`:执行后删除临时容器 +- `--force`:覆盖已存在的文件 +- `--database=postgresql`:配置使用 PostgreSQL +- `--skip-bundle`:暂不安装依赖(稍后统一安装) + +生成的目录结构: ```bash $ ls -Dockerfile app docker-compose.yml tmp -Gemfile bin lib vendor -Gemfile.lock condocker-compose log -README.rdoc condocker-compose.ru public -Rakefile db test +Dockerfile Gemfile Rakefile config lib tmp +Gemfile.lock README.md app config.ru log vendor +docker-compose.yml bin db public ``` -在新的 `Gemfile` 文件去掉加载 `therubyracer` 的行的注释,这样我们便可以使用 Javascript 运行环境: +> ⚠️ **Linux 用户**:如遇权限问题,执行 `sudo chown -R $USER:$USER .` -```bash -gem 'therubyracer', platforms: :ruby -``` +## Step 5: 重新构建镜像 -现在我们已经有一个新的 `Gemfile` 文件,需要再重新创建镜像。(这个会步骤会改变 Dockerfile 文件本身,所以需要重建一次)。 +由于生成了新的 Gemfile,需要重新构建镜像以安装完整依赖: ```bash $ docker compose build ``` -应用现在就可以启动了,但配置还未完成。Rails 默认读取的数据库目标是 `localhost` ,我们需要手动指定容器的 `db` 。同样的,还需要把用户名修改成和 postgres 镜像预定的一致。 -打开最新生成的 `database.yml` 文件。用以下内容替换: +## Step 6: 配置数据库连接 -```bash -development: &default +修改 `config/database.yml`: + +```yaml +default: &default adapter: postgresql encoding: unicode - database: postgres - pool: 5 - username: postgres - password: - host: db + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + url: <%= ENV['DATABASE_URL'] %> + +development: + <<: *default test: <<: *default database: myapp_test + +production: + <<: *default ``` -现在就可以启动应用了。 +> 💡 使用 `DATABASE_URL` 环境变量配置数据库,符合 12-factor 应用原则,便于在不同环境间切换。 + +## Step 7: 启动应用 ```bash $ docker compose up ``` -如果一切正常,你应该可以看到 PostgreSQL 的输出,几秒后可以看到这样的重复信息: +输出示例: -```bash -myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick 1.3.1 -myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.0.0 (2013-11-22) [x86_64-linux-gnu] -myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port=3000 +``` +db-1 | PostgreSQL init process complete; ready for start up. +db-1 | LOG: database system is ready to accept connections +web-1 | => Booting Puma +web-1 | => Rails 7.1.0 application starting in development +web-1 | => Run `bin/rails server --help` for more startup options +web-1 | Puma starting in single mode... +web-1 | * Listening on http://0.0.0.0:3000 ``` -最后, 我们需要做的是创建数据库,打开另一个终端,运行: +## Step 8: 创建数据库 + +在另一个终端执行: ```bash -$ docker compose run web rake db:create +$ docker compose exec web rails db:create +Created database 'myapp_development' +Created database 'myapp_test' ``` -这个 web 应用已经开始在你的 docker 守护进程里面监听着 3000 端口了。 +访问 http://localhost:3000 查看 Rails 欢迎页面。 + +## 常用开发命令 + +```bash +# 数据库迁移 +$ docker compose exec web rails db:migrate + +# Rails 控制台 +$ docker compose exec web rails console + +# 运行测试 +$ docker compose exec web rails test + +# 生成脚手架 +$ docker compose exec web rails generate scaffold Post title:string body:text + +# 进入容器 Shell +$ docker compose exec web bash +``` + +## 常见问题 + +### Q: 数据库连接失败 + +检查 `DATABASE_URL` 环境变量格式是否正确,确保 db 服务已启动: + +```bash +$ docker compose ps +$ docker compose logs db +``` + +### Q: server.pid 文件导致启动失败 + +错误信息:`A server is already running` + +已在 command 中添加 `rm -f tmp/pids/server.pid` 处理。如仍有问题: + +```bash +$ docker compose exec web rm -f tmp/pids/server.pid +``` + +### Q: Gem 安装失败 + +可能需要更新 bundler 或清理缓存: + +```bash +$ docker compose run --rm web bundle update +``` + +## 开发 vs 生产 + +| 配置项 | 开发环境 | 生产环境 | +|--------|---------|---------| +| Rails 服务器 | Puma (开发模式) | Puma + Nginx | +| 代码挂载 | 使用 volumes | 代码打包进镜像 | +| 静态资源 | 动态编译 | 预编译 (`rails assets:precompile`) | +| 数据库密码 | 明文配置 | 使用 Secrets 管理 | + +## 延伸阅读 + +- [使用 Django](django.md):Python Web 框架实战 +- [Compose 模板文件](compose_file.md):配置详解 +- [数据管理](../data_management/README.md):数据持久化 diff --git a/compose/wordpress.md b/compose/wordpress.md index 99efe04..8e8c0fd 100644 --- a/compose/wordpress.md +++ b/compose/wordpress.md @@ -1,51 +1,210 @@ -# 使用 WordPress +# 实战 WordPress -> 本小节内容适合 `PHP` 开发人员阅读。 +## 简介 -`Compose` 可以很便捷的让 `Wordpress` 运行在一个独立的环境中。 +WordPress 是全球最流行的内容管理系统(CMS)。使用 Docker Compose 可以在几分钟内搭建一个包含数据库、Web 服务和持久化存储的生产级 WordPress 环境。 -## 创建空文件夹 +--- -假设新建一个名为 `wordpress` 的文件夹,然后进入这个文件夹。 +## 项目结构 -## 创建 `docker-compose.yml` 文件 - -[`docker-compose.yml`](https://github.com/yeasy/docker_practice/blob/master/compose/demo/wordpress/docker-compose.yml) 文件将开启一个 `wordpress` 服务和一个独立的 `MySQL` 实例: - -```yaml - -services: - - db: - image: mysql:8.0 - command: - - --default_authentication_plugin=mysql_native_password - - --character-set-server=utf8mb4 - - --collation-server=utf8mb4_unicode_ci - volumes: - - db_data:/var/lib/mysql - restart: always - environment: - MYSQL_ROOT_PASSWORD: somewordpress - MYSQL_DATABASE: wordpress - MYSQL_USER: wordpress - MYSQL_PASSWORD: wordpress - - wordpress: - depends_on: - - db - image: wordpress:latest - ports: - - "8000:80" - restart: always - environment: - WORDPRESS_DB_HOST: db:3306 - WORDPRESS_DB_USER: wordpress - WORDPRESS_DB_PASSWORD: wordpress -volumes: - db_data: +``` +wordpress/ +├── docker-compose.yml +├── .env # 环境变量(敏感信息) +└── nginx/ # 可选:反向代理配置 + └── nginx.conf ``` -## 构建并运行项目 +--- -运行 `docker compose up -d` Compose 就会拉取镜像再创建我们所需要的镜像,然后启动 `wordpress` 和数据库容器。 接着浏览器访问 `127.0.0.1:8000` 端口就能看到 `WordPress` 安装界面了。 +## 编写 `docker-compose.yml` + +这是一个生产可用的最小化配置: + +```yaml +services: + # 数据库服务 + db: + image: mysql:8.0 + container_name: wordpress_db + restart: always + command: + # 使用原生密码认证(旧版 WP 兼容性) + - --default-authentication-plugin=mysql_native_password + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + MYSQL_DATABASE: wordpress + MYSQL_USER: wordpress + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - db_data:/var/lib/mysql + networks: + - wp_net + + # WordPress 服务 + wordpress: + image: wordpress:latest + container_name: wordpress_app + restart: always + ports: + - "8000:80" + environment: + WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: ${DB_PASSWORD} + WORDPRESS_DB_NAME: wordpress + volumes: + - wp_data:/var/www/html + # 增加上传文件大小限制 + - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini + depends_on: + - db + networks: + - wp_net + +volumes: + db_data: # 数据库持久化 + wp_data: # WordPress 文件(插件/主题/上传)持久化 + +networks: + wp_net: +``` + +--- + +## 配置文件详解 + +### 1. 环境变量 (.env) + +为了安全,不要在 `docker-compose.yml` 中直接写密码。创建 `.env` 文件: + +```ini +DB_ROOT_PASSWORD=somestrongrootpassword +DB_PASSWORD=somestronguserpassword +``` + +Compose 会自动读取此同级目录下的文件。 + +### 2. 数据持久化 + +我们定义了两个命名卷: +- `db_data`: 确保 MySQL 容器重建后数据不丢失 +- `wp_data`: 保存 WordPress 的核心文件、插件、主题和上传的媒体文件 + +### 3. PHP 配置优化 + +默认的 WordPress 镜像上传文件限制较小(通常 2MB)。创建 `uploads.ini`: + +```ini +file_uploads = On +memory_limit = 256M +upload_max_filesize = 64M +post_max_size = 64M +max_execution_time = 600 +``` + +--- + +## 启动与运行 + +1. 启动服务: + +```bash +$ docker compose up -d +``` + +2. 访问安装界面: + 打开浏览器访问 `http://localhost:8000` + +3. 查看日志: + +```bash +$ docker compose logs -f +``` + +--- + +## 生产环境最佳实践 + +### 1. 数据库备份 + +不要只依赖 Volume。建议定期备份数据库: + +```bash +# 导出 SQL +$ docker exec wordpress_db mysqldump -u wordpress -pwordpress wordpress > backup.sql +``` + +或者添加一个自动备份容器: + +```yaml + backup: + image: tiredofit/db-backup + volumes: + - ./backups:/backup + environment: + - DB_TYPE=mysql + - DB_HOST=db + - DB_NAME=wordpress + - DB_USER=wordpress + - DB_PASS=${DB_PASSWORD} + - DB_DUMP_FREQ=1440 # 每天备份一次 + depends_on: + - db + networks: + - wp_net +``` + +### 2. 使用 Nginx 反向代理 + +在生产环境中,不要直接暴露 WordPress 端口,而是通过 Nginx 进行反向代理并配置 SSL。 + +### 3. 使用 Redis 缓存 + +WordPress 支持 Redis 缓存以提高性能。 + +```yaml + redis: + image: redis:alpine + restart: always + networks: + - wp_net +``` + +在 WordPress 容器环境变量中添加: +```yaml + WORDPRESS_REDIS_HOST: redis +``` +并安装 Redis Object Cache 插件。 + +--- + +## 常见问题 + +### Q: 数据库连接错误 + +**现象**:访问页面显示 "Error establishing a database connection"。 + +**排查**: +1. 检查 `docker compose logs wordpress` +2. 确认 `.env` 中的密码与 YAML 文件引用一致 +3. 确认 `WORDPRESS_DB_HOST` 也是 `db`(服务名) +4. MySQL 8.0 可能需要几秒钟启动,WordPress 会自动重试,稍等片刻即可。 + +### Q: 无法上传大文件 + +**解决**:确保挂载了 `uploads.ini` 配置,并且重启了容器: +```bash +$ docker compose restart wordpress +``` + +--- + +## 延伸阅读 + +- [Compose 模板文件](compose_file.md):深入了解配置项 +- [数据卷](../data_management/volume.md):理解数据持久化 +- [Docker Hub WordPress](https://hub.docker.com/_/wordpress):官方镜像文档 diff --git a/container/attach_exec.md b/container/attach_exec.md index aca5d99..b11db5c 100644 --- a/container/attach_exec.md +++ b/container/attach_exec.md @@ -1,56 +1,264 @@ # 进入容器 -在使用 `-d` 参数时,容器启动后会进入后台。 +## 为什么需要进入容器 -某些时候需要进入容器进行操作,包括使用 `docker attach` 命令或 `docker exec` 命令,推荐大家使用 `docker exec` 命令,原因会在下面说明。 +使用 `-d` 参数启动容器后,容器在后台运行。以下场景需要进入容器内部操作: -## `attach` 命令 +| 场景 | 示例 | +|------|------| +| **调试问题** | 查看日志、检查配置、排查错误 | +| **临时操作** | 执行数据库迁移、清理缓存 | +| **检查状态** | 查看进程、网络连接、文件系统 | +| **开发测试** | 交互式测试命令、验证环境 | -下面示例如何使用 `docker attach` 命令。 +## 两种进入方式 + +Docker 提供两种进入容器的命令: + +| 命令 | 推荐程度 | 特点 | +|------|---------|------| +| `docker exec` | ✅ **推荐** | 启动新进程,退出不影响容器 | +| `docker attach` | ⚠️ 谨慎使用 | 附加到主进程,退出可能停止容器 | + +--- + +## docker exec(推荐) + +### 基本用法 ```bash -$ docker run -dit ubuntu -243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550 +# 进入容器并启动交互式 shell +$ docker exec -it 容器名 /bin/bash -$ docker container ls -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia - -$ docker attach 243c -root@243c32535da7:/# +# 或使用 sh(适用于 Alpine 等精简镜像) +$ docker exec -it 容器名 /bin/sh ``` -*注意:* 如果从这个 stdin 中 exit,会导致容器的停止。 +### 参数说明 -## `exec` 命令 +| 参数 | 作用 | +|------|------| +| `-i` | 保持标准输入打开(interactive) | +| `-t` | 分配伪终端(TTY) | +| `-it` | 两者组合,获得完整交互体验 | +| `-u` | 指定用户(如 `-u root`) | +| `-w` | 指定工作目录 | +| `-e` | 设置环境变量 | -### `-i` `-t` 参数 - -`docker exec` 后边可以跟多个参数,这里主要说明 `-i` `-t` 参数。 - -只用 `-i` 参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。 - -当 `-i` `-t` 参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。 +### 示例 ```bash -$ docker run -dit ubuntu -69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6 +# 启动一个后台容器 +$ docker run -dit --name myubuntu ubuntu +69d137adef7a... -$ docker container ls -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -69d137adef7a ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds zealous_swirles +# 进入容器(交互式 shell) +$ docker exec -it myubuntu bash +root@69d137adef7a:/# ls +bin boot dev etc home lib ... +root@69d137adef7a:/# exit -$ docker exec -i 69d1 bash -ls -bin +# 容器仍在运行! +$ docker ps +CONTAINER ID IMAGE STATUS NAMES +69d137adef7a ubuntu Up 2 minutes myubuntu +``` + +### 执行单条命令 + +不进入交互模式,直接执行命令: + +```bash +# 查看容器内进程 +$ docker exec myubuntu ps aux + +# 查看配置文件 +$ docker exec myubuntu cat /etc/nginx/nginx.conf + +# 以 root 用户执行 +$ docker exec -u root myubuntu apt update +``` + +### 只用 -i 不用 -t 的区别 + +```bash +# 只用 -i:可以执行命令,但没有提示符 +$ docker exec -i myubuntu bash +ls # 输入命令 +bin # 输出结果 boot dev ... -$ docker exec -it 69d1 bash -root@69d137adef7a:/# +# 用 -it:有完整的终端体验 +$ docker exec -it myubuntu bash +root@69d137adef7a:/# # 有提示符 ``` -如果从这个 stdin 中 exit,不会导致容器的停止。这就是为什么推荐大家使用 `docker exec` 的原因。 +> 💡 通常使用 `-it` 组合。只有在脚本中需要通过管道传入命令时才只用 `-i`。 -更多参数说明请使用 `docker exec --help` 查看。 +--- + +## docker attach(谨慎使用) + +### 基本用法 + +```bash +$ docker attach 容器名 +``` + +### 工作原理 + +`attach` 会附加到容器的**主进程**(PID 1)的标准输入输出: + +``` +┌─────────────────────────────────────────┐ +│ 容器 │ +│ ┌─────────────────────────────────┐ │ +│ │ PID 1: /bin/bash (主进程) │◄───┼─── docker attach 附加到这里 +│ │ └─ 你的输入直接发送到主进程 │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────────┘ +``` + +### 示例 + +```bash +# 启动容器 +$ docker run -dit --name myubuntu ubuntu +243c32535da7... + +# 附加到容器 +$ docker attach myubuntu +root@243c32535da7:/# +``` + +### ⚠️ 重要警告 + +**从 attach 会话中输入 `exit` 或按 `Ctrl+D` 会导致容器停止!** + +```bash +$ docker attach myubuntu +root@243c32535da7:/# exit # 这会停止容器! + +$ docker ps +CONTAINER ID IMAGE STATUS NAMES +243c32535da7 ubuntu Exited (0) 2 seconds ago myubuntu +``` + +**原因**:attach 附加到主进程,退出主进程就等于退出容器。 + +### 安全退出 attach + +使用 `Ctrl+P` 然后 `Ctrl+Q` 可以从 attach 会话中**分离**,而不停止容器: + +```bash +$ docker attach myubuntu +root@243c32535da7:/# +# 按 Ctrl+P 然后 Ctrl+Q +read escape sequence + +$ docker ps # 容器仍在运行 +CONTAINER ID IMAGE STATUS NAMES +243c32535da7 ubuntu Up 5 minutes myubuntu +``` + +--- + +## exec vs attach 对比 + +| 特性 | docker exec | docker attach | +|------|-------------|---------------| +| **工作方式** | 在容器内启动新进程 | 附加到主进程 | +| **退出影响** | 不影响容器 | 可能停止容器 | +| **多终端** | 可以开多个 | 共享同一个会话 | +| **适用场景** | 调试、临时操作 | 查看主进程输出 | +| **推荐程度** | ✅ 推荐 | ⚠️ 特殊场景使用 | + +``` +docker exec docker attach +┌─────────────────────┐ ┌─────────────────────┐ +│ 容器 │ │ 容器 │ +│ ┌───────────────┐ │ │ ┌───────────────┐ │ +│ │ PID 1: nginx │ │ │ │ PID 1: bash │◄─┼── 附加到主进程 +│ ├───────────────┤ │ │ └───────────────┘ │ +│ │ PID 50: bash │◄─┼── 新进程 │ │ +│ └───────────────┘ │ │ │ +└─────────────────────┘ └─────────────────────┘ +退出 bash 不影响 nginx 退出 bash 容器停止 +``` + +--- + +## 最佳实践 + +### 1. 首选 docker exec + +```bash +# 进入容器调试 +$ docker exec -it myapp bash + +# 查看日志 +$ docker exec myapp tail -f /var/log/app.log + +# 执行数据库迁移 +$ docker exec myapp python manage.py migrate +``` + +### 2. 生产环境避免进入容器 + +笔者建议:生产环境应尽量避免进入容器直接操作,而是通过: +- 日志系统查看日志(如 `docker logs` 或集中式日志) +- 监控系统查看状态 +- 重新部署而非手动修改 + +### 3. 无 shell 镜像的处理 + +某些精简镜像(如基于 `scratch` 或 `distroless`)没有 shell: + +```bash +# 这会失败 +$ docker exec -it myapp bash +OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found + +# 解决方案:使用调试容器(Docker Desktop 或 Kubernetes debug) +$ docker debug myapp +``` + +--- + +## 常见问题 + +### Q: exec 进入后看不到其他终端的操作 + +这是正常的。exec 启动的是独立进程,多个 exec 会话互不影响。 + +### Q: 容器没有 bash + +尝试使用 sh: + +```bash +$ docker exec -it myapp /bin/sh +``` + +### Q: 需要 root 权限 + +```bash +$ docker exec -u root -it myapp bash +``` + +--- + +## 本章小结 + +| 需求 | 推荐命令 | +|------|---------| +| 进入容器调试 | `docker exec -it 容器名 bash` | +| 执行单条命令 | `docker exec 容器名 命令` | +| 查看主进程输出 | `docker attach 容器名`(慎用) | + +## 延伸阅读 + +- [后台运行](daemon.md):理解容器主进程 +- [查看容器](ls.md):列出和过滤容器 +- [容器日志](logs.md):查看容器输出 diff --git a/container/daemon.md b/container/daemon.md index 95e0214..ca2edda 100644 --- a/container/daemon.md +++ b/container/daemon.md @@ -1,10 +1,19 @@ # 后台运行 -更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 `-d` 参数来实现。 +在生产环境中,我们通常需要容器持续运行,不受终端关闭的影响。本节将深入讲解如何让容器在后台运行,以及理解容器生命周期的核心概念。 -下面举两个例子来说明一下。 +## 核心概念:前台 vs 后台 -如果不使用 `-d` 参数运行容器。 +当你在终端运行一个程序时,有两种模式: + +- **前台运行**:程序占用当前终端,输出直接显示,关闭终端程序就停止 +- **后台运行**:程序在后台执行,不占用终端,终端关闭也不影响程序 + +Docker 容器默认是**前台运行**的。使用 `-d`(detach)参数可以让容器在后台运行。 + +## 基本使用 + +### 前台运行(默认) ```bash $ docker run ubuntu:24.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" @@ -14,33 +23,196 @@ hello world hello world ``` -容器会把输出的结果 (STDOUT) 打印到宿主机上面 +容器会把输出的结果(STDOUT)打印到宿主机上面。此时: +- 终端被占用,无法执行其他命令 +- 按 `Ctrl+C` 会终止容器 +- 关闭终端窗口,容器也会停止 -如果使用了 `-d` 参数运行容器。 +### 后台运行(使用 -d 参数) ```bash $ docker run -d ubuntu:24.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" 77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a ``` -此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 `docker logs` 查看)。 +使用 `-d` 参数后: +- 容器在后台运行 +- 返回容器的完整 ID +- 终端立即释放,可以继续执行其他命令 +- 输出不会直接显示(需要用 `docker logs` 查看) -**注:** 容器是否会长久运行,是和 `docker run` 指定的命令有关,和 `-d` 参数无关。 +## 深入理解:容器为什么会"立即退出"? -使用 `-d` 参数启动后会返回一个唯一的 id,也可以通过 `docker container ls` 命令来查看容器信息。 +> **这是初学者最常遇到的困惑。** 理解这个问题,你就理解了 Docker 的核心设计理念。 + +很多人尝试这样启动容器: + +```bash +$ docker run -d ubuntu:24.04 +``` + +然后用 `docker ps` 查看,发现容器根本不在运行!这是为什么? + +### 核心原理:容器的生命周期与主进程绑定 ``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Docker 容器的生命周期 = 容器内 PID 1 进程的生命周期 │ +│ │ +│ 主进程启动 → 容器运行 │ +│ 主进程退出 → 容器停止 │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +当你运行 `docker run -d ubuntu:24.04` 时: +1. 容器启动 +2. 没有指定命令,默认执行 `/bin/bash` +3. 但没有交互式终端(没有 `-it` 参数),bash 发现没有输入源 +4. bash 立即退出 +5. 主进程退出,容器停止 + +**关键理解**: +- ❌ `-d` 参数**不是**让容器"一直运行" +- ✅ `-d` 参数是让容器"在后台运行",能运行多久取决于主进程 + +### 常见的"立即退出"场景 + +| 场景 | 原因 | 解决方案 | +|------|------|---------| +| `docker run -d ubuntu` | 默认 bash 无输入立即退出 | 指定长期运行的命令 | +| `docker run -d nginx` 后改了配置 | 配置错误导致 nginx 启动失败 | 查看 `docker logs` | +| 自定义镜像容器启动即退 | Dockerfile 的 CMD 执行完毕 | 确保 CMD 是前台进程 | + +## 查看后台容器 + +### 查看运行中的容器 + +```bash $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 77b2dc01fe0f ubuntu:24.04 /bin/sh -c 'while tr 2 minutes ago Up 1 minute agitated_wright ``` -要获取容器的输出信息,可以通过 `docker container logs` 命令。 +### 查看容器输出日志 ```bash -$ docker container logs [container ID or NAMES] +$ docker container logs 77b2dc01fe0f hello world hello world hello world -. . . +... ``` + +**实时查看日志**(类似 `tail -f`): + +```bash +$ docker container logs -f 77b2dc01fe0f +``` + +### 查看已停止的容器 + +```bash +$ docker container ls -a +``` + +加上 `-a` 参数可以看到所有容器,包括已停止的。这对于调试"容器启动即退出"的问题非常有用。 + +## 最佳实践 + +### 1. 长期运行的服务使用 -d + +```bash +# Web 服务器 +$ docker run -d -p 80:80 nginx + +# 数据库 +$ docker run -d -p 3306:3306 mysql:8 + +# 缓存服务 +$ docker run -d -p 6379:6379 redis +``` + +### 2. 调试时先用前台模式 + +当容器启动有问题时,**去掉 `-d` 参数**可以直接看到输出和错误: + +```bash +# 有问题的容器,先前台运行看看发生了什么 +$ docker run myimage:latest +``` + +### 3. 使用 --rm 自动清理 + +对于一次性任务,使用 `--rm` 参数让容器退出后自动删除: + +```bash +$ docker run --rm ubuntu:24.04 echo "Hello, World!" +Hello, World! +# 容器执行完后自动删除 +``` + +### 4. 配合日志查看 + +```bash +# 后台启动 +$ docker run -d --name myapp myimage:latest + +# 查看最近 100 行日志 +$ docker logs --tail 100 myapp + +# 实时跟踪日志 +$ docker logs -f myapp + +# 查看带时间戳的日志 +$ docker logs -t myapp +``` + +## 常见问题排查 + +### Q: 容器启动后立即退出 + +1. **查看退出状态码**: + ```bash + $ docker ps -a --filter "name=mycontainer" + # 查看 STATUS 列,如 "Exited (1)" 表示异常退出 + ``` + +2. **查看容器日志**: + ```bash + $ docker logs mycontainer + ``` + +3. **以交互模式调试**: + ```bash + $ docker run -it myimage:latest /bin/sh + # 进入容器手动执行命令,查找问题 + ``` + +### Q: 容器在后台运行但无法访问服务 + +1. **检查端口映射**: + ```bash + $ docker port mycontainer + ``` + +2. **检查容器内服务状态**: + ```bash + $ docker exec mycontainer ps aux + ``` + +### Q: 如何让已经在后台运行的容器回到前台? + +使用 `docker attach`: + +```bash +$ docker attach mycontainer +``` + +> **注意**:`attach` 会连接到容器的主进程。如果主进程不是交互式的,你可能只能看到输出。使用 `Ctrl+P` `Ctrl+Q` 可以安全退出而不停止容器。 + +## 延伸阅读 + +- [进入容器](attach_exec.md):如何进入正在运行的容器执行命令 +- [容器日志](../appendix/best_practices.md):生产环境的日志管理最佳实践 +- [HEALTHCHECK 健康检查](../image/dockerfile/healthcheck.md):自动检测容器内服务是否正常 +- [Docker Compose](../compose/README.md):管理多个后台容器的更好方式 diff --git a/container/rm.md b/container/rm.md index aa1958c..1c35197 100644 --- a/container/rm.md +++ b/container/rm.md @@ -1,18 +1,238 @@ # 删除容器 -可以使用 `docker container rm` 来删除一个处于终止状态的容器。例如 +## 基本用法 + +使用 `docker rm` 删除已停止的容器: ```bash -$ docker container rm trusting_newton -trusting_newton +$ docker rm 容器名或ID ``` -如果要删除一个运行中的容器,可以添加 `-f` 参数。Docker 会发送 `SIGKILL` 信号给容器。 +> 💡 `docker rm` 是 `docker container rm` 的简写,两者等效。 -# 清理所有处于终止状态的容器 +--- -用 `docker container ls -a` 命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。 +## 删除选项 + +| 选项 | 说明 | 示例 | +|------|------|------| +| 无参数 | 删除已停止的容器 | `docker rm mycontainer` | +| `-f` | 强制删除运行中的容器 | `docker rm -f mycontainer` | +| `-v` | 同时删除关联的匿名卷 | `docker rm -v mycontainer` | + +### 删除已停止的容器 ```bash +$ docker rm mycontainer +mycontainer +``` + +### 强制删除运行中的容器 + +```bash +# 不加 -f 会报错 +$ docker rm running_container +Error: cannot remove running container + +# 加 -f 强制删除 +$ docker rm -f running_container +running_container +``` + +> ⚠️ 强制删除会向容器发送 SIGKILL 信号,可能导致数据丢失。建议先 `docker stop` 优雅停止。 + +### 删除容器及其数据卷 + +```bash +# 删除容器时同时删除其匿名卷 +$ docker rm -v mycontainer +``` + +> 注意:只删除匿名卷,命名卷不会被删除。 + +--- + +## 批量删除 + +### 删除所有已停止的容器 + +```bash +# 方式一:使用 prune 命令(推荐) $ docker container prune + +WARNING! This will remove all stopped containers. +Are you sure you want to continue? [y/N] y +Deleted Containers: +abc123... +def456... +Total reclaimed space: 150MB + +# 方式二:不提示确认 +$ docker container prune -f ``` + +### 删除所有容器(包括运行中的) + +```bash +# 先停止所有容器,再删除 +$ docker stop $(docker ps -q) +$ docker rm $(docker ps -aq) + +# 或者直接强制删除 +$ docker rm -f $(docker ps -aq) +``` + +### 按条件删除 + +```bash +# 删除所有已退出的容器 +$ docker rm $(docker ps -aq -f status=exited) + +# 删除名称包含 "test" 的容器 +$ docker rm $(docker ps -aq -f name=test) + +# 删除 24 小时前创建的容器 +$ docker container prune --filter "until=24h" +``` + +--- + +## 常用过滤条件 + +`docker ps` 的过滤条件可以配合 `rm` 使用: + +| 过滤条件 | 说明 | 示例 | +|---------|------|------| +| `status=exited` | 已退出的容器 | `-f status=exited` | +| `status=created` | 已创建未启动 | `-f status=created` | +| `name=xxx` | 名称匹配 | `-f name=myapp` | +| `ancestor=xxx` | 基于某镜像创建 | `-f ancestor=nginx` | +| `before=xxx` | 在某容器之前创建 | `-f before=mycontainer` | +| `since=xxx` | 在某容器之后创建 | `-f since=mycontainer` | + +### 示例 + +```bash +# 删除所有基于 nginx 镜像的容器 +$ docker rm $(docker ps -aq -f ancestor=nginx) + +# 删除所有创建后未启动的容器 +$ docker rm $(docker ps -aq -f status=created) +``` + +--- + +## 容器与镜像的依赖关系 + +> 有容器依赖的镜像无法删除。 + +```bash +# 尝试删除有容器依赖的镜像 +$ docker image rm nginx +Error: image is being used by stopped container abc123 + +# 需要先删除依赖该镜像的容器 +$ docker rm abc123 +$ docker image rm nginx +``` + +--- + +## 清理策略建议 + +### 开发环境 + +```bash +# 定期清理已停止的容器 +$ docker container prune -f + +# 一键清理所有未使用资源 +$ docker system prune -f +``` + +### 生产环境 + +```bash +# 使用 --rm 参数运行临时容器 +$ docker run --rm ubuntu echo "Hello" +# 容器退出后自动删除 + +# 定期清理(设置保留时间) +$ docker container prune --filter "until=168h" # 保留 7 天内的 +``` + +### 完整清理脚本 + +```bash +#!/bin/bash +# cleanup.sh - Docker 资源清理脚本 + +echo "清理已停止的容器..." +docker container prune -f + +echo "清理未使用的镜像..." +docker image prune -f + +echo "清理未使用的数据卷..." +docker volume prune -f + +echo "清理未使用的网络..." +docker network prune -f + +echo "清理完成!" +docker system df +``` + +--- + +## 常见问题 + +### Q: 容器无法删除 + +```bash +Error: container is running +``` + +解决:先停止容器,或使用 `-f` 强制删除 + +```bash +$ docker stop mycontainer +$ docker rm mycontainer +# 或 +$ docker rm -f mycontainer +``` + +### Q: 删除后磁盘空间没释放 + +可能原因: +1. 容器的数据卷未删除(使用 `-v` 参数) +2. 镜像未删除 +3. 构建缓存未清理 + +解决: + +```bash +# 查看空间占用 +$ docker system df + +# 完整清理 +$ docker system prune -a --volumes +``` + +--- + +## 本章小结 + +| 操作 | 命令 | +|------|------| +| 删除已停止容器 | `docker rm 容器名` | +| 强制删除运行中容器 | `docker rm -f 容器名` | +| 删除容器及匿名卷 | `docker rm -v 容器名` | +| 清理所有已停止容器 | `docker container prune` | +| 删除所有容器 | `docker rm -f $(docker ps -aq)` | + +## 延伸阅读 + +- [终止容器](stop.md):优雅停止容器 +- [删除镜像](../image/rm.md):清理镜像 +- [数据卷](../data_management/volume.md):数据卷管理 diff --git a/container/run.md b/container/run.md index 8585ec1..bed0a0f 100644 --- a/container/run.md +++ b/container/run.md @@ -1,55 +1,166 @@ # 启动容器 -启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(`exited`)的容器重新启动。 +## 启动方式概述 -因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。 +启动容器有两种方式: +- **新建并启动**:基于镜像创建新容器 +- **重新启动**:将已终止的容器重新运行 + +由于 Docker 容器非常轻量,实际使用中常常是随时删除和新建容器,而不是反复重启同一个容器。 ## 新建并启动 -所需要的命令主要为 `docker run`。 +### 基本语法 -例如,下面的命令输出一个 “Hello World”,之后终止容器。 +```bash +docker run [选项] 镜像 [命令] [参数...] +``` + +### 最简单的例子 + +输出 "Hello World" 后容器自动终止: ```bash $ docker run ubuntu:24.04 /bin/echo 'Hello world' Hello world ``` -这跟在本地直接执行 `/bin/echo 'hello world'` 几乎感觉不出任何区别。 +这与直接执行 `/bin/echo 'Hello world'` 几乎没有区别,但实际上已经启动了一个完整的 Ubuntu 容器来执行这条命令。 -下面的命令则启动一个 bash 终端,允许用户进行交互。 +### 交互式容器 + +启动一个可以交互的 bash 终端: ```bash -$ docker run -t -i ubuntu:24.04 /bin/bash +$ docker run -it ubuntu:24.04 /bin/bash root@af8bae53bdd3:/# ``` -其中,`-t` 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, `-i` 则让容器的标准输入保持打开。 +**参数说明**: -在交互模式下,用户可以通过所创建的终端来输入命令,例如 +| 参数 | 作用 | +|------|------| +| `-i` | 保持标准输入(stdin)打开,允许输入 | +| `-t` | 分配伪终端(pseudo-TTY),提供终端界面 | +| `-it` | 两者组合使用,获得交互式终端 | + +在交互模式下可以执行命令: ```bash root@af8bae53bdd3:/# pwd / root@af8bae53bdd3:/# ls bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var +root@af8bae53bdd3:/# exit # 退出容器 ``` -当利用 `docker run` 来创建容器时,Docker 在后台运行的标准操作包括: +## docker run 的完整流程 -* 检查本地是否存在指定的镜像,不存在就从 [registry](../repository/README.md) 下载 -* 利用镜像创建并启动一个容器 -* 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层 -* 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去 -* 从地址池配置一个 ip 地址给容器 -* 执行用户指定的应用程序 -* 执行完毕后容器被终止 +执行 `docker run` 时,Docker 在后台完成以下操作: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ docker run ubuntu:24.04 /bin/echo "Hello" │ +└───────────────────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 1. 检查本地是否有 ubuntu:24.04 镜像 │ +│ ├── 有 → 使用本地镜像 │ +│ └── 无 → 从 Registry 下载 │ +├─────────────────────────────────────────────────────────────────────┤ +│ 2. 创建容器 │ +│ • 基于镜像的只读层 │ +│ • 添加一层可读写层(容器存储层) │ +├─────────────────────────────────────────────────────────────────────┤ +│ 3. 配置网络 │ +│ • 创建虚拟网卡 │ +│ • 分配 IP 地址 │ +│ • 连接到 Docker 网桥 │ +├─────────────────────────────────────────────────────────────────────┤ +│ 4. 启动容器,执行指定命令 │ +├─────────────────────────────────────────────────────────────────────┤ +│ 5. 命令执行完毕,容器停止 │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## 常用启动选项 + +### 基础选项 + +| 选项 | 说明 | 示例 | +|------|------|------| +| `-d` | 后台运行(detach) | `docker run -d nginx` | +| `-it` | 交互式终端 | `docker run -it ubuntu bash` | +| `--name` | 指定容器名称 | `docker run --name myapp nginx` | +| `--rm` | 退出后自动删除容器 | `docker run --rm ubuntu echo hi` | + +### 端口映射 + +```bash +# 将容器的 80 端口映射到宿主机的 8080 端口 +$ docker run -d -p 8080:80 nginx + +# 随机映射端口 +$ docker run -d -P nginx + +# 只绑定到 localhost +$ docker run -d -p 127.0.0.1:8080:80 nginx +``` + +### 数据卷挂载 + +```bash +# 挂载命名卷 +$ docker run -v mydata:/data nginx + +# 挂载宿主机目录 +$ docker run -v /host/path:/container/path nginx + +# 只读挂载 +$ docker run -v /host/path:/container/path:ro nginx +``` + +### 环境变量 + +```bash +# 设置单个环境变量 +$ docker run -e MYSQL_ROOT_PASSWORD=secret mysql + +# 从文件加载环境变量 +$ docker run --env-file .env myapp +``` + +### 资源限制 + +```bash +# 限制内存 +$ docker run -m 512m nginx + +# 限制 CPU +$ docker run --cpus=1.5 nginx +``` ## 启动已终止容器 -可以利用 `docker container start` 命令,直接将一个已经终止(`exited`)的容器启动运行。 +使用 `docker start` 重新启动已停止的容器: -容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 `ps` 或 `top` 来查看进程信息。 +```bash +# 查看所有容器(包括已停止的) +$ docker ps -a +CONTAINER ID IMAGE STATUS NAMES +af8bae53bdd3 ubuntu Exited (0) 2 minutes ago myubuntu + +# 重新启动 +$ docker start myubuntu + +# 启动并附加终端 +$ docker start -ai myubuntu +``` + +## 容器内进程的特点 + +容器内只运行指定的应用程序及其必需资源: ```bash root@ba267838cc1b:/# ps @@ -58,4 +169,61 @@ root@ba267838cc1b:/# ps 11 ? 00:00:00 ps ``` -可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。 +可见容器中仅运行了 `bash` 进程。这种特点使得 Docker 对资源的利用率极高。 + +> 💡 笔者提示:容器内的 PID 1 进程很重要——它是容器的主进程,该进程退出则容器停止。详见[后台运行](daemon.md)章节。 + +## 常见问题 + +### Q: 容器启动后立即退出 + +**原因**:主进程执行完毕或无法保持运行 + +```bash +# 这个容器会立即退出(echo 执行完就结束了) +$ docker run ubuntu echo "hello" + +# 解决:使用能持续运行的命令 +$ docker run -d nginx # nginx 是持续运行的服务 +``` + +详细解释见[后台运行](daemon.md)。 + +### Q: 无法连接容器内的服务 + +**原因**:未正确映射端口 + +```bash +# 错误:没有 -p 参数,外部无法访问 +$ docker run -d nginx + +# 正确:映射端口 +$ docker run -d -p 80:80 nginx +``` + +### Q: 容器内修改的文件丢失 + +**原因**:未使用数据卷,数据保存在容器存储层 + +```bash +# 使用数据卷持久化 +$ docker run -v mydata:/app/data myapp +``` + +详见[数据管理](../data_management/README.md)。 + +## 本章小结 + +| 操作 | 命令 | 说明 | +|------|------|------| +| 新建并运行 | `docker run` | 最常用的启动方式 | +| 交互式启动 | `docker run -it` | 用于调试或临时操作 | +| 后台运行 | `docker run -d` | 用于服务类应用 | +| 启动已停止的容器 | `docker start` | 重用已有容器 | + +## 延伸阅读 + +- [后台运行](daemon.md):理解 `-d` 参数和容器生命周期 +- [进入容器](attach_exec.md):操作运行中的容器 +- [网络配置](../network/README.md):理解端口映射的原理 +- [数据管理](../data_management/README.md):数据持久化方案 diff --git a/container/stop.md b/container/stop.md index 6c1eccb..a21344c 100644 --- a/container/stop.md +++ b/container/stop.md @@ -1,19 +1,256 @@ # 终止容器 -可以使用 `docker container stop` 来终止一个运行中的容器。 +## 终止方式概述 -此外,当 Docker 容器中指定的应用终结时,容器也自动终止。 +终止容器有三种方式: -例如对于上一章节中只启动了一个终端的容器,用户通过 `exit` 命令或 `Ctrl+d` 来退出终端时,所创建的容器立刻终止。 +| 方式 | 命令 | 说明 | +|------|------|------| +| **优雅停止** | `docker stop` | 先发 SIGTERM,超时后发 SIGKILL | +| **强制停止** | `docker kill` | 直接发 SIGKILL | +| **自动终止** | - | 容器主进程退出时自动停止 | -终止状态的容器可以用 `docker container ls -a` 命令看到。例如 +--- + +## docker stop(推荐) + +### 基本用法 ```bash -$ docker container ls -a -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -ba267838cc1b ubuntu:24.04 "/bin/bash" 30 minutes ago Exited (0) About a minute ago trusting_newton +$ docker stop 容器名或ID ``` -处于终止状态的容器,可以通过 `docker container start` 命令来重新启动。 +### 工作原理 -此外,`docker container restart` 命令会将一个运行态的容器终止,然后再重新启动它。 +``` +docker stop mycontainer + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 1. 发送 SIGTERM 信号给容器主进程(PID 1) │ +│ ↓ │ +│ 2. 等待容器优雅退出(默认 10 秒) │ +│ ↓ │ +│ 3. 如果超时仍未退出,发送 SIGKILL 强制终止 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 自定义超时时间 + +```bash +# 等待 30 秒后强制终止 +$ docker stop -t 30 mycontainer + +# 立即发送 SIGKILL(相当于 docker kill) +$ docker stop -t 0 mycontainer +``` + +### 停止多个容器 + +```bash +# 停止多个指定容器 +$ docker stop container1 container2 container3 + +# 停止所有运行中的容器 +$ docker stop $(docker ps -q) +``` + +--- + +## docker kill + +### 基本用法 + +```bash +$ docker kill 容器名或ID +``` + +### 与 stop 的区别 + +| 命令 | 信号 | 使用场景 | +|------|------|---------| +| `docker stop` | SIGTERM → SIGKILL | 正常停止,让应用优雅退出 | +| `docker kill` | SIGKILL | 应用无响应,强制终止 | + +### 发送自定义信号 + +```bash +# 发送 SIGHUP(让进程重新加载配置) +$ docker kill -s HUP mycontainer + +# 发送 SIGTERM +$ docker kill -s TERM mycontainer +``` + +--- + +## 容器自动终止 + +容器的生命周期与主进程绑定。主进程退出时,容器自动停止: + +```bash +# 主进程是交互式 bash +$ docker run -it ubuntu bash +root@abc123:/# exit # 退出 bash → 容器停止 + +# 主进程执行完毕 +$ docker run ubuntu echo "Hello" # echo 执行完 → 容器停止 +``` + +--- + +## 查看已停止的容器 + +```bash +$ docker ps -a +CONTAINER ID IMAGE COMMAND STATUS NAMES +ba267838cc1b ubuntu "/bin/bash" Exited (0) 2 minutes ago myubuntu +c5d3a5e8f7b2 nginx "nginx" Up 5 minutes mynginx +``` + +**STATUS 字段说明**: + +| 状态 | 说明 | +|------|------| +| `Up X minutes` | 运行中 | +| `Exited (0)` | 正常退出(退出码 0) | +| `Exited (1)` | 异常退出(非零退出码) | +| `Exited (137)` | 被 SIGKILL 终止(128 + 9) | +| `Exited (143)` | 被 SIGTERM 终止(128 + 15) | + +--- + +## 重新启动容器 + +### 启动已停止的容器 + +```bash +$ docker start 容器名或ID + +# 启动并附加终端 +$ docker start -ai 容器名 +``` + +### 重启运行中的容器 + +```bash +# 先停止再启动 +$ docker restart 容器名 + +# 自定义停止超时 +$ docker restart -t 30 容器名 +``` + +--- + +## 生命周期状态图 + +``` + docker create + │ + ▼ + ┌──────────┐ + ┌────────│ Created │────────┐ + │ └──────────┘ │ + │ │ │ + │ │ docker start│ + │ ▼ │ + │ ┌──────────┐ │ + │ ┌────│ Running │────┐ │ + │ │ └──────────┘ │ │ + │ │ │ │ │ + │ │ docker │ docker │ │ + │ │ pause │ stop │ │ + │ ▼ │ │ │ + │ ┌──────┐ │ │ │ + │ │Paused│ │ │ │ + │ └──────┘ │ │ │ + │ │ │ │ │ + │ │ docker │ │ │ + │ │ unpause │ │ │ + │ ▼ ▼ │ │ + │ └──────►┌──────────┐◄┘ │ + │ │ Stopped │ │ + │ └──────────┘ │ + │ │ │ + │ │ docker rm │ + │ ▼ │ + │ ┌──────────┐ │ + └──────────►│ Deleted │◄────┘ + └──────────┘ +``` + +--- + +## 批量操作 + +### 停止所有容器 + +```bash +$ docker stop $(docker ps -q) +``` + +### 删除所有已停止的容器 + +```bash +$ docker container prune +``` + +### 停止并删除所有容器 + +```bash +$ docker stop $(docker ps -q) && docker container prune -f +``` + +--- + +## 常见问题 + +### Q: 容器停止很慢 + +原因:应用没有正确处理 SIGTERM 信号,需要等待超时后强制终止。 + +解决方案: +1. 在应用中正确处理 SIGTERM +2. 使用 `docker stop -t 0` 立即终止 +3. 检查 Dockerfile 中的 `STOPSIGNAL` 配置 + +### Q: 如何让容器优雅退出 + +确保容器主进程正确处理信号: + +```dockerfile +# Dockerfile 示例 +FROM node:18 +... +# 使用 exec 形式确保信号能传递给 node 进程 +CMD ["node", "server.js"] +``` + +### Q: 容器无法停止 + +```bash +# 强制终止 +$ docker kill 容器名 + +# 如果仍无法停止,检查系统资源 +$ docker inspect 容器名 +``` + +--- + +## 本章小结 + +| 操作 | 命令 | 说明 | +|------|------|------| +| 优雅停止 | `docker stop` | 先 SIGTERM,超时后 SIGKILL | +| 强制停止 | `docker kill` | 直接 SIGKILL | +| 重新启动 | `docker start` | 启动已停止的容器 | +| 重启 | `docker restart` | 停止后立即启动 | +| 停止全部 | `docker stop $(docker ps -q)` | 停止所有运行中容器 | + +## 延伸阅读 + +- [启动容器](run.md):容器启动详解 +- [删除容器](rm.md):清理容器 +- [容器日志](logs.md):排查停止原因 diff --git a/data_management/bind-mounts.md b/data_management/bind-mounts.md index 7fb4d09..4011d92 100644 --- a/data_management/bind-mounts.md +++ b/data_management/bind-mounts.md @@ -1,73 +1,284 @@ -# 挂载主机目录 +# 挂载主机目录(Bind Mounts) -## 挂载一个主机目录作为数据卷 +## 什么是 Bind Mount -使用 `--mount` 标记可以指定挂载一个本地主机的目录到容器中去。 +Bind Mount(绑定挂载)将**宿主机的目录或文件**直接挂载到容器中。容器可以读写宿主机的文件系统。 -```bash -$ docker run -d -P \ - --name web \ - # -v /src/webapp:/usr/share/nginx/html \ - --mount type=bind,source=/src/webapp,target=/usr/share/nginx/html \ - nginx:alpine +``` +宿主机 容器 +┌─────────────────────┐ ┌─────────────────────┐ +│ /home/user/code/ │ │ │ +│ ├── index.html │◄───────►│ /usr/share/nginx/ │ +│ ├── style.css │ Bind │ html/ │ +│ └── app.js │ Mount │ (同一份文件) │ +└─────────────────────┘ └─────────────────────┘ ``` -上面的命令加载主机的 `/src/webapp` 目录到容器的 `/usr/share/nginx/html`目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,以前使用 `-v` 参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹,现在使用 `--mount` 参数时如果本地目录不存在,Docker 会报错。 +--- -Docker 挂载主机目录的默认权限是 `读写`,用户也可以通过增加 `readonly` 指定为 `只读`。 +## Bind Mount vs Volume -```bash -$ docker run -d -P \ - --name web \ - # -v /src/webapp:/usr/share/nginx/html:ro \ - --mount type=bind,source=/src/webapp,target=/usr/share/nginx/html,readonly \ - nginx:alpine +| 特性 | Bind Mount | Volume | +|------|------------|--------| +| **数据位置** | 宿主机任意路径 | Docker 管理的目录 | +| **路径指定** | 必须是绝对路径 | 卷名 | +| **可移植性** | 依赖宿主机路径 | 更好(Docker 管理) | +| **性能** | 依赖宿主机文件系统 | 优化的存储驱动 | +| **适用场景** | 开发环境、配置文件 | 生产数据持久化 | +| **备份** | 直接访问文件 | 需要通过 Docker | + +### 选择建议 + +``` +需求 推荐方案 +───────────────────────────────────────── +开发时同步代码 → Bind Mount +持久化数据库数据 → Volume +共享配置文件 → Bind Mount +容器间共享数据 → Volume +备份方便 → Bind Mount(直接访问) +生产环境 → Volume ``` -加了 `readonly` 之后,就挂载为 `只读` 了。如果你在容器内 `/usr/share/nginx/html` 目录新建文件,会显示如下错误 +--- + +## 基本语法 + +### 使用 --mount(推荐) ```bash -/usr/share/nginx/html # touch new.txt -touch: new.txt: Read-only file system +$ docker run -d \ + --mount type=bind,source=/宿主机路径,target=/容器路径 \ + nginx ``` -## 查看数据卷的具体信息 - -在主机里使用以下命令可以查看 `web` 容器的信息 +### 使用 -v(简写) ```bash -$ docker inspect web +$ docker run -d \ + -v /宿主机路径:/容器路径 \ + nginx ``` -`挂载主机目录` 的配置信息在 "Mounts" Key 下面 +### 两种语法对比 + +| 特性 | --mount | -v | +|------|---------|-----| +| 语法 | 键值对,更清晰 | 冒号分隔,更简洁 | +| 路径不存在时 | 报错 | 自动创建目录 | +| 推荐程度 | ✅ 推荐 | 常用 | + +--- + +## 使用场景 + +### 场景一:开发环境代码同步 + +```bash +# 将本地代码目录挂载到容器 +$ docker run -d \ + -p 8080:80 \ + --mount type=bind,source=$(pwd)/src,target=/usr/share/nginx/html \ + nginx + +# 修改本地文件,容器内立即生效(热更新) +$ echo "Hello" > src/index.html +# 浏览器刷新即可看到变化 +``` + +### 场景二:配置文件挂载 + +```bash +# 挂载自定义 nginx 配置 +$ docker run -d \ + --mount type=bind,source=/path/to/nginx.conf,target=/etc/nginx/nginx.conf,readonly \ + nginx +``` + +### 场景三:日志收集 + +```bash +# 将容器日志输出到宿主机目录 +$ docker run -d \ + --mount type=bind,source=/var/log/myapp,target=/app/logs \ + myapp +``` + +### 场景四:共享 SSH 密钥 + +```bash +# 挂载 SSH 密钥(只读) +$ docker run --rm -it \ + --mount type=bind,source=$HOME/.ssh,target=/root/.ssh,readonly \ + alpine ssh user@remote +``` + +--- + +## 只读挂载 + +防止容器修改宿主机文件: + +```bash +# --mount 语法 +$ docker run -d \ + --mount type=bind,source=/config,target=/app/config,readonly \ + myapp + +# -v 语法 +$ docker run -d \ + -v /config:/app/config:ro \ + myapp +``` + +容器内尝试写入会报错: + +```bash +$ touch /app/config/new.txt +touch: /app/config/new.txt: Read-only file system +``` + +--- + +## 挂载单个文件 + +```bash +# 挂载 bash 历史记录 +$ docker run --rm -it \ + --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \ + ubuntu bash + +# 挂载自定义配置文件 +$ docker run -d \ + --mount type=bind,source=/path/to/my.cnf,target=/etc/mysql/my.cnf \ + mysql +``` + +> ⚠️ **注意**:挂载单个文件时,如果宿主机上的文件被编辑器替换(而非原地修改),容器内仍是旧文件的 inode。建议重启容器或挂载目录。 + +--- + +## 查看挂载信息 + +```bash +$ docker inspect mycontainer --format '{{json .Mounts}}' | jq +``` + +输出: ```json -"Mounts": [ - { - "Type": "bind", - "Source": "/src/webapp", - "Destination": "/usr/share/nginx/html", - "Mode": "", - "RW": true, - "Propagation": "rprivate" - } -], +[ + { + "Type": "bind", + "Source": "/home/user/code", + "Destination": "/app", + "Mode": "", + "RW": true, + "Propagation": "rprivate" + } +] ``` -## 挂载一个本地主机文件作为数据卷 +| 字段 | 说明 | +|------|------| +| `Type` | 挂载类型(bind) | +| `Source` | 宿主机路径 | +| `Destination` | 容器内路径 | +| `RW` | 是否可读写 | +| `Propagation` | 挂载传播模式 | -`--mount` 标记也可以从主机挂载单个文件到容器中 +--- + +## 常见问题 + +### Q: 路径不存在报错 ```bash -$ docker run --rm -it \ - # -v $HOME/.bash_history:/root/.bash_history \ - --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \ - ubuntu:24.04 \ - bash - -root@2affd44b4667:/# history -1 ls -2 diskutil list +$ docker run --mount type=bind,source=/not/exist,target=/app nginx +docker: Error response from daemon: invalid mount config for type "bind": +bind source path does not exist: /not/exist ``` -这样就可以记录在容器输入过的命令了。 +**解决**:确保源路径存在,或改用 `-v`(会自动创建) + +### Q: 权限问题 + +容器内用户可能无权访问挂载的文件: + +```bash +# 方法1:确保宿主机文件权限允许容器用户访问 +$ chmod -R 755 /path/to/data + +# 方法2:以 root 运行容器 +$ docker run -u root ... + +# 方法3:使用相同的 UID +$ docker run -u $(id -u):$(id -g) ... +``` + +### Q: macOS/Windows 性能问题 + +在 Docker Desktop 上,Bind Mount 性能较差(需要跨文件系统同步): + +```bash +# 使用 :cached 或 :delegated 提高性能(macOS) +$ docker run -v /host/path:/container/path:cached myapp +``` + +| 选项 | 说明 | +|------|------| +| `:cached` | 宿主机权威,容器读取可能延迟 | +| `:delegated` | 容器权威,宿主机读取可能延迟 | +| `:consistent` | 默认,完全一致(最慢) | + +--- + +## 最佳实践 + +### 1. 开发环境使用 Bind Mount + +```bash +# 代码热更新 +$ docker run -v $(pwd):/app -p 3000:3000 node npm run dev +``` + +### 2. 生产环境使用 Volume + +```bash +# 数据持久化 +$ docker run -v mysql_data:/var/lib/mysql mysql +``` + +### 3. 配置文件使用只读挂载 + +```bash +$ docker run -v /config/nginx.conf:/etc/nginx/nginx.conf:ro nginx +``` + +### 4. 注意路径安全 + +```bash +# ❌ 危险:挂载根目录或敏感目录 +$ docker run -v /:/host ... + +# ✅ 只挂载必要的目录 +$ docker run -v /app/data:/data ... +``` + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 将宿主机目录挂载到容器 | +| **语法** | `-v /宿主机:/容器` 或 `--mount type=bind,...` | +| **只读** | 添加 `readonly` 或 `:ro` | +| **适用场景** | 开发环境、配置文件、日志 | +| **vs Volume** | Bind 更灵活,Volume 更适合生产 | + +## 延伸阅读 + +- [数据卷](volume.md):Docker 管理的持久化存储 +- [tmpfs 挂载](tmpfs.md):内存临时存储 +- [Compose 数据管理](../compose/compose_file.md):Compose 中的挂载配置 diff --git a/data_management/volume.md b/data_management/volume.md index a80cca4..60de80a 100644 --- a/data_management/volume.md +++ b/data_management/volume.md @@ -1,38 +1,83 @@ # 数据卷 -`数据卷` 是一个可供一个或多个容器使用的特殊目录,它绕过 UnionFS,可以提供很多有用的特性: +## 为什么需要数据卷 -* `数据卷` 可以在容器之间共享和重用 +容器的存储层有一个关键问题:**容器删除后,数据就没了**。 -* 对 `数据卷` 的修改会立马生效 +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 容器存储层问题 │ +│ │ +│ 容器运行 ─────► 写入数据 ─────► 容器删除 ─────► 数据丢失! │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` -* 对 `数据卷` 的更新,不会影响镜像 +数据卷(Volume)解决了这个问题,它的生命周期独立于容器。 -* `数据卷` 默认会一直存在,即使容器被删除 +--- ->注意:`数据卷` 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会复制到数据卷中(仅数据卷为空时会复制)。 +## 数据卷的特性 -## 创建一个数据卷 +| 特性 | 说明 | +|------|------| +| **持久化** | 容器删除后数据仍然保留 | +| **共享** | 多个容器可以挂载同一个数据卷 | +| **即时生效** | 对数据卷的修改立即可见 | +| **不影响镜像** | 数据卷中的数据不会打包进镜像 | +| **性能更好** | 绕过 UnionFS,直接读写 | + +--- + +## 数据卷 vs 容器存储层 + +``` +容器存储层(不推荐存储重要数据): +┌─────────────────────────────────────────┐ +│ 容器存储层(可读写) │ +├─────────────────────────────────────────┤ +│ 镜像层(只读) │ +└─────────────────────────────────────────┘ + 生命周期 = 容器生命周期 + 容器删除 → 数据丢失 + +数据卷(推荐): +┌─────────────────────────────────────────┐ +│ 容器 │ +│ ┌─────────────────────────────────┐ │ +│ │ /app/data ──────────────────│────┼──► 数据卷 my-data +│ └─────────────────────────────────┘ │ (独立于容器) +└─────────────────────────────────────────┘ + 容器删除 → 数据卷保留 +``` + +--- + +## 数据卷基本操作 + +### 创建数据卷 ```bash $ docker volume create my-vol ``` -查看所有的 `数据卷` +### 列出所有数据卷 ```bash $ docker volume ls - -DRIVER VOLUME NAME -local my-vol +DRIVER VOLUME NAME +local my-vol +local postgres_data +local redis_data ``` -在主机里使用以下命令可以查看指定 `数据卷` 的信息 +### 查看数据卷详情 ```bash $ docker volume inspect my-vol [ { + "CreatedAt": "2024-01-15T10:00:00Z", "Driver": "local", "Labels": {}, "Mountpoint": "/var/lib/docker/volumes/my-vol/_data", @@ -43,55 +88,267 @@ $ docker volume inspect my-vol ] ``` -## 启动一个挂载数据卷的容器 +**关键字段**: +- `Mountpoint`:数据卷在宿主机上的实际存储位置 +- `Driver`:存储驱动(默认 local,也可以用第三方驱动) -在用 `docker run` 命令的时候,使用 `--mount` 标记来将 `数据卷` 挂载到容器里。在一次 `docker run` 中可以挂载多个 `数据卷`。 +--- -下面创建一个名为 `web` 的容器,并加载一个 `数据卷` 到容器的 `/usr/share/nginx/html` 目录。 +## 挂载数据卷 + +### 方式一:--mount(推荐) ```bash -$ docker run -d -P \ +$ docker run -d \ --name web \ - # -v my-vol:/usr/share/nginx/html \ --mount source=my-vol,target=/usr/share/nginx/html \ - nginx:alpine + nginx ``` -## 查看数据卷的具体信息 +**参数说明**: -在主机里使用以下命令可以查看 `web` 容器的信息 +| 参数 | 说明 | +|------|------| +| `source` | 数据卷名称(不存在会自动创建) | +| `target` | 容器内挂载路径 | +| `readonly` | 可选,只读挂载 | + +### 方式二:-v(简写) ```bash -$ docker inspect web +$ docker run -d \ + --name web \ + -v my-vol:/usr/share/nginx/html \ + nginx ``` -`数据卷` 信息在 "Mounts" Key 下面 +**格式**:`-v 数据卷名:容器路径[:选项]` -```json -"Mounts": [ - { - "Type": "volume", - "Name": "my-vol", - "Source": "/var/lib/docker/volumes/my-vol/_data", - "Destination": "/usr/share/nginx/html", - "Driver": "local", - "Mode": "", - "RW": true, - "Propagation": "" - } -], -``` +### 两种方式对比 -## 删除数据卷 +| 特性 | --mount | -v | +|------|---------|-----| +| 语法 | 键值对,更清晰 | 冒号分隔,更简洁 | +| 自动创建卷 | source 不存在会报错 | 自动创建 | +| 推荐程度 | ✅ 推荐(更明确) | 常用(更简洁) | + +### 只读挂载 ```bash +# --mount 方式 +$ docker run -d \ + --mount source=my-vol,target=/data,readonly \ + nginx + +# -v 方式 +$ docker run -d \ + -v my-vol:/data:ro \ + nginx +``` + +--- + +## 使用场景示例 + +### 场景一:数据库持久化 + +```bash +# 创建数据卷 +$ docker volume create postgres_data + +# 启动 PostgreSQL,数据存储在数据卷中 +$ docker run -d \ + --name postgres \ + -e POSTGRES_PASSWORD=secret \ + -v postgres_data:/var/lib/postgresql/data \ + postgres:16 + +# 即使删除容器,数据仍然保留 +$ docker rm -f postgres + +# 重新启动,数据还在 +$ docker run -d \ + --name postgres \ + -e POSTGRES_PASSWORD=secret \ + -v postgres_data:/var/lib/postgresql/data \ + postgres:16 +``` + +### 场景二:多容器共享数据 + +```bash +# 创建共享数据卷 +$ docker volume create shared-data + +# 容器 A 写入数据 +$ docker run -d --name writer \ + -v shared-data:/data \ + alpine sh -c "while true; do date >> /data/log.txt; sleep 5; done" + +# 容器 B 读取数据 +$ docker run --rm \ + -v shared-data:/data \ + alpine cat /data/log.txt +``` + +### 场景三:配置文件持久化 + +```bash +# 将 nginx 配置存储在数据卷中 +$ docker run -d \ + -v nginx-config:/etc/nginx/conf.d \ + -v nginx-logs:/var/log/nginx \ + -p 80:80 \ + nginx +``` + +--- + +## 数据卷管理 + +### 删除数据卷 + +```bash +# 删除指定数据卷 $ docker volume rm my-vol + +# 删除容器时同时删除数据卷 +$ docker rm -v container_name ``` -`数据卷` 是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 `数据卷`,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 `数据卷`。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 `docker rm -v` 这个命令。 - -无主的数据卷可能会占据很多空间,要清理请使用以下命令 +### 清理未使用的数据卷 ```bash +# 查看未被任何容器使用的数据卷 +$ docker volume ls -f dangling=true + +# 删除所有未使用的数据卷 $ docker volume prune + +# 强制删除(不提示确认) +$ docker volume prune -f ``` + +> ⚠️ **注意**:数据卷不会自动垃圾回收。长期运行的系统应定期清理无用数据卷。 + +--- + +## 数据卷备份与恢复 + +### 备份数据卷 + +```bash +# 使用临时容器挂载数据卷,打包备份 +$ docker run --rm \ + -v my-vol:/source:ro \ + -v $(pwd):/backup \ + alpine tar czf /backup/my-vol-backup.tar.gz -C /source . +``` + +**原理**: +1. 创建临时容器 +2. 挂载要备份的数据卷到 `/source` +3. 挂载当前目录到 `/backup` +4. 使用 tar 打包 + +### 恢复数据卷 + +```bash +# 创建新数据卷 +$ docker volume create my-vol-restored + +# 解压备份到新数据卷 +$ docker run --rm \ + -v my-vol-restored:/target \ + -v $(pwd):/backup:ro \ + alpine tar xzf /backup/my-vol-backup.tar.gz -C /target +``` + +### 备份脚本示例 + +```bash +#!/bin/bash +# backup-volume.sh + +VOLUME_NAME=$1 +BACKUP_DIR=${2:-/backups} +TIMESTAMP=$(date +%Y%m%d_%H%M%S) + +docker run --rm \ + -v ${VOLUME_NAME}:/source:ro \ + -v ${BACKUP_DIR}:/backup \ + alpine tar czf /backup/${VOLUME_NAME}_${TIMESTAMP}.tar.gz -C /source . + +echo "Backed up ${VOLUME_NAME} to ${BACKUP_DIR}/${VOLUME_NAME}_${TIMESTAMP}.tar.gz" +``` + +--- + +## 数据卷 vs 绑定挂载 + +Docker 有两种主要的数据持久化方式: + +| 特性 | 数据卷 (Volume) | 绑定挂载 (Bind Mount) | +|------|----------------|---------------------| +| **管理方式** | Docker 管理 | 用户管理 | +| **存储位置** | `/var/lib/docker/volumes/` | 任意宿主机路径 | +| **可移植性** | 更好 | 依赖宿主机路径 | +| **适用场景** | 生产数据持久化 | 开发时同步代码 | +| **备份** | 需要工具 | 直接访问文件 | + +```bash +# 数据卷 +$ docker run -v mydata:/app/data nginx + +# 绑定挂载 +$ docker run -v /host/path:/app/data nginx +``` + +详见 [绑定挂载](bind-mounts.md) 章节。 + +--- + +## 常见问题 + +### Q: 如何知道容器使用了哪些数据卷? + +```bash +$ docker inspect container_name --format '{{json .Mounts}}' | jq +``` + +### Q: 数据卷的数据在哪里? + +```bash +# 查看数据卷详情 +$ docker volume inspect my-vol + +# Mountpoint 字段显示实际路径 +"Mountpoint": "/var/lib/docker/volumes/my-vol/_data" +``` + +> ⚠️ **注意**:不建议直接修改 Mountpoint 中的文件,应通过容器操作。 + +### Q: 如何在不同机器间迁移数据卷? + +1. 在源机器备份:`docker run --rm -v mydata:/data -v $(pwd):/backup alpine tar czf /backup/data.tar.gz -C /data .` +2. 传输 tar.gz 文件 +3. 在目标机器恢复 + +--- + +## 本章小结 + +| 操作 | 命令 | +|------|------| +| 创建数据卷 | `docker volume create name` | +| 列出数据卷 | `docker volume ls` | +| 查看详情 | `docker volume inspect name` | +| 删除数据卷 | `docker volume rm name` | +| 清理未用 | `docker volume prune` | +| 挂载数据卷 | `-v name:/path` 或 `--mount source=name,target=/path` | + +## 延伸阅读 + +- [绑定挂载](bind-mounts.md):挂载宿主机目录 +- [tmpfs 挂载](tmpfs.md):内存中的临时存储 +- [存储驱动](../underly/ufs.md):Docker 存储的底层原理 diff --git a/image/dockerfile/add.md b/image/dockerfile/add.md index 4970b10..633d3a7 100644 --- a/image/dockerfile/add.md +++ b/image/dockerfile/add.md @@ -1,32 +1,221 @@ # ADD 更高级的复制文件 -`ADD` 指令和 `COPY` 的格式和性质基本一致。但是在 `COPY` 基础上增加了一些功能。 +## 基本语法 -比如 `<源路径>` 可以是一个 `URL`,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 `<目标路径>` 去。下载后的文件权限自动设置为 `600`,如果这并不是想要的权限,那么还需要增加额外的一层 `RUN` 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 `RUN` 指令进行解压缩。所以不如直接使用 `RUN` 指令,然后使用 `wget` 或者 `curl` 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。 +```docker +ADD [选项] <源路径>... <目标路径> +ADD [选项] ["<源路径>", ... "<目标路径>"] +``` -如果 `<源路径>` 为一个 `tar` 压缩文件的话,压缩格式为 `gzip`, `bzip2` 以及 `xz` 的情况下,`ADD` 指令将会自动解压缩这个压缩文件到 `<目标路径>` 去。 +`ADD` 在 `COPY` 基础上增加了两个功能: +1. 自动解压 tar 压缩包 +2. 支持从 URL 下载文件(不推荐) -在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 `ubuntu` 中: +--- + +## ADD vs COPY + +| 特性 | COPY | ADD | +|------|------|-----| +| 复制本地文件 | ✅ | ✅ | +| 自动解压 tar | ❌ | ✅ | +| 支持 URL | ❌ | ✅(不推荐) | +| 行为可预测性 | ✅ 高 | ⚠️ 低 | +| 推荐程度 | ✅ **优先使用** | 仅解压场景 | + +> 笔者建议:除非需要自动解压 tar 文件,否则始终使用 COPY。明确的行为比隐式的魔法更好。 + +--- + +## 自动解压功能 + +### 基本用法 + +```docker +# 自动解压 tar.gz 到目标目录 +ADD app.tar.gz /app/ +``` + +ADD 会识别并解压以下格式: +- `.tar` +- `.tar.gz` / `.tgz` +- `.tar.bz2` / `.tbz2` +- `.tar.xz` / `.txz` + +### 实际应用 + +官方基础镜像通常使用 ADD 解压根文件系统: ```docker FROM scratch -ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz / -... +ADD ubuntu-noble-core-cloudimg-amd64-root.tar.gz / ``` -但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 `ADD` 命令了。 +### 解压过程 -在 Docker 官方的 [Dockerfile 最佳实践文档](../../appendix/best_practices.md) 中要求,尽可能的使用 `COPY`,因为 `COPY` 的语义很明确,就是复制文件而已,而 `ADD` 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 `ADD` 的场合,就是所提及的需要自动解压缩的场合。 +``` +ADD app.tar.gz /app/ + │ + ├─ 识别 .tar.gz 格式 + ├─ 自动解压 + └─ 内容放入 /app/ -另外需要注意的是,`ADD` 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。 +app.tar.gz 包含: /app/ 目录结果: +├── src/ ├── src/ +│ └── main.py │ └── main.py +└── config.json └── config.json +``` -因此在 `COPY` 和 `ADD` 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 `COPY` 指令,仅在需要自动解压缩的场合使用 `ADD`。 +--- -在使用该指令的时候还可以加上 `--chown=:` 选项来改变文件的所属用户及所属组。 +## URL 下载功能(不推荐) + +### 基本用法 ```docker -ADD --chown=55:mygroup files* /mydir/ -ADD --chown=bin files* /mydir/ -ADD --chown=1 files* /mydir/ -ADD --chown=10:11 files* /mydir/ +# 从 URL 下载文件 +ADD https://example.com/app.zip /app/app.zip ``` + +### 为什么不推荐 + +| 问题 | 说明 | +|------|------| +| 权限固定 | 下载的文件权限为 600,通常需要额外 RUN 修改 | +| 不会解压 | URL 下载的压缩包不会自动解压 | +| 缓存问题 | URL 内容变化时不会重新下载 | +| 层数增加 | 需要额外 RUN 清理 | + +### 推荐替代方案 + +```docker +# ❌ 不推荐:使用 ADD 下载 +ADD https://example.com/app.tar.gz /tmp/ +RUN tar -xzf /tmp/app.tar.gz -C /app && rm /tmp/app.tar.gz + +# ✅ 推荐:使用 RUN + curl +RUN curl -fsSL https://example.com/app.tar.gz | tar -xz -C /app +``` + +优势: +- 一条 RUN 完成下载、解压、清理 +- 减少镜像层数 +- 更清晰的构建意图 + +--- + +## 修改文件所有者 + +```docker +ADD --chown=node:node app.tar.gz /app/ +ADD --chown=1000:1000 files/ /app/ +``` + +--- + +## 何时使用 ADD + +### ✅ 适合使用 ADD + +```docker +# 解压本地 tar 文件 +FROM scratch +ADD rootfs.tar.gz / + +# 解压应用包 +ADD dist.tar.gz /app/ +``` + +### ❌ 不适合使用 ADD + +```docker +# 复制普通文件(用 COPY) +ADD package.json /app/ # ❌ +COPY package.json /app/ # ✅ + +# 下载文件(用 RUN + curl) +ADD https://example.com/file / # ❌ +RUN curl -fsSL ... -o /file # ✅ + +# 需要保留 tar 不解压(用 COPY) +ADD archive.tar.gz /archives/ # ❌ 会解压 +COPY archive.tar.gz /archives/ # ✅ 保持原样 +``` + +--- + +## 缓存行为 + +ADD 可能导致构建缓存失效: + +```docker +# 如果 app.tar.gz 内容变化,此层及后续层都需重建 +ADD app.tar.gz /app/ +RUN npm install +``` + +**优化建议**: + +```docker +# 先复制依赖文件 +COPY package*.json /app/ +RUN npm install + +# 再添加应用代码 +ADD app.tar.gz /app/ +``` + +--- + +## 最佳实践 + +### 1. 默认使用 COPY + +```docker +# ✅ 大多数场景使用 COPY +COPY . /app/ +``` + +### 2. 仅在需要解压时使用 ADD + +```docker +# ✅ 自动解压场景 +ADD app.tar.gz /app/ +``` + +### 3. 不要用 ADD 下载文件 + +```docker +# ❌ 避免 +ADD https://example.com/file.tar.gz /tmp/ + +# ✅ 推荐 +RUN curl -fsSL https://example.com/file.tar.gz | tar -xz -C /app +``` + +### 4. 解压后清理 + +```docker +# 如果需要控制解压过程 +COPY app.tar.gz /tmp/ +RUN tar -xzf /tmp/app.tar.gz -C /app && \ + rm /tmp/app.tar.gz +``` + +--- + +## 本章小结 + +| 场景 | 推荐指令 | +|------|---------| +| 复制普通文件 | `COPY` | +| 复制目录 | `COPY` | +| 自动解压 tar | `ADD` | +| 从 URL 下载 | `RUN curl` | +| 保持 tar 不解压 | `COPY` | + +## 延伸阅读 + +- [COPY 复制文件](copy.md):基本复制操作 +- [多阶段构建](../multistage-builds.md):减少镜像体积 +- [最佳实践](../../appendix/best_practices.md):Dockerfile 编写指南 diff --git a/image/dockerfile/arg.md b/image/dockerfile/arg.md index 8ec413f..f36c36a 100644 --- a/image/dockerfile/arg.md +++ b/image/dockerfile/arg.md @@ -1,68 +1,238 @@ # ARG 构建参数 -格式:`ARG <参数名>[=<默认值>]` - -构建参数和 `ENV` 的效果一样,都是设置环境变量。所不同的是,`ARG` 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 `ARG` 保存密码之类的信息,因为 `docker history` 还是可以看到所有值的。 - -`Dockerfile` 中的 `ARG` 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 `docker build` 中用 `--build-arg <参数名>=<值>` 来覆盖。 - -灵活的使用 `ARG` 指令,能够在不修改 Dockerfile 的情况下,构建出不同的镜像。 - -ARG 指令有生效范围,如果在 `FROM` 指令之前指定,那么只能用于 `FROM` 指令中。 +## 基本语法 ```docker -ARG DOCKER_USERNAME=library - -FROM ${DOCKER_USERNAME}/alpine - -RUN set -x ; echo ${DOCKER_USERNAME} +ARG <参数名>[=<默认值>] ``` -使用上述 Dockerfile 会发现无法输出 `${DOCKER_USERNAME}` 变量的值,要想正常输出,你必须在 `FROM` 之后再次指定 `ARG` +`ARG` 指令定义构建时的变量,可以在 `docker build` 时通过 `--build-arg` 传入。 + +--- + +## ARG vs ENV + +| 特性 | ARG | ENV | +|------|-----|-----| +| **生效时间** | 仅构建时 | 构建时 + 运行时 | +| **持久性** | 构建后消失 | 写入镜像 | +| **覆盖方式** | `docker build --build-arg` | `docker run -e` | +| **适用场景** | 构建参数(版本号等) | 应用配置 | +| **可见性** | `docker history` 可见 | `docker inspect` 可见 | + +``` +构建时 运行时 +├─ ARG VERSION=1.0 │ (ARG 已消失) +├─ ENV APP_ENV=prod │ APP_ENV=prod(仍存在) +└─ RUN echo $VERSION │ +``` + +> ⚠️ **安全提示**:不要用 ARG 传递密码等敏感信息,`docker history` 可以查看所有 ARG 值。 + +--- + +## 基本用法 + +### 定义和使用 ```docker -# 只在 FROM 中生效 -ARG DOCKER_USERNAME=library +# 定义有默认值的 ARG +ARG NODE_VERSION=20 -FROM ${DOCKER_USERNAME}/alpine - -# 要想在 FROM 之后使用,必须再次指定 -ARG DOCKER_USERNAME=library - -RUN set -x ; echo ${DOCKER_USERNAME} +# 使用 ARG +FROM node:${NODE_VERSION}-alpine +RUN echo "Using Node.js $NODE_VERSION" ``` -对于多阶段构建,尤其要注意这个问题 +### 构建时覆盖 + +```bash +# 使用默认值 +$ docker build -t myapp . + +# 覆盖默认值 +$ docker build --build-arg NODE_VERSION=18 -t myapp . +``` + +--- + +## ARG 的作用域 + +### FROM 之前的 ARG ```docker -# 这个变量在每个 FROM 中都生效 -ARG DOCKER_USERNAME=library +# FROM 之前的 ARG 只能用于 FROM 指令 +ARG REGISTRY=docker.io +ARG IMAGE_NAME=node -FROM ${DOCKER_USERNAME}/alpine +FROM ${REGISTRY}/${IMAGE_NAME}:20 -RUN set -x ; echo 1 - -FROM ${DOCKER_USERNAME}/alpine - -RUN set -x ; echo 2 +# ❌ 这里无法使用上面的 ARG +RUN echo $REGISTRY # 输出空 ``` -对于上述 Dockerfile 两个 `FROM` 指令都可以使用 `${DOCKER_USERNAME}`,对于在各个阶段中使用的变量都必须在每个阶段分别指定: +### FROM 之后重新声明 ```docker -ARG DOCKER_USERNAME=library +ARG NODE_VERSION=20 -FROM ${DOCKER_USERNAME}/alpine +FROM node:${NODE_VERSION}-alpine -# 在FROM 之后使用变量,必须在每个阶段分别指定 -ARG DOCKER_USERNAME=library - -RUN set -x ; echo ${DOCKER_USERNAME} - -FROM ${DOCKER_USERNAME}/alpine - -# 在FROM 之后使用变量,必须在每个阶段分别指定 -ARG DOCKER_USERNAME=library - -RUN set -x ; echo ${DOCKER_USERNAME} +# 需要再次声明才能使用 +ARG NODE_VERSION +RUN echo "Node version: $NODE_VERSION" ``` + +### 多阶段构建中的 ARG + +```docker +ARG BASE_VERSION=alpine + +FROM node:20-${BASE_VERSION} AS builder +# 需要重新声明 +ARG NODE_VERSION=20 +RUN echo "Building with Node $NODE_VERSION" + +FROM node:20-${BASE_VERSION} +# 每个阶段都需要重新声明 +ARG NODE_VERSION=20 +RUN echo "Running with Node $NODE_VERSION" +``` + +--- + +## 常见使用场景 + +### 1. 控制基础镜像版本 + +```docker +ARG ALPINE_VERSION=3.19 +FROM alpine:${ALPINE_VERSION} +``` + +```bash +$ docker build --build-arg ALPINE_VERSION=3.18 . +``` + +### 2. 设置软件版本 + +```docker +ARG NGINX_VERSION=1.25.0 + +RUN curl -fsSL https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz | tar -xz +``` + +### 3. 配置构建环境 + +```docker +ARG BUILD_ENV=production +ARG ENABLE_DEBUG=false + +RUN if [ "$ENABLE_DEBUG" = "true" ]; then \ + npm install --include=dev; \ + else \ + npm install --production; \ + fi +``` + +### 4. 配置私有仓库 + +```docker +ARG NPM_TOKEN + +RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc && \ + npm install && \ + rm ~/.npmrc +``` + +```bash +# 构建时传入 token +$ docker build --build-arg NPM_TOKEN=xxx . +``` + +--- + +## 将 ARG 传递给 ENV + +如果需要在运行时使用 ARG 的值: + +```docker +ARG VERSION=1.0.0 + +# 将 ARG 传递给 ENV +ENV APP_VERSION=$VERSION + +# 运行时可用 +CMD echo "App version: $APP_VERSION" +``` + +--- + +## 预定义 ARG + +Docker 提供了一些预定义的 ARG,无需声明即可使用: + +| ARG | 说明 | +|-----|------| +| `HTTP_PROXY` | HTTP 代理 | +| `HTTPS_PROXY` | HTTPS 代理 | +| `NO_PROXY` | 不使用代理的地址 | +| `FTP_PROXY` | FTP 代理 | + +```bash +# 构建时使用代理 +$ docker build --build-arg HTTP_PROXY=http://proxy:8080 . +``` + +--- + +## 最佳实践 + +### 1. 为 ARG 提供合理默认值 + +```docker +# ✅ 好:有默认值 +ARG NODE_VERSION=20 + +# ⚠️ 需要每次传入 +ARG NODE_VERSION +``` + +### 2. 不要用 ARG 存储敏感信息 + +```docker +# ❌ 错误:密码会被记录在镜像历史中 +ARG DB_PASSWORD +RUN echo "password=$DB_PASSWORD" > /app/.env + +# ✅ 正确:使用 secrets 或运行时环境变量 +``` + +### 3. 使用 ARG 提高构建灵活性 + +```docker +ARG BASE_IMAGE=python:3.12-slim +FROM ${BASE_IMAGE} + +# 可以构建不同基础镜像的版本 +# docker build --build-arg BASE_IMAGE=python:3.11-alpine . +``` + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 定义构建时变量 | +| **语法** | `ARG NAME=value` | +| **覆盖** | `docker build --build-arg NAME=value` | +| **作用域** | FROM 之后需要重新声明 | +| **vs ENV** | ARG 仅构建时,ENV 构建+运行时 | +| **安全** | 不要存储敏感信息 | + +## 延伸阅读 + +- [ENV 设置环境变量](env.md):运行时环境变量 +- [FROM 指令](../../image/build.md):基础镜像指定 +- [多阶段构建](../multistage-builds.md):复杂构建场景 diff --git a/image/dockerfile/cmd.md b/image/dockerfile/cmd.md index 2360c9f..91edf30 100644 --- a/image/dockerfile/cmd.md +++ b/image/dockerfile/cmd.md @@ -1,49 +1,268 @@ # CMD 容器启动命令 -`CMD` 指令的格式和 `RUN` 相似,也是两种格式: +## 什么是 CMD -* `shell` 格式:`CMD <命令>` -* `exec` 格式:`CMD ["可执行文件", "参数1", "参数2"...]` -* 参数列表格式:`CMD ["参数1", "参数2"...]`。在指定了 `ENTRYPOINT` 指令后,用 `CMD` 指定具体的参数。 +`CMD` 指令用于指定容器启动时默认执行的命令。它定义了容器的"主进程"。 -之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。`CMD` 指令就是用于指定默认的容器主进程的启动命令的。 +> **核心概念**:容器的生命周期 = 主进程的生命周期。CMD 指定的命令就是这个主进程。 -在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,`ubuntu` 镜像默认的 `CMD` 是 `/bin/bash`,如果我们直接 `docker run -it ubuntu` 的话,会直接进入 `bash`。我们也可以在运行时指定运行别的命令,如 `docker run -it ubuntu cat /etc/os-release`。这就是用 `cat /etc/os-release` 命令替换了默认的 `/bin/bash` 命令了,输出了系统版本信息。 +--- -在指令格式上,一般推荐使用 `exec` 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 `"`,而不要使用单引号。 +## 语法格式 -如果使用 `shell` 格式的话,实际的命令会被包装为 `sh -c` 的参数的形式进行执行。比如: +CMD 有三种格式: -```docker -CMD echo $HOME -``` +| 格式 | 语法 | 推荐程度 | +|------|------|---------| +| **exec 格式** | `CMD ["可执行文件", "参数1", "参数2"]` | ✅ **推荐** | +| **shell 格式** | `CMD 命令 参数1 参数2` | ⚠️ 简单场景 | +| **参数格式** | `CMD ["参数1", "参数2"]` | 配合 ENTRYPOINT | -在实际执行中,会将其变更为: - -```docker -CMD [ "sh", "-c", "echo $HOME" ] -``` - -这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。 - -提到 `CMD` 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。 - -Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 `systemd` 去启动后台服务,容器内没有后台服务的概念。 - -一些初学者将 `CMD` 写为: - -```docker -CMD service nginx start -``` - -然后发现容器执行后就立即退出了。甚至在容器内去使用 `systemctl` 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。 - -对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。 - -而使用 `service nginx start` 命令,则是希望 init 系统以后台守护进程的形式启动 nginx 服务。而刚才说了 `CMD service nginx start` 会被理解为 `CMD [ "sh", "-c", "service nginx start"]`,因此主进程实际上是 `sh`。那么当 `service nginx start` 命令结束后,`sh` 也就结束了,`sh` 作为主进程退出了,自然就会令容器退出。 - -正确的做法是直接执行 `nginx` 可执行文件,并且要求以前台形式运行。比如: +### exec 格式(推荐) ```docker CMD ["nginx", "-g", "daemon off;"] +CMD ["python", "app.py"] +CMD ["node", "server.js"] ``` + +**优点**: +- 直接执行指定程序,是容器的 PID 1 +- 正确接收信号(如 SIGTERM) +- 无需 shell 解析 + +### shell 格式 + +```docker +CMD echo "Hello World" +CMD nginx -g "daemon off;" +``` + +**实际执行**:会被包装为 `sh -c` + +```docker +# 你写的 +CMD echo $HOME + +# 实际执行的 +CMD ["sh", "-c", "echo $HOME"] +``` + +**优点**:可以使用环境变量、管道等 shell 特性 +**缺点**:主进程是 sh,信号无法正确传递给应用 + +--- + +## exec 格式 vs shell 格式 + +| 特性 | exec 格式 | shell 格式 | +|------|----------|-----------| +| 主进程 | 指定的程序 | `/bin/sh` | +| 信号传递 | ✅ 正确 | ❌ 无法传递 | +| 环境变量 | ❌ 需要 shell 包装 | ✅ 自动解析 | +| 推荐使用 | ✅ 大多数场景 | 需要 shell 特性时 | + +### 信号传递问题示例 + +```docker +# ❌ shell 格式:docker stop 会超时 +CMD node server.js +# 实际是 sh -c "node server.js" +# SIGTERM 发给 sh,不会传递给 node + +# ✅ exec 格式:docker stop 正常工作 +CMD ["node", "server.js"] +# SIGTERM 直接发给 node +``` + +--- + +## 运行时覆盖 CMD + +`docker run` 后的命令会覆盖 Dockerfile 中的 CMD: + +```bash +# ubuntu 默认 CMD 是 /bin/bash +$ docker run -it ubuntu # 进入 bash +$ docker run ubuntu cat /etc/os-release # 覆盖为 cat 命令 +``` + +``` +Dockerfile: docker run 命令: +CMD ["/bin/bash"] + cat /etc/os-release + │ │ + └───────► 被覆盖 ◄───────┘ + ↓ + 执行: cat /etc/os-release +``` + +--- + +## 经典错误:容器立即退出 + +### 错误示例 + +```docker +# ❌ 容器启动后立即退出 +CMD service nginx start +``` + +### 原因分析 + +``` +1. CMD service nginx start + ↓ 被转换为 +2. CMD ["sh", "-c", "service nginx start"] + ↓ +3. sh 启动,执行 service 命令 + ↓ +4. service 命令将 nginx 放到后台 + ↓ +5. service 命令结束,sh 退出 + ↓ +6. 容器主进程(sh)退出 → 容器停止 +``` + +### 正确做法 + +```docker +# ✅ 让 nginx 在前台运行 +CMD ["nginx", "-g", "daemon off;"] +``` + +--- + +## CMD vs ENTRYPOINT + +| 指令 | 用途 | 运行时行为 | +|------|------|-----------| +| **CMD** | 默认命令 | `docker run` 参数会**覆盖**它 | +| **ENTRYPOINT** | 入口点 | `docker run` 参数会**追加**到它后面 | + +### 单独使用 CMD + +```docker +# Dockerfile +CMD ["curl", "-s", "http://example.com"] +``` + +```bash +$ docker run myimage # 执行默认命令 +$ docker run myimage curl -v ... # 完全覆盖 +``` + +### 搭配 ENTRYPOINT + +```docker +# Dockerfile +ENTRYPOINT ["curl", "-s"] +CMD ["http://example.com"] +``` + +```bash +$ docker run myimage # curl -s http://example.com +$ docker run myimage http://other.com # curl -s http://other.com(参数覆盖) +``` + +详见 [ENTRYPOINT 入口点](entrypoint.md) 章节。 + +--- + +## 最佳实践 + +### 1. 优先使用 exec 格式 + +```docker +# ✅ 推荐 +CMD ["python", "app.py"] + +# ⚠️ 仅在需要 shell 特性时使用 +CMD ["sh", "-c", "echo $PATH && python app.py"] +``` + +### 2. 确保应用在前台运行 + +```docker +# ✅ 前台运行 +CMD ["nginx", "-g", "daemon off;"] +CMD ["apache2ctl", "-D", "FOREGROUND"] +CMD ["java", "-jar", "app.jar"] + +# ❌ 不要使用后台服务命令 +CMD service nginx start +CMD systemctl start nginx +``` + +### 3. 使用双引号 + +```docker +# ✅ 正确:双引号 +CMD ["node", "server.js"] + +# ❌ 错误:单引号(JSON 不支持) +CMD ['node', 'server.js'] +``` + +### 4. 配合 ENTRYPOINT 使用 + +```docker +# 用于可配置参数的场景 +ENTRYPOINT ["python", "app.py"] +CMD ["--port", "8080"] + +# 运行时可以覆盖端口 +$ docker run myapp --port 9000 +``` + +--- + +## 常见问题 + +### Q: CMD 可以写多个吗? + +不可以。多个 CMD 只有最后一个生效: + +```docker +CMD ["echo", "first"] +CMD ["echo", "second"] # 只有这个生效 +``` + +### Q: 如何在 CMD 中使用环境变量? + +```docker +# 方法1:使用 shell 格式 +CMD echo "Port is $PORT" + +# 方法2:显式使用 sh -c +CMD ["sh", "-c", "echo Port is $PORT"] +``` + +### Q: 为什么我的容器不响应 Ctrl+C? + +可能是使用了 shell 格式,信号被 sh 吃掉了: + +```docker +# ❌ 信号无法传递 +CMD python app.py + +# ✅ 信号正确传递 +CMD ["python", "app.py"] +``` + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 指定容器启动时的默认命令 | +| **推荐格式** | exec 格式 `CMD ["程序", "参数"]` | +| **覆盖方式** | `docker run image 新命令` | +| **与 ENTRYPOINT** | CMD 作为 ENTRYPOINT 的默认参数 | +| **核心原则** | 应用必须在前台运行 | + +## 延伸阅读 + +- [ENTRYPOINT 入口点](entrypoint.md):固定的启动命令 +- [后台运行](../../container/daemon.md):容器前台/后台概念 +- [最佳实践](../../appendix/best_practices.md):Dockerfile 编写指南 diff --git a/image/dockerfile/copy.md b/image/dockerfile/copy.md index 62a92c3..7bf01fd 100644 --- a/image/dockerfile/copy.md +++ b/image/dockerfile/copy.md @@ -1,46 +1,261 @@ # COPY 复制文件 -格式: - -* `COPY [--chown=:] <源路径>... <目标路径>` -* `COPY [--chown=:] ["<源路径1>",... "<目标路径>"]` - -和 `RUN` 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。 - -`COPY` 指令将从构建上下文目录中 `<源路径>` 的文件/目录复制到新的一层的镜像内的 `<目标路径>` 位置。比如: +## 基本语法 ```docker -COPY package.json /usr/src/app/ +COPY [选项] <源路径>... <目标路径> +COPY [选项] ["<源路径1>", "<源路径2>", ... "<目标路径>"] ``` -`<源路径>` 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 [`filepath.Match`](https://golang.org/pkg/path/filepath/#Match) 规则,如: +`COPY` 指令将构建上下文中的文件或目录复制到镜像内。 + +--- + +## 基本用法 + +### 复制单个文件 ```docker -COPY hom* /mydir/ -COPY hom?.txt /mydir/ +# 复制文件到指定目录 +COPY package.json /app/ + +# 复制文件并重命名 +COPY config.json /app/settings.json ``` -`<目标路径>` 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 `WORKDIR` 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。 - -此外,还需要注意一点,使用 `COPY` 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。 - -在使用该指令的时候还可以加上 `--chown=:` 选项来改变文件的所属用户及所属组。 +### 复制多个文件 ```docker -COPY --chown=55:mygroup files* /mydir/ -COPY --chown=bin files* /mydir/ -COPY --chown=1 files* /mydir/ -COPY --chown=10:11 files* /mydir/ +# 复制多个指定文件 +COPY package.json package-lock.json /app/ + +# 使用通配符 +COPY *.json /app/ +COPY src/*.js /app/src/ ``` -如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。 - -## 使用 `--link` 优化多阶段构建 - -在 BuildKit 中,可以使用 `--link` 选项来优化多阶段构建的性能。使用 `--link` 后,文件会以独立层的形式添加,无需依赖前序指令的结果: +### 复制目录 ```docker +# 复制整个目录的内容(不是目录本身) +COPY src/ /app/src/ +``` + +> ⚠️ **注意**:复制目录时,复制的是目录的**内容**,不包含目录本身。 + +``` +构建上下文: 镜像内: +src/ /app/src/ +├── index.js → ├── index.js +└── utils.js └── utils.js +``` + +--- + +## 通配符规则 + +COPY 支持 Go 的 `filepath.Match` 通配符规则: + +| 通配符 | 说明 | 示例 | +|--------|------|------| +| `*` | 匹配任意字符序列 | `*.json` | +| `?` | 匹配单个字符 | `config?.json` | +| `[abc]` | 匹配括号内任一字符 | `[abc].txt` | +| `[a-z]` | 匹配范围内字符 | `file[0-9].txt` | + +```docker +COPY hom* /mydir/ # home.txt, homework.md 等 +COPY hom?.txt /mydir/ # home.txt, homy.txt 等 +COPY app[0-9].js /app/ # app0.js ~ app9.js +``` + +--- + +## 目标路径 + +### 绝对路径 + +```docker +COPY app.js /usr/src/app/ +``` + +### 相对路径(基于 WORKDIR) + +```docker +WORKDIR /app +COPY package.json ./ # 复制到 /app/package.json +COPY src/ ./src/ # 复制到 /app/src/ +``` + +### 自动创建目录 + +如果目标目录不存在,Docker 会自动创建: + +```docker +# /app/config/ 不存在也会自动创建 +COPY settings.json /app/config/ +``` + +--- + +## 修改文件所有者 + +使用 `--chown` 选项设置文件的用户和组: + +```docker +# 使用用户名和组名 +COPY --chown=node:node package.json /app/ + +# 使用 UID 和 GID +COPY --chown=1000:1000 . /app/ + +# 只指定用户 +COPY --chown=node . /app/ +``` + +> 💡 结合 `USER` 指令使用,确保应用以非 root 用户运行。 + +--- + +## 保留文件元数据 + +COPY 会保留源文件的元数据: +- 读、写、执行权限 +- 修改时间 + +这对于脚本文件特别重要: + +```docker +# start.sh 的可执行权限会被保留 +COPY start.sh /app/ +``` + +--- + +## COPY vs ADD + +| 特性 | COPY | ADD | +|------|------|-----| +| 复制本地文件 | ✅ | ✅ | +| 自动解压 tar | ❌ | ✅ | +| 支持 URL | ❌ | ✅(不推荐) | +| 推荐程度 | ✅ **推荐** | ⚠️ 特殊场景使用 | + +```docker +# 推荐:使用 COPY +COPY app.tar.gz /app/ +RUN tar -xzf /app/app.tar.gz + +# ADD 会自动解压(行为不明显,不推荐) +ADD app.tar.gz /app/ +``` + +> 笔者建议:除非需要自动解压 tar 文件,否则始终使用 COPY。明确的行为比隐式的魔法更好。 + +--- + +## 多阶段构建中的 COPY + +### 从其他构建阶段复制 + +```docker +# 构建阶段 +FROM node:20 AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# 生产阶段 +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +``` + +### 使用 --link 优化缓存(BuildKit) + +```docker +# 使用 --link 后,文件以独立层添加,不依赖前序指令 COPY --link --from=builder /app/dist /usr/share/nginx/html ``` -这样可以更高效地利用缓存,加速构建过程。 +`--link` 的优势: +- 更高效利用构建缓存 +- 并行化构建过程 +- 加速多阶段构建 + +--- + +## .dockerignore + +使用 `.dockerignore` 排除不需要复制的文件: + +```gitignore +# .dockerignore +node_modules +.git +.env +*.log +Dockerfile +.dockerignore +``` + +这可以: +- 减小构建上下文大小 +- 加速构建 +- 避免复制敏感文件 + +--- + +## 最佳实践 + +### 1. 利用缓存,先复制依赖文件 + +```docker +# ✅ 好:先复制依赖定义,再安装,最后复制代码 +COPY package.json package-lock.json ./ +RUN npm install +COPY . . + +# ❌ 差:一次性复制所有文件,代码变更会导致重新 npm install +COPY . . +RUN npm install +``` + +### 2. 使用 .dockerignore + +```docker +# 确保 node_modules 不被复制 +COPY . . +# .dockerignore 中应包含 node_modules +``` + +### 3. 明确复制路径 + +```docker +# ✅ 好:明确的路径 +COPY src/ /app/src/ +COPY package.json /app/ + +# ❌ 差:过于宽泛 +COPY . . +``` + +--- + +## 本章小结 + +| 操作 | 示例 | +|------|------| +| 复制文件 | `COPY app.js /app/` | +| 复制多个文件 | `COPY *.json /app/` | +| 复制目录内容 | `COPY src/ /app/src/` | +| 修改所有者 | `COPY --chown=node:node . /app/` | +| 从构建阶段复制 | `COPY --from=builder /app/dist ./` | + +## 延伸阅读 + +- [ADD 指令](add.md):复制和解压 +- [WORKDIR 指令](workdir.md):设置工作目录 +- [多阶段构建](../multistage-builds.md):优化镜像大小 +- [最佳实践](../../appendix/best_practices.md):Dockerfile 编写指南 diff --git a/image/dockerfile/entrypoint.md b/image/dockerfile/entrypoint.md index f8d0ba4..81d4b4d 100644 --- a/image/dockerfile/entrypoint.md +++ b/image/dockerfile/entrypoint.md @@ -1,124 +1,306 @@ # ENTRYPOINT 入口点 -`ENTRYPOINT` 的格式和 `RUN` 指令格式一样,分为 `exec` 格式和 `shell` 格式。 +## 什么是 ENTRYPOINT -`ENTRYPOINT` 的目的和 `CMD` 一样,都是在指定容器启动程序及参数。`ENTRYPOINT` 在运行时也可以替代,不过比 `CMD` 要略显繁琐,需要通过 `docker run` 的参数 `--entrypoint` 来指定。 +`ENTRYPOINT` 指定容器启动时运行的入口程序。与 CMD 不同,ENTRYPOINT 定义的命令不会被 `docker run` 的参数覆盖,而是**接收这些参数**。 -当指定了 `ENTRYPOINT` 后,`CMD` 的含义就发生了改变,不再是直接的运行其命令,而是将 `CMD` 的内容作为参数传给 `ENTRYPOINT` 指令,换句话说实际执行时,将变为: +> **核心作用**:让镜像像一个可执行程序一样使用,`docker run` 的参数作为这个程序的参数。 -```bash - "" +--- + +## 语法格式 + +| 格式 | 语法 | 推荐程度 | +|------|------|---------| +| **exec 格式** | `ENTRYPOINT ["可执行文件", "参数1"]` | ✅ **推荐** | +| **shell 格式** | `ENTRYPOINT 命令 参数` | ⚠️ 不推荐 | + +```docker +# exec 格式(推荐) +ENTRYPOINT ["nginx", "-g", "daemon off;"] + +# shell 格式(不推荐) +ENTRYPOINT nginx -g "daemon off;" ``` -那么有了 `CMD` 后,为什么还要有 `ENTRYPOINT` 呢?这种 ` ""` 有什么好处么?让我们来看几个场景。 +--- -#### 场景一:让镜像变成像命令一样使用 +## ENTRYPOINT vs CMD -假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 `CMD` 来实现: +### 核心区别 + +| 特性 | ENTRYPOINT | CMD | +|------|------------|-----| +| **定位** | 固定的入口程序 | 默认参数 | +| **docker run 参数** | 追加为参数 | 完全覆盖 | +| **覆盖方式** | `--entrypoint` | 直接指定命令 | +| **适用场景** | 把镜像当命令用 | 提供默认行为 | + +### 行为对比 + +```docker +# 只用 CMD +CMD ["curl", "-s", "http://example.com"] +``` + +```bash +$ docker run myimage # curl -s http://example.com +$ docker run myimage -v # 执行 -v(错误!) +$ docker run myimage curl -v ... # curl -v ...(完全替换) +``` + +```docker +# 只用 ENTRYPOINT +ENTRYPOINT ["curl", "-s"] +``` + +```bash +$ docker run myimage # curl -s(缺参数) +$ docker run myimage http://example.com # curl -s http://example.com ✓ +``` + +```docker +# ENTRYPOINT + CMD 组合(推荐) +ENTRYPOINT ["curl", "-s"] +CMD ["http://example.com"] +``` + +```bash +$ docker run myimage # curl -s http://example.com(默认) +$ docker run myimage http://other.com # curl -s http://other.com ✓ +$ docker run myimage -v http://other.com # curl -s -v http://other.com ✓ +``` + +--- + +## 场景一:让镜像像命令一样使用 + +### 需求 + +创建一个查询公网 IP 的"命令"镜像。 + +### 使用 CMD 的问题 ```docker FROM ubuntu:24.04 -RUN apt-get update \ - && apt-get install -y curl \ - && rm -rf /var/lib/apt/lists/* -CMD [ "curl", "-s", "http://myip.ipip.net" ] +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +CMD ["curl", "-s", "http://myip.ipip.net"] ``` -假如我们使用 `docker build -t myip .` 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行: - ```bash -$ docker run myip -当前 IP:61.148.226.66 来自:北京市 联通 +$ docker run myip # ✓ 正常工作 +当前 IP:61.148.226.66 + +$ docker run myip -i # ✗ 错误! +exec: "-i": executable file not found +# -i 替换了整个 CMD,被当作可执行文件 ``` -嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 `CMD` 中可以看到实质的命令是 `curl`,那么如果我们希望显示 HTTP 头信息,就需要加上 `-i` 参数。那么我们可以直接加 `-i` 参数给 `docker run myip` 么? - -```bash -$ docker run myip -i -docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n". -``` - -我们可以看到可执行文件找不到的报错,`executable file not found`。之前我们说过,跟在镜像名后面的是 `command`,运行时会替换 `CMD` 的默认值。因此这里的 `-i` 替换了原来的 `CMD`,而不是添加在原来的 `curl -s http://myip.ipip.net` 后面。而 `-i` 根本不是命令,所以自然找不到。 - -那么如果我们希望加入 `-i` 这参数,我们就必须重新完整的输入这个命令: - -```bash -$ docker run myip curl -s http://myip.ipip.net -i -``` - -这显然不是很好的解决方案,而使用 `ENTRYPOINT` 就可以解决这个问题。现在我们重新用 `ENTRYPOINT` 来实现这个镜像: +### 使用 ENTRYPOINT 解决 ```docker FROM ubuntu:24.04 -RUN apt-get update \ - && apt-get install -y curl \ - && rm -rf /var/lib/apt/lists/* -ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ] +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"] ``` -这次我们再来尝试直接使用 `docker run myip -i`: - ```bash -$ docker run myip -当前 IP:61.148.226.66 来自:北京市 联通 +$ docker run myip # ✓ 正常工作 +当前 IP:61.148.226.66 -$ docker run myip -i +$ docker run myip -i # ✓ 添加 -i 参数 HTTP/1.1 200 OK -Server: nginx/1.8.0 -Date: Tue, 22 Nov 2016 05:12:40 GMT -Content-Type: text/html; charset=UTF-8 -Vary: Accept-Encoding -X-Powered-By: PHP/5.6.24-1~dotdeb+7.1 -X-Cache: MISS from cache-2 -X-Cache-Lookup: MISS from cache-2:80 -X-Cache: MISS from proxy-2_6 -Transfer-Encoding: chunked -Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006 -Connection: keep-alive - -当前 IP:61.148.226.66 来自:北京市 联通 +... +当前 IP:61.148.226.66 ``` -可以看到,这次成功了。这是因为当存在 `ENTRYPOINT` 后,`CMD` 的内容将会作为参数传给 `ENTRYPOINT`,而这里 `-i` 就是新的 `CMD`,因此会作为参数传给 `curl`,从而达到了我们预期的效果。 +### 交互图示 -#### 场景二:应用运行前的准备工作 +``` +ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"] + │ +docker run myip -i + │ + ▼ +curl -s http://myip.ipip.net -i +└─────────────────────────────┘ + ENTRYPOINT + docker run 参数 +``` -启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。 +--- -比如 `mysql` 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。 +## 场景二:启动前的准备工作 -此外,可能希望避免使用 `root` 用户去启动服务,从而提高安全性,而在启动服务前还需要以 `root` 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 `root` 身份执行,方便调试等。 +### 需求 -这些准备工作是和容器 `CMD` 无关的,无论 `CMD` 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 `ENTRYPOINT` 中去执行,而这个脚本会将接到的参数(也就是 ``)作为命令,在脚本最后执行。比如官方镜像 `redis` 中就是这么做的: +在启动主服务前执行初始化脚本(如数据库迁移、权限设置)。 + +### 实现方式 ```docker -FROM alpine:3.4 -... -RUN addgroup -S redis && adduser -S -G redis redis -... +FROM redis:7-alpine +COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] - -EXPOSE 6379 -CMD [ "redis-server" ] +CMD ["redis-server"] ``` -可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 `ENTRYPOINT` 为 `docker-entrypoint.sh` 脚本。 +**docker-entrypoint.sh**: ```bash #!/bin/sh -... -# allow the container to be started with `--user` -if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then - find . \! -user redis -exec chown redis '{}' + - exec gosu redis "$0" "$@" +set -e + +# 准备工作 +echo "Initializing..." + +# 如果第一个参数是 redis-server,以 redis 用户运行 +if [ "$1" = 'redis-server' ]; then + chown -R redis:redis /data + exec gosu redis "$@" fi +# 其他命令直接执行 exec "$@" ``` -该脚本的内容就是根据 `CMD` 的内容来判断,如果是 `redis-server` 的话,则切换到 `redis` 用户身份启动服务器,否则依旧使用 `root` 身份执行。比如: +### 工作流程 + +``` +docker run redis docker run redis bash + │ │ + ▼ ▼ +docker-entrypoint.sh redis-server docker-entrypoint.sh bash + │ │ + ├─ 初始化 ├─ 初始化 + ├─ chown -R redis:redis /data │ + └─ exec gosu redis redis-server └─ exec bash + (以 redis 用户运行) (以 root 用户运行) +``` + +### 关键点 + +1. **exec "$@"**:用传入的参数替换当前进程,确保信号正确传递 +2. **条件判断**:根据 CMD 不同执行不同逻辑 +3. **用户切换**:使用 `gosu` 切换用户(比 `su` 更适合容器) + +--- + +## 场景三:带参数的应用 + +```docker +FROM python:3.12-slim +WORKDIR /app +COPY . . +RUN pip install -r requirements.txt + +ENTRYPOINT ["python", "app.py"] +CMD ["--host", "0.0.0.0", "--port", "8080"] +``` ```bash -$ docker run -it redis id -uid=0(root) gid=0(root) groups=0(root) +# 使用默认参数 +$ docker run myapp +# 执行: python app.py --host 0.0.0.0 --port 8080 + +# 覆盖参数 +$ docker run myapp --host 0.0.0.0 --port 9000 +# 执行: python app.py --host 0.0.0.0 --port 9000 + +# 完全不同的参数 +$ docker run myapp --help +# 执行: python app.py --help ``` + +--- + +## 覆盖 ENTRYPOINT + +使用 `--entrypoint` 参数覆盖: + +```bash +# 正常运行 +$ docker run myimage + +# 覆盖 ENTRYPOINT 进入 shell 调试 +$ docker run --entrypoint /bin/sh myimage + +# 覆盖 ENTRYPOINT 并传入参数 +$ docker run --entrypoint /bin/cat myimage /etc/os-release +``` + +--- + +## ENTRYPOINT 与 CMD 组合表 + +| ENTRYPOINT | CMD | 最终执行命令 | +|------------|-----|-------------| +| 无 | 无 | 无(容器无法启动) | +| 无 | `["cmd", "p1"]` | `cmd p1` | +| `["ep", "p1"]` | 无 | `ep p1` | +| `["ep", "p1"]` | `["cmd", "p2"]` | `ep p1 cmd p2` | +| `ep p1`(shell) | `["cmd", "p2"]` | `/bin/sh -c "ep p1"`(CMD 被忽略) | + +> ⚠️ **注意**:shell 格式的 ENTRYPOINT 会忽略 CMD! + +--- + +## 最佳实践 + +### 1. 使用 exec 格式 + +```docker +# ✅ 推荐 +ENTRYPOINT ["python", "app.py"] + +# ❌ 避免 shell 格式 +ENTRYPOINT python app.py +``` + +### 2. 提供有意义的默认参数 + +```docker +ENTRYPOINT ["nginx"] +CMD ["-g", "daemon off;"] +``` + +### 3. 入口脚本使用 exec + +```bash +#!/bin/sh +# 准备工作... + +# 使用 exec 替换当前进程 +exec "$@" +``` + +### 4. 处理信号 + +确保 ENTRYPOINT 脚本能正确传递信号: + +```bash +#!/bin/bash +trap 'kill -TERM $PID' TERM INT + +# 启动应用 +app "$@" & +PID=$! + +# 等待应用退出 +wait $PID +``` + +--- + +## 本章小结 + +| ENTRYPOINT | CMD | 适用场景 | +|------------|-----|---------| +| ✓ | ✗ | 镜像作为固定命令使用 | +| ✗ | ✓ | 简单的默认命令 | +| ✓ | ✓ | **推荐**:固定命令 + 可配置参数 | + +## 延伸阅读 + +- [CMD 容器启动命令](cmd.md):默认命令 +- [最佳实践](../../appendix/best_practices.md):启动命令设计 +- [后台运行](../../container/daemon.md):前台/后台概念 diff --git a/image/dockerfile/env.md b/image/dockerfile/env.md index ca39362..23eb81c 100644 --- a/image/dockerfile/env.md +++ b/image/dockerfile/env.md @@ -1,35 +1,248 @@ # ENV 设置环境变量 -格式有两种: - -* `ENV ` -* `ENV = =...` - -这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 `RUN`,还是运行时的应用,都可以直接使用这里定义的环境变量。 +## 基本语法 ```docker -ENV VERSION=1.0 DEBUG=on \ - NAME="Happy Feet" +# 格式一:单个变量 +ENV + +# 格式二:多个变量(推荐) +ENV = = ... ``` -这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。 +--- -定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 `node` 镜像 `Dockerfile` 中,就有类似这样的代码: +## 基本用法 + +### 设置单个变量 ```docker -ENV NODE_VERSION 7.2.0 - -RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ - && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ - && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ - && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ - && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \ - && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \ - && ln -s /usr/local/bin/node /usr/local/bin/nodejs +ENV NODE_VERSION 20.10.0 +ENV APP_ENV production ``` -在这里先定义了环境变量 `NODE_VERSION`,其后的 `RUN` 这层里,多次使用 `$NODE_VERSION` 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 `7.2.0` 即可,`Dockerfile` 构建维护变得更轻松了。 +### 设置多个变量 -下列指令可以支持环境变量展开: `ADD`、`COPY`、`ENV`、`EXPOSE`、`FROM`、`LABEL`、`USER`、`WORKDIR`、`VOLUME`、`STOPSIGNAL`、`ONBUILD`、`RUN`。 +```docker +ENV NODE_VERSION=20.10.0 \ + APP_ENV=production \ + APP_NAME="My Application" +``` -可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 `Dockerfile` 制作更多的镜像,只需使用不同的环境变量即可。 +> 💡 包含空格的值用双引号括起来。 + +--- + +## 环境变量的作用 + +### 1. 后续指令中使用 + +```docker +ENV NODE_VERSION=20.10.0 + +# 在 RUN 中使用 +RUN curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz \ + | tar -xJ -C /usr/local --strip-components=1 + +# 在 WORKDIR 中使用 +ENV APP_HOME=/app +WORKDIR $APP_HOME + +# 在 COPY 中使用 +COPY . $APP_HOME +``` + +### 2. 容器运行时使用 + +```docker +ENV DATABASE_URL=postgres://localhost/mydb +``` + +应用代码中可以读取: + +```python +import os +db_url = os.environ.get('DATABASE_URL') +``` + +```javascript +const dbUrl = process.env.DATABASE_URL; +``` + +--- + +## 支持环境变量的指令 + +以下指令可以使用 `$变量名` 或 `${变量名}` 格式: + +| 指令 | 示例 | +|------|------| +| `RUN` | `RUN echo $VERSION` | +| `CMD` | `CMD ["sh", "-c", "echo $HOME"]` | +| `ENTRYPOINT` | 同上 | +| `COPY` | `COPY . $APP_HOME` | +| `ADD` | `ADD app.tar.gz $APP_HOME` | +| `WORKDIR` | `WORKDIR $APP_HOME` | +| `EXPOSE` | `EXPOSE $PORT` | +| `VOLUME` | `VOLUME $DATA_DIR` | +| `USER` | `USER $USERNAME` | +| `LABEL` | `LABEL version=$VERSION` | +| `FROM` | `FROM node:$NODE_VERSION` | + +--- + +## 运行时覆盖 + +使用 `-e` 或 `--env` 覆盖 Dockerfile 中定义的环境变量: + +```bash +# 覆盖单个变量 +$ docker run -e APP_ENV=development myimage + +# 覆盖多个变量 +$ docker run -e APP_ENV=development -e DEBUG=true myimage + +# 从环境变量文件读取 +$ docker run --env-file .env myimage +``` + +### .env 文件格式 + +```bash +# .env +APP_ENV=development +DEBUG=true +DATABASE_URL=postgres://localhost/mydb +``` + +--- + +## ENV vs ARG + +| 特性 | ENV | ARG | +|------|-----|-----| +| **生效时间** | 构建时 + 运行时 | 仅构建时 | +| **持久性** | 写入镜像,运行时可用 | 构建后消失 | +| **覆盖方式** | `docker run -e` | `docker build --build-arg` | +| **适用场景** | 应用配置 | 构建参数(如版本号) | + +### 组合使用 + +```docker +# ARG 接收构建时参数 +ARG NODE_VERSION=20 + +# ENV 保存到运行时 +ENV NODE_VERSION=$NODE_VERSION + +# 后续指令使用 +RUN curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/... +``` + +```bash +# 构建时指定版本 +$ docker build --build-arg NODE_VERSION=18 -t myapp . +``` + +--- + +## 最佳实践 + +### 1. 统一管理版本号 + +```docker +# ✅ 好:版本集中管理 +ENV NGINX_VERSION=1.25.0 \ + NODE_VERSION=20.10.0 \ + PYTHON_VERSION=3.12.0 + +RUN apt-get install nginx=${NGINX_VERSION} + +# ❌ 差:版本分散在各处 +RUN apt-get install nginx=1.25.0 +``` + +### 2. 不要存储敏感信息 + +```docker +# ❌ 错误:密码写入镜像 +ENV DB_PASSWORD=secret123 + +# ✅ 正确:运行时传入 +# docker run -e DB_PASSWORD=xxx myimage +``` + +### 3. 为应用提供合理默认值 + +```docker +ENV APP_ENV=production \ + APP_PORT=8080 \ + LOG_LEVEL=info +``` + +### 4. 使用有意义的变量名 + +```docker +# ✅ 好:清晰的命名 +ENV REDIS_HOST=localhost \ + REDIS_PORT=6379 + +# ❌ 差:模糊的命名 +ENV HOST=localhost \ + PORT=6379 +``` + +--- + +## 常见问题 + +### Q: 环境变量在 CMD 中不展开 + +exec 格式不会自动展开环境变量: + +```docker +# ❌ 不会展开 $PORT +CMD ["python", "app.py", "--port", "$PORT"] + +# ✅ 使用 shell 格式或显式调用 sh +CMD ["sh", "-c", "python app.py --port $PORT"] +``` + +### Q: 如何查看容器的环境变量 + +```bash +$ docker inspect mycontainer --format '{{json .Config.Env}}' +$ docker exec mycontainer env +``` + +### Q: 多行 ENV 还是多个 ENV + +```docker +# ✅ 推荐:减少层数 +ENV VAR1=value1 \ + VAR2=value2 \ + VAR3=value3 + +# ⚠️ 多个 ENV 会创建多层 +ENV VAR1=value1 +ENV VAR2=value2 +ENV VAR3=value3 +``` + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **语法** | `ENV KEY=value` | +| **作用范围** | 构建时 + 运行时 | +| **覆盖方式** | `docker run -e KEY=value` | +| **与 ARG** | ARG 仅构建时,ENV 持久化到运行时 | +| **安全** | 不要存储敏感信息 | + +## 延伸阅读 + +- [ARG 构建参数](arg.md):构建时变量 +- [Compose 环境变量](../../compose/compose_file.md):Compose 中的环境变量 +- [最佳实践](../../appendix/best_practices.md):Dockerfile 编写指南 diff --git a/image/dockerfile/expose.md b/image/dockerfile/expose.md index 48005f9..2743ddd 100644 --- a/image/dockerfile/expose.md +++ b/image/dockerfile/expose.md @@ -1,7 +1,219 @@ # EXPOSE 声明端口 -格式为 `EXPOSE <端口1> [<端口2>...]`。 +## 基本语法 -`EXPOSE` 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 `docker run -P` 时,会自动随机映射 `EXPOSE` 的端口。 +```docker +EXPOSE <端口> [<端口>/<协议>...] +``` -要将 `EXPOSE` 和在运行时使用 `-p <宿主端口>:<容器端口>` 区分开来。`-p`,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 `EXPOSE` 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。 +`EXPOSE` 声明容器运行时提供服务的端口。这是一个**文档性质的声明**,告诉使用者容器会监听哪些端口。 + +--- + +## 基本用法 + +```docker +# 声明单个端口 +EXPOSE 80 + +# 声明多个端口 +EXPOSE 80 443 + +# 声明 TCP 和 UDP 端口 +EXPOSE 80/tcp +EXPOSE 53/udp +``` + +--- + +## EXPOSE 的作用 + +### 1. 文档说明 + +告诉镜像使用者,容器将在哪些端口提供服务: + +```docker +# 使用者一看就知道这是 web 应用 +EXPOSE 80 443 +``` + +```bash +# 查看镜像暴露的端口 +$ docker inspect nginx --format '{{.Config.ExposedPorts}}' +map[80/tcp:{}] +``` + +### 2. 配合 -P 使用 + +使用 `docker run -P` 时,Docker 会自动映射 EXPOSE 的端口到宿主机随机端口: + +```docker +# Dockerfile +EXPOSE 80 +``` + +```bash +$ docker run -P nginx +$ docker port $(docker ps -q) +80/tcp -> 0.0.0.0:32768 +``` + +--- + +## EXPOSE vs -p + +| 特性 | EXPOSE | -p | +|------|--------|-----| +| **位置** | Dockerfile | docker run 命令 | +| **作用** | 声明/文档 | 实际端口映射 | +| **是否必需** | 否 | 是(外部访问时) | +| **映射发生时** | 不发生 | 运行时发生 | + +``` +┌────────────────────────────────────────────────────────────┐ +│ EXPOSE 80 │ +│ ↓ │ +│ 仅声明意图 │ +│ └───────────────────────────────────────┘ │ +│ │ +│ docker run -p │ +│ ↓ │ +│ ┌─────────────────────┐ │ +│ │ 实际端口映射 │ │ +│ │ 宿主机 ←→ 容器 │ │ +│ └─────────────────────┘ │ +└────────────────────────────────────────────────────────────┘ +``` + +### 没有 EXPOSE 也能 -p + +```docker +# 即使没有 EXPOSE,也可以使用 -p +FROM nginx +# 没有 EXPOSE +``` + +```bash +# 仍然可以映射端口 +$ docker run -p 8080:80 mynginx +``` + +--- + +## 常见误解 + +### 误解:EXPOSE 会打开端口 + +```docker +# ❌ 错误理解:这不会让容器可从外部访问 +EXPOSE 80 +``` + +EXPOSE 不会: +- 自动进行端口映射 +- 让服务可从外部访问 +- 在容器启动时开启端口监听 + +EXPOSE 只是元数据声明。容器是否实际监听该端口,取决于容器内的应用。 + +### 正确理解 + +```docker +# Dockerfile +FROM nginx +EXPOSE 80 # 1. 声明:这个容器会在 80 端口提供服务 +``` + +```bash +# 运行:需要 -p 才能从外部访问 +$ docker run -p 8080:80 nginx # 2. 映射:宿主机 8080 → 容器 80 +``` + +--- + +## 最佳实践 + +### 1. 总是声明应用使用的端口 + +```docker +# Web 服务 +FROM nginx +EXPOSE 80 443 + +# 数据库 +FROM postgres +EXPOSE 5432 + +# Redis +FROM redis +EXPOSE 6379 +``` + +### 2. 使用明确的协议 + +```docker +# 默认是 TCP +EXPOSE 80 + +# 明确指定 UDP +EXPOSE 53/udp + +# 同时支持 TCP 和 UDP +EXPOSE 53/tcp 53/udp +``` + +### 3. 与应用实际端口保持一致 + +```docker +# ✅ 好:EXPOSE 与应用端口一致 +ENV PORT=3000 +EXPOSE 3000 +CMD ["node", "server.js"] + +# ❌ 差:EXPOSE 与应用端口不一致(误导) +EXPOSE 80 +CMD ["node", "server.js"] # 实际监听 3000 +``` + +--- + +## 使用环境变量 + +```docker +ARG PORT=80 +EXPOSE $PORT +``` + +--- + +## 在 Compose 中 + +```yaml +services: + web: + build: . + ports: + - "8080:80" # 映射端口(类似 -p) + expose: + - "80" # 仅声明(类似 EXPOSE) +``` + +`expose` 在 Compose 中仅用于容器间通信的文档说明,不进行端口映射。 + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 声明容器提供服务的端口(文档) | +| **不会** | 自动映射端口或开放外部访问 | +| **配合** | `docker run -P` 自动映射 | +| **外部访问** | 需要 `-p 宿主机端口:容器端口` | +| **语法** | `EXPOSE 80` 或 `EXPOSE 80/tcp` | + +## 延伸阅读 + +- [网络配置](../../network/README.md):Docker 网络详解 +- [端口映射](../../network/port_bindingbindingbinding.md):-p 参数详解 +- [Compose 端口](../../compose/compose_file.md):Compose 中的端口配置 diff --git a/image/dockerfile/healthcheck.md b/image/dockerfile/healthcheck.md index 2684347..665e29a 100644 --- a/image/dockerfile/healthcheck.md +++ b/image/dockerfile/healthcheck.md @@ -1,83 +1,206 @@ # HEALTHCHECK 健康检查 -格式: +## 基本语法 -* `HEALTHCHECK [选项] CMD <命令>`:设置检查容器健康状况的命令 -* `HEALTHCHECK NONE`:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令 +```docker +HEALTHCHECK [选项] CMD <命令> +HEALTHCHECK NONE +``` -`HEALTHCHECK` 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。 +`HEALTHCHECK` 指令告诉 Docker 如何判断容器状态是否正常。这是保障服务高可用的重要机制。 -在没有 `HEALTHCHECK` 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。 +--- -而自 1.12 之后,Docker 提供了 `HEALTHCHECK` 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。 +## 为什么需要 HEALTHCHECK -当在一个镜像指定了 `HEALTHCHECK` 指令后,用其启动容器,初始状态会为 `starting`,在 `HEALTHCHECK` 指令检查成功后变为 `healthy`,如果连续一定次数失败,则会变为 `unhealthy`。 +在没有 HEALTHCHECK 之前,Docker 只能通过**进程退出码**来判断容器状态。 -`HEALTHCHECK` 支持下列选项: +**问题场景**: +- Web 服务死锁,无法响应请求,但进程仍在运行 +- 数据库正在启动中,尚未准备好接受连接 +- 应用陷入死循环,CPU 爆满但进程存活 -* `--interval=<间隔>`:两次健康检查的间隔,默认为 30 秒; -* `--timeout=<时长>`:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒; -* `--retries=<次数>`:当连续失败指定次数后,则将容器状态视为 `unhealthy`,默认 3 次。 +**引入 HEALTHCHECK 后**: +Docker 定期执行指定的检查命令,根据返回值判断容器是否"健康"。 -和 `CMD`, `ENTRYPOINT` 一样,`HEALTHCHECK` 只可以出现一次,如果写了多个,只有最后一个生效。 +``` +容器状态转换: +Starting ──成功──> Healthy ──失败N次──> Unhealthy + ▲ │ + └──────成功──────┘ +``` -在 `HEALTHCHECK [选项] CMD` 后面的命令,格式和 `ENTRYPOINT` 一样,分为 `shell` 格式,和 `exec` 格式。命令的返回值决定了该次健康检查的成功与否:`0`:成功;`1`:失败;`2`:保留,不要使用这个值。 +--- -假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 `curl` 来帮助判断,其 `Dockerfile` 的 `HEALTHCHECK` 可以这么写: +## 基本用法 + +### Web 服务检查 ```docker FROM nginx RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* -HEALTHCHECK --interval=5s --timeout=3s \ + +HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD curl -fs http://localhost/ || exit 1 ``` -这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 `curl -fs http://localhost/ || exit 1` 作为健康检查命令。 +### 命令返回值 -使用 `docker build` 来构建这个镜像: +- `0`: 成功 (healthy) +- `1`: 失败 (unhealthy) +- `2`: 保留值 (不使用) -```bash -$ docker build -t myweb:v1 . +### 常用选项 + +| 选项 | 说明 | 默认值 | +|------|------|--------| +| `--interval` | 两次检查的间隔 | 30s | +| `--timeout` | 检查命令的超时时间 | 30s | +| `--start-period` | 启动缓冲期(期间失败不计入次数) | 0s | +| `--retries` | 连续失败多少次标记为 unhealthy | 3 | + +--- + +## 屏蔽健康检查 + +如果基础镜像定义了 HEALTHCHECK,但你不想使用它: + +```docker +FROM my-base-image +HEALTHCHECK NONE ``` -构建好了后,我们启动一个容器: +--- -```bash -$ docker run -d --name web -p 80:80 myweb:v1 +## 常见检查脚本 + +### HTTP 服务 + +使用 `curl` 或 `wget`: + +```docker +# 使用 curl +HEALTHCHECK CMD curl -f http://localhost/ || exit 1 + +# 使用 wget (Alpine 默认包含) +HEALTHCHECK CMD wget -q --spider http://localhost/ || exit 1 ``` -当运行该镜像后,可以通过 `docker container ls` 看到最初的状态为 `(health: starting)`: +### 数据库 -```bash -$ docker container ls -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 3 seconds ago Up 2 seconds (health: starting) 80/tcp, 443/tcp web +```docker +# MySQL +HEALTHCHECK CMD mysqladmin ping -h localhost || exit 1 + +# Redis +HEALTHCHECK CMD redis-cli ping || exit 1 ``` -在等待几秒钟后,再次 `docker container ls`,就会看到健康状态变化为了 `(healthy)`: +### 自定义脚本 -```bash -$ docker container ls -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 18 seconds ago Up 16 seconds (healthy) 80/tcp, 443/tcp web +```docker +COPY healthcheck.sh /usr/local/bin/ +HEALTHCHECK CMD ["healthcheck.sh"] ``` -如果健康检查连续失败超过了重试次数,状态就会变为 `(unhealthy)`。 +--- -为了帮助排障,健康检查命令的输出(包括 `stdout` 以及 `stderr`)都会被存储于健康状态里,可以用 `docker inspect` 来查看。 +## 在 Compose 中使用 + +可以在 `docker-compose.yml` 中覆盖或定义健康检查: + +```yaml +services: + web: + image: nginx + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 1m30s + timeout: 10s + retries: 3 + start_period: 40s +``` + +带健康检查的依赖启动: + +```yaml +services: + web: + depends_on: + db: + condition: service_healthy # 等待 db 变健康才启动 web + db: + image: mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] +``` + +--- + +## 查看健康状态 ```bash -$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool +# 查看容器状态(包含健康信息) +$ docker ps +CONTAINER ID STATUS +abc123 Up 1 minute (healthy) +def456 Up 2 minutes (unhealthy) + +# 查看详细健康日志 +$ docker inspect --format '{{json .State.Health}}' mycontainer | jq { - "FailingStreak": 0, - "Log": [ - { - "End": "2016-11-25T14:35:37.940957051Z", - "ExitCode": 0, - "Output": "\n\n\nWelcome to nginx!\n\n\n\n

Welcome to nginx!

\n

If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.

\n\n

For online documentation and support please refer to\nnginx.org.
\nCommercial support is available at\nnginx.com.

\n\n

Thank you for using nginx.

\n\n\n", - "Start": "2016-11-25T14:35:37.780192565Z" - } - ], - "Status": "healthy" + "Status": "healthy", + "FailingStreak": 0, + "Log": [ + { + "Start": "...", + "End": "...", + "ExitCode": 0, + "Output": "..." + } + ] } ``` + +--- + +## 最佳实践 + +### 1. 避免副作用 + +健康检查会被频繁执行,不要在检查脚本中进行写操作或消耗大量资源的操作。 + +### 2. 使用轻量级工具 + +优先使用镜像中已有的工具(如 `wget`),避免为了健康检查安装庞大的依赖(如 `curl`)。 + +### 3. 设置合理的 Start Period + +应用启动可能需要时间(如 Java 应用)。设置 `--start-period` 可以防止在启动阶段因检查失败而误判。 + +```docker +# 给应用 1 分钟启动时间 +HEALTHCHECK --start-period=60s CMD curl -f http://localhost/ || exit 1 +``` + +### 4. 只检查核心依赖 + +健康检查应主要关注**当前服务**是否可用,而不是检查其下游依赖(数据库等)。下游依赖的检查应由应用逻辑处理。 + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 检测容器应用是否真实可用 | +| **命令** | `HEALTHCHECK [选项] CMD command` | +| **状态** | starting, healthy, unhealthy | +| **Compose** | 支持 `condition: service_healthy` 依赖 | +| **注意** | 避免副作用,节省资源 | + +## 延伸阅读 + +- [CMD 容器启动命令](cmd.md):启动主进程 +- [Compose 模板文件](../../compose/compose_file.md):Compose 中的健康检查 +- [Docker 调试](../../appendix/debug.md):容器排障 diff --git a/image/dockerfile/label.md b/image/dockerfile/label.md index 1f816de..e7b0158 100644 --- a/image/dockerfile/label.md +++ b/image/dockerfile/label.md @@ -1,17 +1,154 @@ -# LABEL 指令 +# LABEL 为镜像添加元数据 -`LABEL` 指令用来给镜像以键值对的形式添加一些元数据(metadata)。 +## 基本语法 ```docker -LABEL = = = ... +LABEL = = ... ``` -我们还可以用一些标签来申明镜像的作者、文档地址等: +`LABEL` 指令以键值对的形式给镜像添加元数据。这些数据不会影响镜像的功能,但可以帮助用户理解镜像,或被自动化工具使用。 + +--- + +## 为什么需要 LABEL + +1. **版本管理**:记录版本号、构建时间、Git Commit ID +2. **联系信息**:维护者邮箱、文档地址、支持渠道 +3. **自动化工具**: CI/CD 工具可以读取标签触发操作 +4. **许可证信息**:声明开源协议 + +--- + +## 基本用法 + +### 定义单个标签 ```docker -LABEL org.opencontainers.image.authors="yeasy" - -LABEL org.opencontainers.image.documentation="https://yeasy.gitbooks.io" +LABEL version="1.0" +LABEL description="这是一个 Web 应用服务器" ``` -具体可以参考 https://github.com/opencontainers/image-spec/blob/master/annotations.md +### 定义多个标签(推荐) + +```docker +LABEL maintainer="user@example.com" \ + version="1.2.0" \ + description="My App Description" \ + org.opencontainers.image.authors="Yeasy" +``` + +> 💡 包含空格的值需要用引号括起来。 + +--- + +## 常用标签规范 (OCI Annotations) + +为了标准和互操作性,推荐使用 [OCI Image Format Specification](https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys) 定义的标准标签: + +| 标签 Key | 说明 | 示例 | +|----------|------|------| +| `org.opencontainers.image.created` | 构建时间(RFC 3339) | `2024-01-01T00:00:00Z` | +| `org.opencontainers.image.authors` | 作者/维护者 | `support@example.com` | +| `org.opencontainers.image.url` | 项目主页 | `https://example.com` | +| `org.opencontainers.image.documentation`| 文档地址 | `https://example.com/docs` | +| `org.opencontainers.image.source` | 源码仓库 | `https://github.com/user/repo` | +| `org.opencontainers.image.version` | 版本号 | `1.0.0` | +| `org.opencontainers.image.licenses` | 许可证 | `MIT` | +| `org.opencontainers.image.title` | 镜像标题 | `My App` | +| `org.opencontainers.image.description` | 描述 | `Production ready web server` | + +### 示例 + +```docker +LABEL org.opencontainers.image.authors="yeasy" \ + org.opencontainers.image.documentation="https://yeasy.gitbooks.io" \ + org.opencontainers.image.source="https://github.com/yeasy/docker_practice" \ + org.opencontainers.image.licenses="MIT" +``` + +--- + +## MAINTAINER 指令(已废弃) + +旧版本的 Dockerfile 中常看到 `MAINTAINER` 指令: + +```docker +# ❌ 已弃用 +MAINTAINER user@example.com +``` + +现在推荐使用 `LABEL`: + +```docker +# ✅ 推荐 +LABEL maintainer="user@example.com" +# 或 +LABEL org.opencontainers.image.authors="user@example.com" +``` + +--- + +## 动态标签 + +配合 `ARG` 使用,可以在构建时动态注入标签: + +```docker +ARG BUILD_DATE +ARG VCS_REF + +LABEL org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.revision=$VCS_REF +``` + +构建命令: + +```bash +$ docker build \ + --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ + --build-arg VCS_REF=$(git rev-parse --short HEAD) \ + . +``` + +--- + +## 查看标签 + +### docker inspect + +查看镜像的标签信息: + +```bash +$ docker inspect nginx --format '{{json .Config.Labels}}' | jq +{ + "maintainer": "NGINX Docker Maintainers " +} +``` + +### 过滤器 + +可以使用标签过滤镜像: + +```bash +# 列出作者是 yeasy 的所有镜像 +$ docker images --filter "label=org.opencontainers.image.authors=yeasy" + +# 删除所有带有特定标签的镜像 +$ docker rmi $(docker images -q --filter "label=stage=builder") +``` + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 添加 key-value 元数据 | +| **语法** | `LABEL k=v k=v ...` | +| **规范** | 推荐使用 OCI 标准标签 | +| **弃用** | 不要再使用 `MAINTAINER` | +| **查看** | `docker inspect` | + +## 延伸阅读 + +- [OCI 标签规范](https://github.com/opencontainers/image-spec/blob/main/annotations.md) +- [Dockerfile 最佳实践](../../appendix/best_practices.md) diff --git a/image/dockerfile/onbuild.md b/image/dockerfile/onbuild.md index 50aa803..384bd5d 100644 --- a/image/dockerfile/onbuild.md +++ b/image/dockerfile/onbuild.md @@ -1,65 +1,151 @@ # ONBUILD 为他人做嫁衣裳 -格式:`ONBUILD <其它指令>`。 - -`ONBUILD` 是一个特殊的指令,它后面跟的是其它指令,比如 `RUN`, `COPY` 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。 - -`Dockerfile` 中的其它指令都是为了定制当前镜像而准备的,唯有 `ONBUILD` 是为了帮助别人定制自己而准备的。 - -假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 `npm` 进行包管理,所有依赖、配置、启动信息等会放到 `package.json` 文件里。在拿到程序代码后,需要先进行 `npm install` 才可以获得所有需要的依赖。然后就可以通过 `npm start` 来启动应用。因此,一般来说会这样写 `Dockerfile`: +## 基本语法 ```docker -FROM node:slim -RUN mkdir /app +ONBUILD <其它指令> +``` + +`ONBUILD` 是一个特殊的指令,它后面跟的是其它指令(如 `RUN`, `COPY` 等),这些指令**在当前镜像构建时不会执行**,只有当以当前镜像为基础镜像去构建下一级镜像时才会被执行。 + +--- + +## 为什么需要 ONBUILD + +`ONBUILD` 主要用于制作**语言栈基础镜像**或**框架基础镜像**。 + +### 场景:维护 Node.js 项目 + +假设你有多个 Node.js 项目,它们的构建流程都一样: +1. 创建目录 +2. 复制 `package.json` +3. 执行 `npm install` +4. 复制源码 +5. 启动应用 + +如果不使用 `ONBUILD`,每个项目的 Dockerfile 都要重复这些步骤,且通过 `COPY` 复制文件时,基础镜像无法预知子项目的文件名。 + +### 使用 ONBUILD 的解决方案 + +**基础镜像 (my-node-base)**: + +```docker +FROM node:20-alpine WORKDIR /app -COPY ./package.json /app -RUN [ "npm", "install" ] -COPY . /app/ -CMD [ "npm", "start" ] + +# 这些指令将在子镜像构建时执行 +ONBUILD COPY package*.json ./ +ONBUILD RUN npm install +ONBUILD COPY . . + +CMD ["npm", "start"] ``` -把这个 `Dockerfile` 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果我们还有第二个 Node.js 项目也差不多呢?好吧,那就再把这个 `Dockerfile` 复制到第二个项目里。那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题。 - -如果第一个 Node.js 项目在开发过程中,发现这个 `Dockerfile` 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 `Dockerfile`,再次构建,问题解决。第一个项目没问题了,但是第二个项目呢?虽然最初 `Dockerfile` 是复制、粘贴自第一个项目的,但是并不会因为第一个项目修复了他们的 `Dockerfile`,而第二个项目的 `Dockerfile` 就会被自动修复。 - -那么我们可不可以做一个基础镜像,然后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 `Dockerfile` 的变化,重新构建后就继承了基础镜像的更新?好吧,可以,让我们看看这样的结果。那么上面的这个 `Dockerfile` 就会变为: +**子项目 Dockerfile**: ```docker -FROM node:slim -RUN mkdir /app -WORKDIR /app -CMD [ "npm", "start" ] +FROM my-node-base +# 只需要一行! +# 构建时会自动执行 COPY 和 RUN ``` -这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 `my-node` 的话,各个项目内的自己的 `Dockerfile` 就变为: +--- + +## 执行机制 + +``` +基础镜像构建: +Dockerfile (含 ONBUILD) ──build──> 基础镜像 (记录了 ONBUILD 触发器) + (指令未执行) + +子镜像构建: +FROM 基础镜像 ──build──> 读取基础镜像触发器 ──> 执行触发器指令 ──> 继续执行子 Dockerfile +``` + +--- + +## 常见使用场景 + +### 1. 自动处理依赖安装 ```docker -FROM my-node -COPY ./package.json /app -RUN [ "npm", "install" ] -COPY . /app/ +# Python 基础镜像 +ONBUILD COPY requirements.txt ./ +ONBUILD RUN pip install -r requirements.txt ``` -基础镜像变化后,各个项目都用这个 `Dockerfile` 重新构建镜像,会继承基础镜像的更新。 - -那么,问题解决了么?没有。准确说,只解决了一半。如果这个 `Dockerfile` 里面有些东西需要调整呢?比如 `npm install` 都需要加一些参数,那怎么办?这一行 `RUN` 是不可能放入基础镜像的,因为涉及到了当前项目的 `./package.json`,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 `Dockerfile` 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。 - -`ONBUILD` 可以解决这个问题。让我们用 `ONBUILD` 重新写一下基础镜像的 `Dockerfile`: +### 2. 自动编译代码 ```docker -FROM node:slim -RUN mkdir /app -WORKDIR /app -ONBUILD COPY ./package.json /app -ONBUILD RUN [ "npm", "install" ] -ONBUILD COPY . /app/ -CMD [ "npm", "start" ] +# Go 基础镜像 +ONBUILD COPY . . +ONBUILD RUN go build -o app main.go ``` -这次我们回到原始的 `Dockerfile`,但是这次将项目相关的指令加上 `ONBUILD`,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 `Dockerfile` 就变成了简单地: +### 3. 处理静态资源 ```docker -FROM my-node +# Nginx 静态网站基础镜像 +ONBUILD COPY dist/ /usr/share/nginx/html/ ``` -是的,只有这么一行。当在各个项目目录中,用这个只有一行的 `Dockerfile` 构建镜像时,之前基础镜像的那三行 `ONBUILD` 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 `npm install`,生成应用镜像。 +--- + +## 注意事项 + +### 1. 继承性限制 + +`ONBUILD` 指令**只会继承一次**。 +- 镜像 A (含 ONBUILD) +- 镜像 B (FROM A) -> 触发 ONBUILD +- 镜像 C (FROM B) -> **不会**再次触发 ONBUILD + +### 2. 构建上下文 + +子镜像构建时,`ONBUILD COPY . .` 中的 `.` 指的是**子项目**的构建上下文,而不是基础镜像的上下文。 + +### 3. 不允许级联 + +`ONBUILD ONBUILD` 是非法的。你不能写 `ONBUILD ONBUILD COPY ...`。 + +### 4. 可能会导致构建失败 + +由于 `ONBUILD` 实际上是在子镜像中执行指令,如果子项目的上下文不满足要求(例如缺少 `package.json`),会导致子镜像构建失败,且错误信息可能比较隐晦。 + +--- + +## 最佳实践 + +### 1. 命名规范 + +建议在镜像标签中添加 `-onbuild` 后缀,明确告知使用者该镜像包含触发器。 + +``` +node:20-onbuild +python:3.12-onbuild +``` + +### 2. 避免执行耗时操作 + +尽量不要在 `ONBUILD` 中执行过于耗时或不确定的操作(如更新系统软件),这会让子镜像构建变得缓慢且不可控。 + +### 3. 清理工作 + +如果 `ONBUILD` 指令产生了临时文件,最好在同一个指令链中清理,或者提供机制让子镜像清理。 + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 定义在子镜像构建时执行的指令 | +| **语法** | `ONBUILD INSTRUCTION` | +| **适用** | 基础架构镜像(Node, Python, Go 等) | +| **限制** | 只继承一次,不可级联 | +| **规范** | 建议使用 `-onbuild` 标签后缀 | + +## 延伸阅读 + +- [COPY 指令](copy.md):文件复制 +- [Dockerfile 最佳实践](../../appendix/best_practices.md):基础镜像设计 diff --git a/image/dockerfile/shell.md b/image/dockerfile/shell.md index c207c39..bdde589 100644 --- a/image/dockerfile/shell.md +++ b/image/dockerfile/shell.md @@ -1,33 +1,141 @@ # SHELL 指令 -格式:`SHELL ["executable", "parameters"]` - -`SHELL` 指令可以指定 `RUN` `ENTRYPOINT` `CMD` 指令的 shell,Linux 中默认为 `["/bin/sh", "-c"]` +## 基本语法 ```docker +SHELL ["executable", "parameters"] +``` + +`SHELL` 指令允许覆盖 Docker 默认的 shell。 +- **Linux 默认**:`["/bin/sh", "-c"]` +- **Windows 默认**:`["cmd", "/S", "/C"]` + +该指令会影响后续的 `RUN`, `CMD`, `ENTRYPOINT` 指令(当它们使用 shell 格式时)。 + +--- + +## 为什么要用 SHELL 指令 + +### 1. 使用 bash 特性 + +默认的 `/bin/sh`(通常是 dash 或 alpine 的 ash)功能有限。如果你需要使用 bash 的特有功能(如数组、`{}` 扩展、`pipefail` 等),可以切换 shell。 + +```docker +FROM ubuntu:24.04 + +# 切换到 bash +SHELL ["/bin/bash", "-c"] + +# 现在可以使用 bash 特性了 +RUN echo {a..z} +``` + +### 2. 增强错误处理 (pipefail) + +默认情况下,管道命令 `cmd1 | cmd2` 只要 `cmd2` 成功,整个指令就视为成功。这可能掩盖构建错误。 + +```docker +# ❌ 这里的 wget 失败了,但构建继续(因为 tar 成功了) +RUN wget -O - https://invalid-url | tar xz +``` + +使用 `SHELL` 启用 `pipefail`: + +```docker +# ✅ 启用 pipefail +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# 如果 wget 失败,整个 RUN 就会失败 +RUN wget -O - https://invalid-url | tar xz +``` + +### 3. Windows 环境 + +在 Windows 容器中,经常需要在 `cmd` 和 `powershell` 之间切换。 + +```docker +FROM mcr.microsoft.com/windows/servercore:ltsc2022 + +# 默认是 cmd +RUN echo Default shell is cmd + +# 切换到 powershell +SHELL ["powershell", "-command"] +RUN Write-Host "Hello from PowerShell" + +# 切回 cmd +SHELL ["cmd", "/S", "/C"] +``` + +--- + +## 作用范围 + +`SHELL` 指令可以出现多次,每次只影响其后的指令: + +```docker +FROM ubuntu:24.04 + +# 使用默认 sh +RUN echo "Using sh" + +SHELL ["/bin/bash", "-c"] +# 使用 bash +RUN echo "Using bash" + SHELL ["/bin/sh", "-c"] - -RUN lll ; ls - -SHELL ["/bin/sh", "-cex"] - -RUN lll ; ls +# 回到 sh +RUN echo "Using sh again" ``` -两个 `RUN` 运行同一命令,第二个 `RUN` 运行的命令会打印出每条命令并当遇到错误时退出。 +--- -当 `ENTRYPOINT` `CMD` 以 shell 格式指定时,`SHELL` 指令所指定的 shell 也会成为这两个指令的 shell +## 对其他指令的影响 + +`SHELL` 影响的是所有使用 **shell 格式** 的指令: + +| 指令格式 | 是否受 SHELL 影响 | +|---------|-------------------| +| `RUN command` | ✅ 是 | +| `RUN ["exec", "param"]` | ❌ 否 | +| `CMD command` | ✅ 是 | +| `CMD ["exec", "param"]` | ❌ 否 | +| `ENTRYPOINT command` | ✅ 是 | +| `ENTRYPOINT ["exec", "param"]` | ❌ 否 | + +--- + +## 最佳实践 + +### 1. 推荐开启 pipefail + +对于使用 bash 的镜像,强烈建议开启 `pipefail`,以确保构建过程中的错误能被及时捕获。 ```docker -SHELL ["/bin/sh", "-cex"] - -# /bin/sh -cex "nginx" -ENTRYPOINT nginx +SHELL ["/bin/bash", "-o", "pipefail", "-c"] ``` -```docker -SHELL ["/bin/sh", "-cex"] +### 2. 明确意图 -# /bin/sh -cex "nginx" -CMD nginx -``` +如果由于脚本需求必须更改 shell,最好在 Dockerfile 中显式声明,而不是依赖默认行为。 + +### 3. 尽量保持一致 + +避免在 Dockerfile 中频繁切换 SHELL,这会使构建过程难以理解和调试。尽量在头部定义一次即可。 + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 更改 RUN/CMD/ENTRYPOINT 的默认 shell | +| **Linux 默认** | `["/bin/sh", "-c"]` | +| **Windows 默认** | `["cmd", "/S", "/C"]` | +| **推荐用法** | `SHELL ["/bin/bash", "-o", "pipefail", "-c"]` | +| **影响范围** | 后续所有使用 shell 格式的指令 | + +## 延伸阅读 + +- [RUN 指令](../../image/build.md):执行命令 +- [Dockerfile 最佳实践](../../appendix/best_practices.md):错误处理与调试 diff --git a/image/dockerfile/user.md b/image/dockerfile/user.md index 272c4f5..652e3b3 100644 --- a/image/dockerfile/user.md +++ b/image/dockerfile/user.md @@ -1,26 +1,273 @@ # USER 指定当前用户 -格式:`USER <用户名>[:<用户组>]` - -`USER` 指令和 `WORKDIR` 相似,都是改变环境状态并影响以后的层。`WORKDIR` 是改变工作目录,`USER` 则是改变之后层的执行 `RUN`, `CMD` 以及 `ENTRYPOINT` 这类命令的身份。 - -注意,`USER` 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。 +## 基本语法 ```docker -RUN groupadd -r redis && useradd -r -g redis redis -USER redis -RUN [ "redis-server" ] +USER <用户名>[:<用户组>] +USER [:] ``` -如果以 `root` 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 `su` 或者 `sudo`,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 [`gosu`](https://github.com/tianon/gosu)。 +`USER` 指令切换后续指令(RUN、CMD、ENTRYPOINT)的执行用户。 + +--- + +## 为什么要使用 USER + +> 笔者强调:以非 root 用户运行容器是最重要的安全实践之一。 + +``` +root 用户运行的风险: +┌────────────────────────────────────────────────────────┐ +│ 容器内 root ←─ 可能逃逸 ─→ 宿主机 root │ +│ │ │ │ +│ └── 漏洞利用 ───────────────→ 完全控制宿主机 │ +└────────────────────────────────────────────────────────┘ + +非 root 用户运行: +┌────────────────────────────────────────────────────────┐ +│ 容器内普通用户 ──逃逸后──→ 宿主机普通用户 │ +│ │ │ │ +│ └── 权限受限,危害降低 ─────→ 无法控制系统 │ +└────────────────────────────────────────────────────────┘ +``` + +--- + +## 基本用法 + +### 创建并切换用户 ```docker -# 建立 redis 用户,并使用 gosu 换另一个用户执行命令 -RUN groupadd -r redis && useradd -r -g redis redis -# 下载 gosu -RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \ - && chmod +x /usr/local/bin/gosu \ - && gosu nobody true -# 设置 CMD,并以另外的用户执行 -CMD [ "exec", "gosu", "redis", "redis-server" ] +FROM node:20-alpine + +# 1. 创建用户和组 +RUN addgroup -g 1001 appgroup && \ + adduser -u 1001 -G appgroup -D appuser + +# 2. 设置目录权限 +WORKDIR /app +COPY --chown=appuser:appgroup . . + +# 3. 切换用户 +USER appuser + +# 4. 后续命令以 appuser 身份运行 +CMD ["node", "server.js"] ``` + +### 使用 UID/GID + +```docker +# 也可以使用数字 +USER 1001:1001 +``` + +--- + +## 用户必须已存在 + +`USER` 指令只能切换到**已存在**的用户: + +```docker +# ❌ 错误:用户不存在 +USER nonexistent +# Error: unable to find user nonexistent + +# ✅ 正确:先创建用户 +RUN useradd -r -s /bin/false appuser +USER appuser +``` + +### 创建用户的方式 + +**Debian/Ubuntu**: + +```docker +RUN groupadd -r appgroup && \ + useradd -r -g appgroup appuser +``` + +**Alpine**: + +```docker +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S -G appgroup appuser +``` + +| 选项 | 说明 | +|------|------| +| `-r` (useradd) / `-S` (adduser) | 创建系统用户 | +| `-g` | 指定主组 | +| `-G` | 指定附加组 | +| `-u` | 指定 UID | +| `-s /bin/false` | 禁用登录 shell | + +--- + +## 运行时切换用户 + +### 使用 gosu(推荐) + +在 ENTRYPOINT 脚本中切换用户时,不要使用 `su` 或 `sudo`,应使用 [gosu](https://github.com/tianon/gosu): + +```docker +FROM debian:bookworm + +# 创建用户 +RUN groupadd -r redis && useradd -r -g redis redis + +# 安装 gosu +RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/* + +COPY docker-entrypoint.sh /usr/local/bin/ +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["redis-server"] +``` + +**docker-entrypoint.sh**: + +```bash +#!/bin/bash +set -e + +# 以 root 执行初始化 +chown -R redis:redis /data + +# 用 gosu 切换到 redis 用户运行服务 +exec gosu redis "$@" +``` + +### 为什么不用 su/sudo + +| 问题 | su/sudo | gosu | +|------|---------|------| +| TTY 要求 | 需要 | 不需要 | +| 信号传递 | 不正确 | 正确 | +| 子进程 | 是 | exec 替换 | +| 容器中使用 | ❌ | ✅ | + +--- + +## 运行时覆盖用户 + +使用 `-u` 或 `--user` 参数: + +```bash +# 以指定用户运行 +$ docker run -u 1001:1001 myimage + +# 以 root 运行(调试时) +$ docker run -u root myimage +``` + +--- + +## 文件权限处理 + +切换用户后,确保应用有权访问文件: + +```docker +FROM node:20-alpine + +# 创建用户 +RUN adduser -D -u 1001 appuser + +WORKDIR /app + +# 方式1:使用 --chown +COPY --chown=appuser:appuser . . + +# 方式2:手动 chown(减少层数) +# COPY . . +# RUN chown -R appuser:appuser /app + +USER appuser +CMD ["node", "server.js"] +``` + +--- + +## 最佳实践 + +### 1. 始终使用非 root 用户 + +```docker +# ✅ 推荐 +RUN adduser -D appuser +USER appuser +CMD ["myapp"] + +# ❌ 避免 +CMD ["myapp"] # 以 root 运行 +``` + +### 2. 使用固定 UID/GID + +便于在宿主机和容器间共享文件: + +```docker +# 使用常见的非 root UID +RUN addgroup -g 1000 -S appgroup && \ + adduser -u 1000 -S -G appgroup appuser +USER 1000:1000 +``` + +### 3. 多阶段构建中的 USER + +```docker +# 构建阶段可以用 root +FROM node:20 AS builder +WORKDIR /app +COPY . . +RUN npm install && npm run build + +# 生产阶段用非 root +FROM node:20-alpine +RUN adduser -D appuser +WORKDIR /app +COPY --from=builder --chown=appuser:appuser /app/dist . +USER appuser +CMD ["node", "server.js"] +``` + +--- + +## 常见问题 + +### Q: 权限被拒绝 + +```bash +permission denied: '/app/data.log' +``` + +**解决**:确保目录权限正确 + +```docker +RUN mkdir -p /app/data && chown appuser:appuser /app/data +``` + +### Q: 无法绑定低于 1024 的端口 + +非 root 用户无法绑定 80、443 等端口。 + +**解决**: +1. 使用高端口(如 8080) +2. 在运行时映射端口:`docker run -p 80:8080` + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 切换后续指令的执行用户 | +| **语法** | `USER username` 或 `USER UID:GID` | +| **前提** | 用户必须已存在 | +| **运行时覆盖** | `docker run -u` | +| **切换工具** | 使用 gosu,不用 su/sudo | + +## 延伸阅读 + +- [安全](../../security/README.md):容器安全实践 +- [ENTRYPOINT](entrypoint.md):入口脚本中的用户切换 +- [最佳实践](../../appendix/best_practices.md):Dockerfile 安全 diff --git a/image/dockerfile/volume.md b/image/dockerfile/volume.md index 0e8731c..c53687a 100644 --- a/image/dockerfile/volume.md +++ b/image/dockerfile/volume.md @@ -1,20 +1,247 @@ # VOLUME 定义匿名卷 -格式为: - -* `VOLUME ["<路径1>", "<路径2>"...]` -* `VOLUME <路径>` - -之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 `Dockerfile` 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。 +## 基本语法 ```docker +VOLUME ["/路径1", "/路径2"] +VOLUME /路径 +``` + +`VOLUME` 指令创建挂载点,并标记为外部挂载的卷。 + +--- + +## 为什么使用 VOLUME + +> **核心原则**:容器存储层应该保持无状态,任何运行时数据都应该存储在卷中。 + +``` +没有 VOLUME: 使用 VOLUME: +┌─────────────────────┐ ┌─────────────────────┐ +│ 容器存储层 │ │ 容器存储层 │ +│ ┌─────────────┐ │ │ (只读/无状态) │ +│ │ 数据库文件 │←─问题 │ │ +│ │ 日志文件 │ │ └──────────┬──────────┘ +│ │ 上传文件 │ │ │ +│ └─────────────┘ │ ┌──────────▼──────────┐ +└─────────────────────┘ │ 数据卷 │ +容器删除 = 数据丢失 │ ┌─────────────┐ │ + │ │ 持久化数据 │←─安全 + │ └─────────────┘ │ + └─────────────────────┘ + 容器删除,数据保留 +``` + +--- + +## 基本用法 + +### 定义单个卷 + +```docker +FROM mysql:8.0 +VOLUME /var/lib/mysql +``` + +### 定义多个卷 + +```docker +FROM myapp +VOLUME ["/data", "/logs", "/config"] +``` + +--- + +## VOLUME 的行为 + +### 1. 自动创建匿名卷 + +如果运行时未指定挂载,Docker 会自动创建匿名卷: + +```bash +$ docker run mysql:8.0 +$ docker volume ls +DRIVER VOLUME NAME +local a1b2c3d4e5f6... # 自动创建的匿名卷 +``` + +### 2. 可被命名卷覆盖 + +```bash +# 使用命名卷替代匿名卷 +$ docker run -v mysql_data:/var/lib/mysql mysql:8.0 +``` + +### 3. 可被 Bind Mount 覆盖 + +```bash +# 使用宿主机目录替代 +$ docker run -v /my/data:/var/lib/mysql mysql:8.0 +``` + +--- + +## VOLUME 在构建时的特殊行为 + +> ⚠️ **重要**:VOLUME 之后对该目录的修改会被丢弃! + +```docker +FROM ubuntu +VOLUME /data + +# ❌ 这个文件不会出现在镜像中! +RUN echo "hello" > /data/test.txt +``` + +**原因**:VOLUME 指令之后,Docker 将该目录视为外部挂载点,不再记录对它的修改。 + +### 正确做法 + +```docker +FROM ubuntu + +# ✅ 先写入文件 +RUN mkdir -p /data && echo "hello" > /data/test.txt + +# 再声明 VOLUME VOLUME /data ``` -这里的 `/data` 目录就会在容器运行时自动挂载为匿名卷,任何向 `/data` 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如: +--- -```bash -$ docker run -d -v mydata:/data xxxx +## 常见使用场景 + +### 数据库持久化 + +```docker +FROM postgres:15 +VOLUME /var/lib/postgresql/data ``` -在这行命令中,就使用了 `mydata` 这个命名卷挂载到了 `/data` 这个位置,替代了 `Dockerfile` 中定义的匿名卷的挂载配置。 +### 日志目录 + +```docker +FROM nginx +VOLUME /var/log/nginx +``` + +### 上传文件目录 + +```docker +FROM myapp +VOLUME /app/uploads +``` + +--- + +## 查看 VOLUME 定义 + +```bash +# 查看镜像定义的 VOLUME +$ docker inspect mysql:8.0 --format '{{json .Config.Volumes}}' | jq +{ + "/var/lib/mysql": {} +} + +# 查看容器挂载的卷 +$ docker inspect mycontainer --format '{{json .Mounts}}' | jq +``` + +--- + +## VOLUME vs docker run -v + +| 特性 | Dockerfile VOLUME | docker run -v | +|------|-------------------|---------------| +| **定义时机** | 镜像构建时 | 容器运行时 | +| **默认行为** | 创建匿名卷 | 可指定命名卷或路径 | +| **灵活性** | 低(固定路径) | 高(可任意指定) | +| **适用场景** | 定义必须持久化的路径 | 灵活的数据管理 | + +--- + +## 在 Compose 中 + +```yaml +services: + db: + image: postgres:15 + volumes: + # 命名卷(推荐) + - postgres_data:/var/lib/postgresql/data + # Bind Mount + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + +volumes: + postgres_data: # 声明命名卷 +``` + +--- + +## 安全注意事项 + +### 匿名卷可能导致数据丢失 + +```bash +# 使用 --rm 运行的容器,匿名卷会在容器删除时一起删除 +$ docker run --rm mysql:8.0 +# 容器停止后,数据丢失! +``` + +**解决**:始终使用命名卷 + +```bash +$ docker run -v mysql_data:/var/lib/mysql mysql:8.0 +``` + +--- + +## 最佳实践 + +### 1. 定义必须持久化的路径 + +```docker +# 数据库必须使用卷 +FROM postgres:15 +VOLUME /var/lib/postgresql/data +``` + +### 2. 不要在 VOLUME 后修改目录 + +```docker +# ❌ 避免 +VOLUME /app/data +RUN cp init-data.json /app/data/ + +# ✅ 正确 +RUN mkdir -p /app/data && cp init-data.json /app/data/ +VOLUME /app/data +``` + +### 3. 文档中说明 VOLUME 用途 + +```docker +# 持久化用户上传的文件 +VOLUME /app/uploads + +# 持久化数据库数据 +VOLUME /var/lib/mysql +``` + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 创建挂载点,标记为外部卷 | +| **语法** | `VOLUME /path` | +| **默认行为** | 自动创建匿名卷 | +| **覆盖方式** | `docker run -v name:/path` | +| **注意** | VOLUME 之后的修改会丢失 | + +## 延伸阅读 + +- [数据卷](../../data_management/volume.md):卷的管理和使用 +- [挂载主机目录](../../data_management/bind-mounts.md):Bind Mount +- [Compose 数据管理](../../compose/compose_file.md):Compose 中的卷配置 diff --git a/image/dockerfile/workdir.md b/image/dockerfile/workdir.md index b8bae25..8e0c536 100644 --- a/image/dockerfile/workdir.md +++ b/image/dockerfile/workdir.md @@ -1,36 +1,196 @@ # WORKDIR 指定工作目录 -格式为 `WORKDIR <工作目录路径>`。 - -使用 `WORKDIR` 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,`WORKDIR` 会帮你建立目录。 - -之前提到一些初学者常犯的错误是把 `Dockerfile` 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误: +## 基本语法 ```docker -RUN cd /app -RUN echo "hello" > world.txt +WORKDIR <工作目录路径> ``` -如果将这个 `Dockerfile` 进行构建镜像运行后,会发现找不到 `/app/world.txt` 文件,或者其内容不是 `hello`。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 `Dockerfile` 中,这两行 `RUN` 命令的执行环境根本不同,是两个完全不同的容器。这就是对 `Dockerfile` 构建分层存储的概念不了解所导致的错误。 +`WORKDIR` 指定后续指令的工作目录。如果目录不存在,Docker 会自动创建。 -之前说过每一个 `RUN` 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 `RUN cd /app` 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。 +--- -因此如果需要改变以后各层的工作目录的位置,那么应该使用 `WORKDIR` 指令。 +## 基本用法 ```docker WORKDIR /app -RUN echo "hello" > world.txt +RUN pwd # 输出 /app +RUN echo "hello" > world.txt # 创建 /app/world.txt +COPY . . # 复制到 /app/ ``` -如果你的 `WORKDIR` 指令使用的相对路径,那么所切换的路径与之前的 `WORKDIR` 有关: +--- + +## 为什么需要 WORKDIR + +### 常见错误 + +```docker +# ❌ 错误:cd 在下一个 RUN 中无效 +RUN cd /app +RUN echo "hello" > world.txt # 文件在根目录! +``` + +### 原因分析 + +``` +RUN cd /app + ↓ +启动容器 → cd /app(仅内存变化)→ 提交镜像层 → 容器销毁 + │ + ↓ 工作目录未改变! +RUN echo "hello" > world.txt + ↓ +启动新容器(工作目录在 /)→ 创建 /world.txt +``` + +每个 RUN 都在新容器中执行,**前一个 RUN 的内存状态(包括工作目录)不会保留**。 + +### 正确做法 + +```docker +# ✅ 正确:使用 WORKDIR +WORKDIR /app +RUN echo "hello" > world.txt # 创建 /app/world.txt +``` + +--- + +## 相对路径 + +WORKDIR 支持相对路径,基于上一个 WORKDIR: ```docker WORKDIR /a WORKDIR b WORKDIR c -RUN pwd +RUN pwd # 输出 /a/b/c ``` -`RUN pwd` 的工作目录为 `/a/b/c`。 +--- + +## 使用环境变量 + +```docker +ENV APP_HOME=/app +WORKDIR $APP_HOME + +RUN pwd # 输出 /app +``` + +--- + +## 多阶段构建中的 WORKDIR + +```docker +# 构建阶段 +FROM node:20 AS builder +WORKDIR /build +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# 生产阶段 +FROM nginx:alpine +WORKDIR /usr/share/nginx/html +COPY --from=builder /build/dist . +``` + +--- + +## 最佳实践 + +### 1. 尽早设置 WORKDIR + +```docker +FROM node:20 +WORKDIR /app # 尽早设置 + +COPY package*.json ./ +RUN npm install +COPY . . +CMD ["node", "server.js"] +``` + +### 2. 使用绝对路径 + +```docker +# ✅ 推荐:绝对路径,意图明确 +WORKDIR /app + +# ⚠️ 避免:相对路径可能造成混淆 +WORKDIR app +``` + +### 3. 不要用 RUN cd + +```docker +# ❌ 避免 +RUN cd /app && echo "hello" > world.txt + +# ✅ 推荐 +WORKDIR /app +RUN echo "hello" > world.txt +``` + +### 4. 适时重置 WORKDIR + +```docker +WORKDIR /app +# ... 应用相关操作 ... + +WORKDIR /data +# ... 数据相关操作 ... +``` + +--- + +## 与其他指令的关系 + +| 指令 | WORKDIR 的影响 | +|------|---------------| +| `RUN` | 在 WORKDIR 中执行命令 | +| `CMD` | 在 WORKDIR 中启动 | +| `ENTRYPOINT` | 在 WORKDIR 中启动 | +| `COPY` | 相对目标路径基于 WORKDIR | +| `ADD` | 相对目标路径基于 WORKDIR | + +```docker +WORKDIR /app + +RUN pwd # /app +COPY . . # 复制到 /app +CMD ["./start.sh"] # /app/start.sh +``` + +--- + +## 运行时覆盖 + +使用 `-w` 参数覆盖工作目录: + +```bash +$ docker run -w /tmp myimage pwd +/tmp +``` + +--- + +## 本章小结 + +| 要点 | 说明 | +|------|------| +| **作用** | 设置后续指令的工作目录 | +| **语法** | `WORKDIR /path` | +| **自动创建** | 目录不存在会自动创建 | +| **持久性** | 影响后续所有指令,直到下次 WORKDIR | +| **不要用** | `RUN cd /path`(无效) | + +## 延伸阅读 + +- [COPY 复制文件](copy.md):文件复制 +- [RUN 执行命令](../../image/build.md):执行构建命令 +- [最佳实践](../../appendix/best_practices.md):Dockerfile 编写指南 diff --git a/image/list.md b/image/list.md index 1f6403f..92c8d0a 100644 --- a/image/list.md +++ b/image/list.md @@ -1,150 +1,258 @@ # 列出镜像 -要想列出已经下载下来的镜像,可以使用 `docker image ls` 命令。 +## 基本用法 + +查看本地已下载的镜像: ```bash $ docker image ls -REPOSITORY TAG IMAGE ID CREATED SIZE -redis latest 5f515359c7f8 5 days ago 183 MB -nginx latest 05a60462f8ba 5 days ago 181 MB -mongo 3.2 fe9198c04d62 5 days ago 342 MB - 00285df0df87 5 days ago 342 MB -ubuntu 18.04 329ed837d508 3 days ago 63.3MB -ubuntu bionic 329ed837d508 3 days ago 63.3MB +REPOSITORY TAG IMAGE ID CREATED SIZE +redis latest 5f515359c7f8 5 days ago 183MB +nginx latest 05a60462f8ba 5 days ago 181MB +ubuntu 24.04 329ed837d508 3 days ago 78MB +ubuntu noble 329ed837d508 3 days ago 78MB ``` -列表包含了 `仓库名`、`标签`、`镜像 ID`、`创建时间` 以及 `所占用的空间`。 +> 💡 `docker images` 是 `docker image ls` 的简写,两者等效。 -其中仓库名、标签在之前的基础概念章节已经介绍过了。**镜像 ID** 则是镜像的唯一标识,一个镜像可以对应多个 **标签**。因此,在上面的例子中,我们可以看到 `ubuntu:24.04` 和 `ubuntu:noble` 拥有相同的 ID,因为它们对应的是同一个镜像。 +--- -## 镜像体积 +## 输出字段说明 -如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,`ubuntu:24.04` 镜像大小,在这里是 `78MB`,但是在 [Docker Hub](https://hub.docker.com/_/ubuntu) 显示的却是 `29MB`。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 `docker image ls` 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。 +| 字段 | 说明 | +|------|------| +| **REPOSITORY** | 仓库名 | +| **TAG** | 标签(版本) | +| **IMAGE ID** | 镜像唯一标识(短 ID,前 12 位) | +| **CREATED** | 创建时间 | +| **SIZE** | 本地占用空间 | -另外一个需要注意的问题是,`docker image ls` 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。 +### 同一镜像多个标签 -你可以通过 `docker system df` 命令来便捷的查看镜像、容器、数据卷所占用的空间。 +注意上面的 `ubuntu:24.04` 和 `ubuntu:noble` 拥有相同的 IMAGE ID——它们是同一个镜像的不同标签,只占用一份存储空间。 + +--- + +## 理解镜像大小 + +### 本地大小 vs Hub 显示大小 + +| 位置 | 显示大小 | 说明 | +|------|---------|------| +| Docker Hub | 29MB | 压缩后的网络传输大小 | +| docker image ls | 78MB | 本地解压后的实际大小 | + +### 实际磁盘占用 + +由于镜像是分层存储,不同镜像可能共享相同的层: + +``` +ubuntu:24.04 nginx:latest redis:latest + │ │ │ + └───────┬───────┘ │ + ▼ │ + 共享基础层 ◄───────────────────┘ +``` + +因此,`docker image ls` 中各镜像大小之和 > 实际磁盘占用。 + +### 查看实际空间占用 ```bash $ docker system df - -TYPE TOTAL ACTIVE SIZE RECLAIMABLE -Images 24 0 1.992GB 1.992GB (100%) -Containers 1 0 62.82MB 62.82MB (100%) -Local Volumes 9 0 652.2MB 652.2MB (100%) -Build Cache 0B 0B +TYPE TOTAL ACTIVE SIZE RECLAIMABLE +Images 15 3 2.5GB 1.8GB (72%) +Containers 5 2 100MB 80MB (80%) +Local Volumes 8 2 500MB 400MB (80%) +Build Cache 0 0 0B 0B ``` -## 虚悬镜像 +--- -上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 ``。: +## 过滤镜像 + +### 按仓库名过滤 ```bash - 00285df0df87 5 days ago 342 MB +# 列出所有 ubuntu 镜像 +$ docker images ubuntu +REPOSITORY TAG IMAGE ID SIZE +ubuntu 24.04 329ed837d508 78MB +ubuntu noble 329ed837d508 78MB +ubuntu 22.04 a1b2c3d4e5f6 72MB ``` -这个镜像原本是有镜像名和标签的,原来为 `mongo:3.2`,随着官方镜像维护,发布了新版本后,重新 `docker pull mongo:3.2` 时,`mongo:3.2` 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 ``。除了 `docker pull` 可能导致这种情况,`docker build` 也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 `` 的镜像。这类无标签镜像也被称为 **虚悬镜像(dangling image)** ,可以用下面的命令专门显示这类镜像: +### 按仓库名和标签过滤 ```bash -$ docker image ls -f dangling=true -REPOSITORY TAG IMAGE ID CREATED SIZE - 00285df0df87 5 days ago 342 MB +$ docker images ubuntu:24.04 +REPOSITORY TAG IMAGE ID SIZE +ubuntu 24.04 329ed837d508 78MB ``` -一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。 +### 使用过滤器 --filter + +| 过滤条件 | 说明 | 示例 | +|---------|------|------| +| `dangling=true` | 虚悬镜像 | `-f dangling=true` | +| `before=镜像` | 在某镜像之前创建 | `-f before=nginx:latest` | +| `since=镜像` | 在某镜像之后创建 | `-f since=nginx:latest` | +| `label=key=value` | 按 LABEL 过滤 | `-f label=version=1.0` | +| `reference=pattern` | 按名称模式 | `-f reference='*:latest'` | ```bash +# 列出 nginx 之后创建的镜像 +$ docker images -f since=nginx:latest + +# 列出所有带 latest 标签的镜像 +$ docker images -f reference='*:latest' + +# 列出带特定 LABEL 的镜像 +$ docker images -f label=maintainer=example@email.com +``` + +--- + +## 虚悬镜像(Dangling Images) + +### 什么是虚悬镜像 + +仓库名和标签都显示为 `` 的镜像: + +```bash +$ docker images +REPOSITORY TAG IMAGE ID SIZE + 00285df0df87 342MB +``` + +### 产生原因 + +1. **镜像重新构建**:新镜像使用了旧镜像的标签,旧镜像标签被移除 +2. **docker pull 更新**:拉取更新版本时,旧版本失去标签 + +### 处理虚悬镜像 + +```bash +# 列出虚悬镜像 +$ docker images -f dangling=true + +# 删除虚悬镜像 $ docker image prune ``` +--- + ## 中间层镜像 -为了加速镜像构建、重复利用资源,Docker 会利用 **中间层镜像**。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 `docker image ls` 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 `-a` 参数。 +### 查看所有镜像(包含中间层) ```bash -$ docker image ls -a +$ docker images -a ``` -这样会看到很多无标签的镜像,与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。实际上,这些镜像也没必要删除,因为之前说过,相同的层只会存一遍,而这些镜像是别的镜像的依赖,因此并不会因为它们被列出来而多存了一份,无论如何你也会需要它们。只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。 +会显示很多无标签镜像——这些是构建过程中产生的中间层,被其他镜像依赖。 -## 列出部分镜像 +> ⚠️ 不要删除中间层镜像。它们是其他镜像的依赖,删除会导致上层镜像无法使用。删除顶层镜像时会自动清理不再需要的中间层。 -不加任何参数的情况下,`docker image ls` 会列出所有顶层镜像,但是有时候我们只希望列出部分镜像。`docker image ls` 有好几个参数可以帮助做到这个事情。 +--- -根据仓库名列出镜像 +## 格式化输出 + +### 只输出 ID ```bash -$ docker image ls ubuntu -REPOSITORY TAG IMAGE ID CREATED SIZE -ubuntu 18.04 329ed837d508 3 days ago 63.3MB -ubuntu bionic 329ed837d508 3 days ago 63.3MB -``` - -列出特定的某个镜像,也就是说指定仓库名和标签 - -```bash -$ docker image ls ubuntu:24.04 -REPOSITORY TAG IMAGE ID CREATED SIZE -ubuntu 18.04 329ed837d508 3 days ago 63.3MB -``` - -除此以外,`docker image ls` 还支持强大的过滤器参数 `--filter`,或者简写 `-f`。之前我们已经看到了使用过滤器来列出虚悬镜像的用法,它还有更多的用法。比如,我们希望看到在 `mongo:3.2` 之后建立的镜像,可以用下面的命令: - -```bash -$ docker image ls -f since=mongo:3.2 -REPOSITORY TAG IMAGE ID CREATED SIZE -redis latest 5f515359c7f8 5 days ago 183 MB -nginx latest 05a60462f8ba 5 days ago 181 MB -``` - -想查看某个位置之前的镜像也可以,只需要把 `since` 换成 `before` 即可。 - -此外,如果镜像构建时,定义了 `LABEL`,还可以通过 `LABEL` 来过滤。 - -```bash -$ docker image ls -f label=com.example.version=0.1 -... -``` - -## 以特定格式显示 - -默认情况下,`docker image ls` 会输出一个完整的表格,但是我们并非所有时候都会需要这些内容。比如,刚才删除虚悬镜像的时候,我们需要利用 `docker image ls` 把所有的虚悬镜像的 ID 列出来,然后才可以交给 `docker image rm` 命令作为参数来删除指定的这些镜像,这个时候就用到了 `-q` 参数。 - -```bash -$ docker image ls -q +$ docker images -q 5f515359c7f8 05a60462f8ba -fe9198c04d62 -00285df0df87 -329ed837d508 329ed837d508 ``` -`--filter` 配合 `-q` 产生出指定范围的 ID 列表,然后送给另一个 `docker` 命令作为参数,从而针对这组实体成批的进行某种操作的做法在 Docker 命令行使用过程中非常常见,不仅仅是镜像,将来我们会在各个命令中看到这类搭配以完成很强大的功能。因此每次在文档看到过滤器后,可以多注意一下它们的用法。 - -另外一些时候,我们可能只是对表格的结构不满意,希望自己组织列;或者不希望有标题,这样方便其它程序解析结果等,这就用到了 [Go 的模板语法](https://gohugo.io/templates/introduction/)。 - -比如,下面的命令会直接列出镜像结果,并且只包含镜像ID和仓库名: +常用于配合其他命令: ```bash -$ docker image ls --format "{{.ID}}: {{.Repository}}" +# 删除所有镜像 +$ docker rmi $(docker images -q) + +# 删除所有 redis 镜像 +$ docker rmi $(docker images -q redis) +``` + +### 显示完整 ID + +```bash +$ docker images --no-trunc +``` + +### 显示摘要 + +```bash +$ docker images --digests +REPOSITORY TAG DIGEST IMAGE ID +nginx latest sha256:b4f0e0bdeb5... e43d811ce2f4 +``` + +### 自定义格式 + +使用 Go 模板语法自定义输出: + +```bash +# 只显示 ID 和仓库名 +$ docker images --format "{{.ID}}: {{.Repository}}" 5f515359c7f8: redis 05a60462f8ba: nginx -fe9198c04d62: mongo -00285df0df87: -329ed837d508: ubuntu 329ed837d508: ubuntu + +# 表格形式(带标题) +$ docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" +REPOSITORY TAG SIZE +redis latest 183MB +nginx latest 181MB +ubuntu 24.04 78MB ``` -或者打算以表格等距显示,并且有标题行,和默认一样,不过自己定义列: +### 可用模板字段 + +| 字段 | 说明 | +|------|------| +| `.ID` | 镜像 ID | +| `.Repository` | 仓库名 | +| `.Tag` | 标签 | +| `.Digest` | 摘要 | +| `.CreatedSince` | 创建后经过的时间 | +| `.CreatedAt` | 创建时间 | +| `.Size` | 大小 | + +--- + +## 常用命令组合 ```bash -$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" -IMAGE ID REPOSITORY TAG -5f515359c7f8 redis latest -05a60462f8ba nginx latest -fe9198c04d62 mongo 3.2 -00285df0df87 -329ed837d508 ubuntu 18.04 -329ed837d508 ubuntu bionic +# 列出所有镜像及其大小,按大小排序(需要系统 sort 命令) +$ docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | sort -h + +# 查找大于 500MB 的镜像 +$ docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | grep -E "^[0-9]+GB|^[5-9][0-9]{2}MB" + +# 导出镜像列表 +$ docker images --format "{{.Repository}}:{{.Tag}}" > images.txt ``` + +--- + +## 本章小结 + +| 操作 | 命令 | +|------|------| +| 列出所有镜像 | `docker images` | +| 按仓库名过滤 | `docker images nginx` | +| 列出虚悬镜像 | `docker images -f dangling=true` | +| 只输出 ID | `docker images -q` | +| 显示摘要 | `docker images --digests` | +| 自定义格式 | `docker images --format "..."` | +| 查看空间占用 | `docker system df` | + +## 延伸阅读 + +- [获取镜像](pull.md):从 Registry 拉取镜像 +- [删除镜像](rm.md):清理本地镜像 +- [镜像](../basic_concept/image.md):理解镜像概念 diff --git a/image/pull.md b/image/pull.md index 1fdda46..3acf58b 100644 --- a/image/pull.md +++ b/image/pull.md @@ -1,23 +1,59 @@ # 获取镜像 -之前提到过,[Docker Hub](https://hub.docker.com/search?q=&type=image) 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像。 +## docker pull 命令 -从 Docker 镜像仓库获取镜像的命令是 `docker pull`。其命令格式为: +从镜像仓库获取镜像的命令是 `docker pull`: ```bash -$ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] +docker pull [选项] [Registry地址/]仓库名[:标签] ``` -具体的选项可以通过 `docker pull --help` 命令看到,这里我们说一下镜像名称的格式。 +### 镜像名称格式 -* Docker 镜像仓库地址:地址的格式一般是 `<域名/IP>[:端口号]`。默认地址是 Docker Hub(`docker.io`)。 -* 仓库名:如之前所说,这里的仓库名是两段式名称,即 `<用户名>/<软件名>`。对于 Docker Hub,如果不给出用户名,则默认为 `library`,也就是官方镜像。 +``` +docker.io / library / ubuntu : 24.04 +────┬──── ───┬─── ──┬─── ──┬── + │ │ │ │ +Registry地址 用户名 仓库名 标签 + (可省略) (可省略) +``` -比如: +| 组成部分 | 说明 | 默认值 | +|---------|------|--------| +| Registry 地址 | 镜像仓库地址 | `docker.io`(Docker Hub) | +| 用户名 | 镜像所属用户/组织 | `library`(官方镜像) | +| 仓库名 | 镜像名称 | 必须指定 | +| 标签 | 版本标识 | `latest` | + +### 示例 + +```bash +# 完整格式 +$ docker pull docker.io/library/ubuntu:24.04 + +# 省略 Registry(默认 Docker Hub) +$ docker pull library/ubuntu:24.04 + +# 省略 library(官方镜像) +$ docker pull ubuntu:24.04 + +# 省略标签(默认 latest) +$ docker pull ubuntu + +# 拉取第三方镜像 +$ docker pull bitnami/redis:latest + +# 从其他 Registry 拉取 +$ docker pull ghcr.io/username/myapp:v1.0 +``` + +--- + +## 下载过程解析 ```bash $ docker pull ubuntu:24.04 -18.04: Pulling from library/ubuntu +24.04: Pulling from library/ubuntu 92dc2a97ff99: Pull complete be13a9d27eb8: Pull complete c8299583700a: Pull complete @@ -26,43 +62,171 @@ Status: Downloaded newer image for ubuntu:24.04 docker.io/library/ubuntu:24.04 ``` -上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub (`docker.io`)获取镜像。而镜像名称是 `ubuntu:24.04`,因此将会获取官方镜像 `library/ubuntu` 仓库中标签为 `24.04` 的镜像。`docker pull` 命令的输出结果最后一行给出了镜像的完整名称,即: `docker.io/library/ubuntu:24.04`。 +### 输出解读 -从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 `sha256` 的摘要,以确保下载一致性。 +| 输出内容 | 说明 | +|---------|------| +| `Pulling from library/ubuntu` | 正在从官方 ubuntu 仓库拉取 | +| `92dc2a97ff99: Pull complete` | 各层的下载状态(显示层 ID 前 12 位) | +| `Digest: sha256:...` | 镜像内容的唯一摘要 | +| `docker.io/library/ubuntu:24.04` | 镜像的完整名称 | -在使用上面命令的时候,你可能会发现,你所看到的层 ID 以及 `sha256` 的摘要和这里的不一样。这是因为官方镜像是一直在维护的,有任何新的 bug,或者版本更新,都会进行修复再以原来的标签发布,这样可以确保任何使用这个标签的用户可以获得更安全、更稳定的镜像。 +### 分层下载 -*如果从 Docker Hub 下载镜像非常缓慢,可以参照 [镜像加速器](/install/mirror.md) 一节配置加速器。* +从输出可以看到,镜像是**分层下载**的: -## 运行 - -有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。以上面的 `ubuntu:24.04` 为例,如果我们打算启动里面的 `bash` 并且进行交互式操作的话,可以执行下面的命令。 - -```bash -$ docker run -it --rm ubuntu:24.04 bash - -root@e7009c6ce357:/# cat /etc/os-release -NAME="Ubuntu" -VERSION="24.04 LTS (Noble Numbat)" -ID=ubuntu -ID_LIKE=debian -PRETTY_NAME="Ubuntu 24.04 LTS" -VERSION_ID="24.04" -HOME_URL="https://www.ubuntu.com/" -SUPPORT_URL="https://help.ubuntu.com/" -BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" -PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" -VERSION_CODENAME=noble -UBUNTU_CODENAME=noble +``` +┌─────────────────────────────────────────────────────────────┐ +│ ubuntu:24.04 镜像 │ +├─────────────────────────────────────────────────────────────┤ +│ 第3层 c8299583700a ───────► 已存在,跳过下载 │ +├─────────────────────────────────────────────────────────────┤ +│ 第2层 be13a9d27eb8 ───────► 下载中... 完成 │ +├─────────────────────────────────────────────────────────────┤ +│ 第1层 92dc2a97ff99 ───────► 下载中... 完成 │ +└─────────────────────────────────────────────────────────────┘ ``` -`docker run` 就是运行容器的命令,具体格式我们会在 [容器](../container) 一节进行详细讲解,我们这里简要的说明一下上面用到的参数。 +如果本地已有相同的层,Docker 会跳过下载,节省带宽和时间。 -* `-it`:这是两个参数,一个是 `-i`:交互式操作,一个是 `-t` 终端。我们这里打算进入 `bash` 执行一些命令并查看返回结果,因此我们需要交互式终端。 -* `--rm`:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 `docker rm`。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 `--rm` 可以避免浪费空间。 -* `ubuntu:24.04`:这是指用 `ubuntu:24.04` 镜像为基础来启动容器。 -* `bash`:放在镜像名后的是 **命令**,这里我们希望有个交互式 Shell,因此用的是 `bash`。 +--- -进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里,我们执行了 `cat /etc/os-release`,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是 `Ubuntu 24.04 LTS` 系统。 +## 常用选项 -最后我们通过 `exit` 退出了这个容器。 +| 选项 | 说明 | 示例 | +|------|------|------| +| `--all-tags, -a` | 拉取所有标签 | `docker pull -a ubuntu` | +| `--platform` | 指定平台架构 | `docker pull --platform linux/arm64 nginx` | +| `--quiet, -q` | 静默模式 | `docker pull -q nginx` | + +### 指定平台 + +在 Apple Silicon Mac 上拉取 x86 镜像: + +```bash +$ docker pull --platform linux/amd64 nginx +``` + +--- + +## 拉取后运行 + +拉取镜像后,可以基于它启动容器: + +```bash +# 拉取镜像 +$ docker pull ubuntu:24.04 + +# 运行容器 +$ docker run -it --rm ubuntu:24.04 bash +root@e7009c6ce357:/# cat /etc/os-release +PRETTY_NAME="Ubuntu 24.04 LTS" +... +root@e7009c6ce357:/# exit +``` + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `-it` | 交互式终端模式 | +| `--rm` | 退出后自动删除容器 | +| `bash` | 启动命令 | + +> 💡 `docker run` 在需要时会自动 `pull` 镜像,因此通常不需要单独执行 `docker pull`。 + +--- + +## 镜像加速 + +从 Docker Hub 下载可能较慢。可以配置镜像加速器: + +```json +// /etc/docker/daemon.json (Linux) +// ~/.docker/daemon.json (Docker Desktop) +{ + "registry-mirrors": [ + "https://your-accelerator-url" + ] +} +``` + +配置后重启 Docker: + +```bash +$ sudo systemctl restart docker # Linux +# 或在 Docker Desktop 中重启 +``` + +详见 [镜像加速器](../install/mirror.md) 章节。 + +--- + +## 验证镜像完整性 + +### 查看镜像摘要 + +```bash +$ docker images --digests ubuntu +REPOSITORY TAG DIGEST IMAGE ID +ubuntu 24.04 sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26 ca2b0f26964c +``` + +### 使用摘要拉取 + +用摘要拉取可确保获取完全相同的镜像: + +```bash +$ docker pull ubuntu@sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26 +``` + +> 笔者建议:生产环境使用摘要而非标签,因为标签可能被覆盖,摘要则是不可变的。 + +--- + +## 常见问题 + +### Q: 下载速度很慢 + +1. 配置镜像加速器 +2. 检查网络连接 +3. 尝试拉取更小的镜像版本(如 `alpine` 变体) + +### Q: 提示镜像不存在 + +```bash +Error: pull access denied, repository does not exist +``` + +可能原因: +- 镜像名拼写错误 +- 私有镜像未登录(需要 `docker login`) +- 镜像确实不存在 + +### Q: 磁盘空间不足 + +```bash +# 清理未使用的镜像 +$ docker image prune + +# 清理所有未使用资源 +$ docker system prune +``` + +--- + +## 本章小结 + +| 操作 | 命令 | +|------|------| +| 拉取镜像 | `docker pull 镜像名:标签` | +| 拉取所有标签 | `docker pull -a 镜像名` | +| 指定平台 | `docker pull --platform linux/amd64 镜像名` | +| 用摘要拉取 | `docker pull 镜像名@sha256:...` | + +## 延伸阅读 + +- [列出镜像](list.md):查看本地镜像 +- [删除镜像](rm.md):清理本地镜像 +- [镜像加速器](../install/mirror.md):加速镜像下载 +- [Docker Hub](../repository/dockerhub.md):官方镜像仓库 diff --git a/image/rm.md b/image/rm.md index 026a51c..f909fce 100644 --- a/image/rm.md +++ b/image/rm.md @@ -1,87 +1,255 @@ # 删除本地镜像 -如果要删除本地的镜像,可以使用 `docker image rm` 命令,其格式为: +## 基本用法 + +使用 `docker image rm` 删除本地镜像: ```bash $ docker image rm [选项] <镜像1> [<镜像2> ...] ``` -## 用 ID、镜像名、摘要删除镜像 +> 💡 `docker rmi` 是 `docker image rm` 的简写,两者等效。 -其中,`<镜像>` 可以是 `镜像短 ID`、`镜像长 ID`、`镜像名` 或者 `镜像摘要`。 +--- -比如我们有这么一些镜像: +## 镜像标识方式 + +删除镜像时,可以使用多种方式指定镜像: + +| 方式 | 说明 | 示例 | +|------|------|------| +| **短 ID** | ID 的前几位(通常 3-4 位) | `docker rmi 501` | +| **长 ID** | 完整的镜像 ID | `docker rmi 501ad78535f0...` | +| **镜像名:标签** | 仓库名和标签 | `docker rmi redis:alpine` | +| **镜像摘要** | 精确的内容摘要 | `docker rmi nginx@sha256:...` | + +### 使用短 ID 删除 ```bash $ docker image ls -REPOSITORY TAG IMAGE ID CREATED SIZE -centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB -redis alpine 501ad78535f0 3 weeks ago 21.03 MB -docker latest cf693ec9b5c7 3 weeks ago 105.1 MB -nginx latest e43d811ce2f4 5 weeks ago 181.5 MB +REPOSITORY TAG IMAGE ID SIZE +redis alpine 501ad78535f0 30MB +nginx latest e43d811ce2f4 142MB + +# 只需输入足够区分的前几位 +$ docker rmi 501 +Untagged: redis:alpine +Deleted: sha256:501ad78535f0... ``` -我们可以用镜像的完整 ID,也称为 `长 ID`,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 `短 ID` 来删除镜像。`docker image ls` 默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。 - -比如这里,如果我们要删除 `redis:alpine` 镜像,可以执行: +### 使用镜像名删除 ```bash -$ docker image rm 501 +$ docker rmi redis:alpine +Untagged: redis:alpine +Deleted: sha256:501ad78535f0... +``` + +### 使用摘要删除 + +摘要删除最精确,适用于 CI/CD 场景: + +```bash +# 查看镜像摘要 +$ docker images --digests +REPOSITORY TAG DIGEST IMAGE ID +nginx latest sha256:b4f0e0bdeb5... e43d811ce2f4 + +# 使用摘要删除 +$ docker rmi nginx@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 +``` + +--- + +## 理解输出信息 + +删除镜像时会看到两类信息:**Untagged** 和 **Deleted** + +```bash +$ docker rmi redis:alpine Untagged: redis:alpine Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7 Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23 -Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa -Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3 -Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7 ``` -我们也可以用`镜像名`,也就是 `<仓库名>:<标签>`,来删除镜像。 +### Untagged vs Deleted + +| 操作 | 含义 | +|------|------| +| **Untagged** | 移除镜像的标签 | +| **Deleted** | 删除镜像的存储层 | + +### 删除流程 + +``` +docker rmi redis:alpine + │ + ▼ +┌───────────────────────────────────────────────────────────────┐ +│ 1. Untag:移除 redis:alpine 标签 │ +│ ↓ │ +│ 2. 检查是否还有其他标签指向这个镜像 │ +│ ├── 有 → 只 Untag,不删除 │ +│ └── 无 → │ +│ ↓ │ +│ 3. 检查是否有容器依赖 │ +│ ├── 有 → 报错,无法删除 │ +│ └── 无 → │ +│ ↓ │ +│ 4. 从上到下逐层删除,检查每层是否被其他镜像使用 │ +│ ├── 被使用 → 保留 │ +│ └── 未使用 → Deleted │ +└───────────────────────────────────────────────────────────────┘ +``` + +--- + +## 批量删除 + +### 删除所有虚悬镜像 + +虚悬镜像(dangling):没有标签的镜像,通常是旧版本被新版本覆盖后产生的 ```bash -$ docker image rm centos -Untagged: centos:latest -Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c -Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a -Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38 +# 查看虚悬镜像 +$ docker images -f dangling=true + +# 删除虚悬镜像 +$ docker image prune + +# 不提示确认 +$ docker image prune -f ``` -当然,更精确的是使用 `镜像摘要` 删除镜像。 +### 删除所有未使用的镜像 ```bash -$ docker image ls --digests -REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE -node slim sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 6e0c4c8e3913 3 weeks ago 214 MB +# 删除所有没有被容器使用的镜像 +$ docker image prune -a -$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 -Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 +# 保留最近 24 小时的 +$ docker image prune -a --filter "until=24h" ``` -## Untagged 和 Deleted - -如果观察上面这几个命令的运行输出信息的话,你会注意到删除行为分为两类,一类是 `Untagged`,另一类是 `Deleted`。我们之前介绍过,镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。 - -因此当我们使用上面命令删除镜像的时候,实际上是在要求删除某个标签的镜像。所以首先需要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的 `Untagged` 的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 `Delete` 行为就不会发生。所以并非所有的 `docker image rm` 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。 - -当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。这就是为什么,有时候会奇怪,为什么明明没有别的标签指向这个镜像,但是它还是存在的原因,也是为什么有时候会发现所删除的层数和自己 `docker pull` 看到的层数不一样的原因。 - -除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。之前讲过,容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。因此该镜像如果被这个容器所依赖的,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。 - -## 用 docker image ls 命令来配合 - -像其它可以承接多个实体的命令一样,可以使用 `docker image ls -q` 来配合使用 `docker image rm`,这样可以成批的删除希望删除的镜像。我们在“镜像列表”章节介绍过很多过滤镜像列表的方式都可以拿过来使用。 - -比如,我们需要删除所有仓库名为 `redis` 的镜像: +### 按条件删除 ```bash -$ docker image rm $(docker image ls -q redis) +# 删除所有 redis 镜像 +$ docker rmi $(docker images -q redis) + +# 删除 mongo:3.2 之前的所有镜像 +$ docker rmi $(docker images -q -f before=mongo:3.2) + +# 删除某个时间之前的镜像 +$ docker image prune -a --filter "until=168h" # 7天前 ``` -或者删除所有在 `mongo:3.2` 之前的镜像: +--- + +## 删除失败的常见原因 + +### 原因一:有容器依赖 ```bash -$ docker image rm $(docker image ls -q -f before=mongo:3.2) +$ docker rmi nginx +Error: conflict: unable to remove repository reference "nginx" +(must force) - container abc123 is using its referenced image ``` -充分利用你的想象力和 Linux 命令行的强大,你可以完成很多非常赞的功能。 +**解决方案**: + +```bash +# 方案1:先删除依赖的容器 +$ docker rm abc123 +$ docker rmi nginx + +# 方案2:强制删除镜像(容器仍可运行,但无法再创建新容器) +$ docker rmi -f nginx +``` + +### 原因二:多个标签指向同一镜像 + +```bash +$ docker images +REPOSITORY TAG IMAGE ID +ubuntu 24.04 ca2b0f26964c +ubuntu latest ca2b0f26964c # 同一个镜像 + +$ docker rmi ubuntu:24.04 +Untagged: ubuntu:24.04 +# 只是移除标签,镜像仍存在(因为还有 ubuntu:latest 指向它) +``` + +### 原因三:被其他镜像依赖(中间层) + +```bash +$ docker rmi some_base_image +Error: image has dependent child images +``` + +中间层镜像被其他镜像依赖,无法删除。需要先删除依赖它的镜像。 + +--- + +## 常用过滤条件 + +| 过滤条件 | 说明 | 示例 | +|---------|------|------| +| `dangling=true` | 虚悬镜像 | `-f dangling=true` | +| `before=镜像` | 在某镜像之前 | `-f before=mongo:3.2` | +| `since=镜像` | 在某镜像之后 | `-f since=mongo:3.2` | +| `label=key=value` | 按标签过滤 | `-f label=version=1.0` | +| `reference=pattern` | 按名称模式 | `-f reference='*:latest'` | + +--- + +## 清理策略 + +### 开发环境 + +```bash +# 定期清理虚悬镜像 +$ docker image prune -f + +# 一键清理所有未使用资源 +$ docker system prune -a +``` + +### CI/CD 环境 + +```bash +# 只保留最近使用的镜像 +$ docker image prune -a --filter "until=72h" -f +``` + +### 查看空间占用 + +```bash +$ docker system df +TYPE TOTAL ACTIVE SIZE RECLAIMABLE +Images 15 3 2.5GB 1.8GB (72%) +Containers 5 2 100MB 80MB (80%) +Local Volumes 8 2 500MB 400MB (80%) +Build Cache 0 0 0B 0B +``` + +--- + +## 本章小结 + +| 操作 | 命令 | +|------|------| +| 删除指定镜像 | `docker rmi 镜像名:标签` | +| 强制删除 | `docker rmi -f 镜像名` | +| 删除虚悬镜像 | `docker image prune` | +| 删除未使用镜像 | `docker image prune -a` | +| 批量删除 | `docker rmi $(docker images -q -f ...)` | +| 查看空间占用 | `docker system df` | + +## 延伸阅读 + +- [列出镜像](list.md):查看和过滤镜像 +- [删除容器](../container/rm.md):清理容器 +- [数据卷](../data_management/volume.md):清理数据卷 diff --git a/introduction/what.md b/introduction/what.md index f771960..baa2ac4 100644 --- a/introduction/what.md +++ b/introduction/what.md @@ -1,21 +1,124 @@ # 什么是 Docker -**Docker** 最初是 `dotCloud` 公司创始人 [Solomon Hykes](https://github.com/shykes) 在法国期间发起的一个公司内部项目,它是基于 `dotCloud` 公司多年云服务技术的一次革新,并于 [2013 年 3 月以 Apache 2.0 授权协议开源](https://en.wikipedia.org/wiki/Docker_\(software\)),主要项目代码在 [GitHub](https://github.com/moby/moby) 上进行维护。`Docker` 项目后来还加入了 Linux 基金会,并成立推动 [开放容器联盟(OCI)](https://opencontainers.org/)。 +## 一句话理解 Docker -**Docker** 自开源后受到广泛的关注和讨论,至今其 [GitHub 项目](https://github.com/moby/moby) 已经超过 6.8 万个星标和一万多个 `fork`。甚至由于 `Docker` 项目的火爆,在 `2013` 年底,[dotCloud 公司决定改名为 Docker](https://www.docker.com/blog/dotcloud-is-becoming-docker-inc/)。`Docker` 最初是在 `Ubuntu 12.04` 上开发实现的;`Red Hat` 则从 `RHEL 6.5` 开始对 `Docker` 进行支持;`Google` 也在其 `PaaS` 产品中广泛应用 `Docker`。 +> **Docker 是一种轻量级的虚拟化技术,它让应用程序及其依赖环境可以被打包成一个标准化的单元,在任何地方都能一致地运行。** -**Docker** 使用 `Google` 公司推出的 [Go 语言](https://golang.google.cn/) 进行开发实现,基于 `Linux` 内核的 [cgroup](https://zh.wikipedia.org/wiki/Cgroups),[namespace](https://en.wikipedia.org/wiki/Linux_namespaces),以及 [OverlayFS](https://docs.docker.com/storage/storagedriver/overlayfs-driver/) 类的 [Union FS](https://en.wikipedia.org/wiki/Union_mount) 等技术,对进程进行封装隔离,属于 [操作系统层面的虚拟化技术](https://en.wikipedia.org/wiki/Operating-system-level_virtualization)。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 [LXC](https://linuxcontainers.org/lxc/introduction/),从 `0.7` 版本以后开始去除 `LXC`,转而使用自行开发的 [libcontainer](https://github.com/docker/libcontainer),从 `1.11` 版本开始,则进一步演进为使用 [runC](https://github.com/opencontainers/runc) 和 [containerd](https://github.com/containerd/containerd)。 +如果用一个生活中的类比:**Docker 之于软件,就像集装箱之于货物**。 -![Docker 架构](./_images/docker-on-linux.png) +在集装箱发明之前,货物的运输是一件麻烦的事情——不同的货物需要不同的包装、不同的装卸方式,换一种运输工具就要重新装卸。集装箱的出现改变了这一切:无论里面装的是什么,集装箱的外形是标准的,可以用同样的方式装卸、堆放和运输。 -> `runc` 是一个 Linux 命令行工具,用于根据 [OCI容器运行时规范](https://github.com/opencontainers/runtime-spec) 创建和运行容器。 +Docker 做的事情类似:无论你的应用是用 Python、Java、Node.js 还是其他语言写的,无论它需要什么样的依赖库和环境,一旦被打包成 Docker 镜像,就可以用同样的方式在任何支持 Docker 的机器上运行。 -> `containerd` 是一个守护程序,它管理容器生命周期,提供了在一个节点上执行容器和管理镜像的最小功能集。 +## Docker 的核心价值 -**Docker** 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 `Docker` 技术比虚拟机技术更为轻便、快捷。 +笔者认为,Docker 解决的是软件开发中最古老的问题之一:**"在我机器上明明能跑啊!"** -下面的图片比较了 **Docker** 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。 +``` +开发环境 生产环境 +┌─────────────────┐ ┌─────────────────┐ +│ Python 3.9 │ ≠ │ Python 3.7 │ +│ Ubuntu 22.04 │ │ CentOS 7 │ +│ 特定版本的库 │ │ 不同版本的库 │ +└─────────────────┘ └─────────────────┘ + ↓ ↓ + 运行正常 运行失败! +``` + +有了 Docker: + +``` +开发环境 生产环境 +┌─────────────────┐ ┌─────────────────┐ +│ Docker 镜像 │ = │ 同一个镜像 │ +│ (包含所有依赖) │ │ (完全一致) │ +└─────────────────┘ └─────────────────┘ + ↓ ↓ + 运行正常 运行正常! +``` + +## Docker vs 虚拟机 + +很多人第一次接触 Docker 时会问:**"这不就是虚拟机吗?"** + +答案是:**不是,而且差别很大。** + +### 传统虚拟机 + +传统虚拟机技术是虚拟出一套完整的硬件,在其上运行一个完整的操作系统,再在该系统上运行应用: ![传统虚拟化](../.gitbook/assets/virtualization.png) +### Docker 容器 + +而 Docker 容器内的应用直接运行于宿主的内核,容器内没有自己的内核,也没有进行硬件虚拟: + ![Docker](../.gitbook/assets/docker.png) + +### 关键区别 + +| 特性 | Docker 容器 | 传统虚拟机 | +|------|-------------|------------| +| **启动速度** | 秒级 | 分钟级 | +| **资源占用** | MB 级别 | GB 级别 | +| **性能** | 接近原生 | 有明显损耗 | +| **隔离级别** | 进程级隔离 | 完全隔离 | +| **单机数量** | 可运行上千个 | 通常几十个 | + +> 笔者经常用这个类比来解释:虚拟机像是每个应用都住在一栋独立的房子里(有自己的地基、水电系统),而容器像是大家住在同一栋公寓楼里的不同房间(共享地基和水电系统,但各自独立)。 + +## Docker 的技术基础 + +Docker 使用 [Go 语言](https://golang.google.cn/) 开发,基于 Linux 内核的以下技术: + +- **[Namespace](https://en.wikipedia.org/wiki/Linux_namespaces)**:实现资源隔离(进程、网络、文件系统等) +- **[Cgroups](https://zh.wikipedia.org/wiki/Cgroups)**:实现资源限制(CPU、内存、I/O 等) +- **[Union FS](https://en.wikipedia.org/wiki/Union_mount)**:实现分层存储(如 OverlayFS) + +> 如果你对这些底层技术感兴趣,可以阅读本书的[底层实现](../underly/README.md)章节。 + +### Docker 架构演进 + +Docker 的底层实现经历了多次演进: + +``` +2013 2014 2015 现在 + │ │ │ │ + ▼ ▼ ▼ ▼ +LXC ──→ libcontainer ──→ runC ──→ containerd + runC + │ + └── OCI 标准化 +``` + +- **LXC**(2013):Docker 最初基于 Linux Containers +- **libcontainer**(2014,v0.7):Docker 自研的容器运行时 +- **runC**(2015,v1.11):捐献给 OCI 的标准容器运行时 +- **containerd**:高级容器运行时,管理容器生命周期 + +![Docker 架构](./_images/docker-on-linux.png) + +> `runc` 是一个 Linux 命令行工具,用于根据 [OCI 容器运行时规范](https://github.com/opencontainers/runtime-spec) 创建和运行容器。 + +> `containerd` 是一个守护程序,它管理容器生命周期,提供了在一个节点上执行容器和管理镜像的最小功能集。 + +## Docker 的历史与生态 + +**Docker** 最初是 `dotCloud` 公司创始人 [Solomon Hykes](https://github.com/shykes) 在法国期间发起的一个公司内部项目,于 [2013 年 3 月以 Apache 2.0 授权协议开源](https://en.wikipedia.org/wiki/Docker_(software))。 + +Docker 的发展历程: + +- **2013 年 3 月**:开源发布 +- **2013 年底**:dotCloud 公司改名为 Docker, Inc. +- **2015 年**:成立 [开放容器联盟(OCI)](https://opencontainers.org/),推动容器标准化 +- **至今**:[GitHub 项目](https://github.com/moby/moby) 超过 6.8 万星标 + +Docker 的成功推动了整个容器生态的发展,催生了 Kubernetes、Podman 等众多相关项目。笔者认为,Docker 最大的贡献不仅是技术本身,更是它**让容器技术从系统管理员的工具变成了每个开发者都能使用的标准工具**。 + +## 本章小结 + +- Docker 是一种轻量级虚拟化技术,核心价值是**环境一致性** +- 与虚拟机相比,Docker 更轻量、更快速、资源利用率更高 +- Docker 基于 Linux 内核的 Namespace、Cgroups 和 Union FS 技术 +- Docker 推动了容器技术的标准化(OCI)和生态发展 + +接下来,让我们了解[为什么要使用 Docker](why.md)。 diff --git a/introduction/why.md b/introduction/why.md index df6ce2c..9a9560c 100644 --- a/introduction/why.md +++ b/introduction/why.md @@ -1,40 +1,208 @@ # 为什么要使用 Docker? -作为一种新兴的虚拟化方式,`Docker` 跟传统的虚拟化方式相比具有众多的优势。 +在回答"为什么用 Docker"之前,笔者想先问一个问题:**你有没有经历过这些场景?** -## 更高效的利用系统资源 +## 没有 Docker 的世界 -由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销,`Docker` 对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。 +### 场景一:"在我电脑上明明能跑" -## 更快速的启动时间 +``` +周五下午 5:00 +├── 开发者:代码写完了,本地测试通过,提交!🎉 +├── 周一早上 9:00 +│ └── 测试:"这个功能在测试环境跑不起来" +└── 开发者:" 不可能,在我电脑上明明能跑啊……" +``` -传统的虚拟机技术启动应用服务往往需要数分钟,而 `Docker` 容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。 +笔者统计过,这个问题通常由以下原因导致: +- Python/Node/Java 版本不一致 +- 依赖库版本不一致 +- 操作系统配置不一致 +- 某些环境变量没有设置 +- "哦,忘了说我本地装了个 XXX" -## 一致的运行环境 +### 场景二:环境配置的噩梦 -开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 `Docker` 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 *「这段代码在我机器上没问题啊」* 这类问题。 +``` +新同事入职 +├── Day 1:领电脑,配环境 +├── Day 2:继续配环境,遇到问题 +├── Day 3:换种方法配环境 +├── Day 4:问老同事怎么配的,他也忘了 +└── Day 5:终于能跑起来了!但不知道为什么…… +``` -## 持续交付和部署 +### 场景三:服务器迁移的恐惧 -对开发和运维([DevOps](https://zh.wikipedia.org/wiki/DevOps))人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。 +``` +运维:"我们需要把服务迁移到新服务器" +开发:"旧服务器上的配置文档在哪?" +运维:"当时是一个已经离职的同事配的……" +所有人:😱 +``` -使用 `Docker` 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 [Dockerfile](../image/dockerfile/) 来进行镜像构建,并结合 [持续集成(Continuous Integration)](https://en.wikipedia.org/wiki/Continuous_integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 [持续部署(Continuous Delivery/Deployment)](https://en.wikipedia.org/wiki/Continuous_delivery) 系统进行自动部署。 +## Docker 如何解决这些问题 -而且使用 [`Dockerfile`](../image/build.md) 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。 +### 核心理念:一次构建,到处运行 -## 更轻松的迁移 +``` +开发环境 测试环境 生产环境 + │ │ │ + ▼ ▼ ▼ +┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Docker │ = │ Docker │ = │ Docker │ +│ 镜像 │ │ 镜像 │ │ 镜像 │ +└─────────┘ └─────────┘ └─────────┘ + ↓ ↓ ↓ + 完全一致 完全一致 完全一致 +``` -由于 `Docker` 确保了执行环境的一致性,使得应用的迁移更加容易。`Docker` 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。 +## Docker 的核心优势 -## 更轻松的维护和扩展 +### 1. 环境一致性 -`Docker` 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,`Docker` 团队同各个开源项目团队一起维护了一大批高质量的 [官方镜像](https://hub.docker.com/search/?type=image&image_filter=official),既可以直接在生产环境使用,又可以作为基础进一步定制,大大的降低了应用服务的镜像制作成本。 +Docker 镜像包含了应用运行所需的**一切**:代码、运行时、系统工具、库、配置。这意味着: -## 对比传统虚拟机总结 +- ✅ 开发环境和生产环境完全一致 +- ✅ 不会再有"在我机器上能跑"的问题 +- ✅ 新人入职,一条命令就能启动开发环境 -| 特性 | 容器 | 虚拟机 | -| :-------- | :-------- | :---------- | -| 启动 | 秒级 | 分钟级 | -| 硬盘使用 | 一般为 `MB` | 一般为 `GB` | -| 性能 | 接近原生 | 弱于 | -| 系统支持量 | 单机支持上千个容器 | 一般几十个 | +```bash +# 新同事入职第一天 +$ git clone https://github.com/company/project.git +$ docker compose up +# 完整的开发环境就准备好了 +``` + +### 2. 秒级启动 + +传统虚拟机启动需要几分钟(引导操作系统),而 Docker 容器启动通常只需要**几秒甚至几百毫秒**。 + +笔者实测数据: + +| 启动内容 | 虚拟机 | Docker 容器 | +|---------|--------|-------------| +| 空系统 | ~60 秒 | ~0.5 秒 | +| MySQL | ~90 秒 | ~3 秒 | +| 完整 Web 应用 | ~120 秒 | ~5 秒 | + +这个差异对以下场景尤为重要: +- **CI/CD 流水线**:每次构建节省几分钟,一天累积下来就是几小时 +- **弹性扩容**:流量高峰时能快速启动更多实例 +- **开发体验**:快速重启服务进行调试 + +### 3. 资源效率 + +Docker 容器共享宿主机内核,无需为每个应用运行完整的操作系统。 + +``` +传统虚拟机方案: +┌────────────────────────────────────────────────┐ +│ 物理服务器 (64GB 内存) │ +├──────────────┬───────────────┬─────────────────┤ +│ VM1 │ VM2 │ VM3 │ +│ 8GB 内存 │ 8GB 内存 │ 8GB 内存 │ +│ (含 OS 2GB) │ (含 OS 2GB) │ (含 OS 2GB) │ +│ 应用 1 │ 应用 2 │ 应用 3 │ +└──────────────┴───────────────┴─────────────────┘ +实际可用于应用:3 × 6GB = 18GB ❌ + +Docker 方案: +┌────────────────────────────────────────────────┐ +│ 物理服务器 (64GB 内存) │ +│ 宿主机 OS + Docker (约 4GB) │ +├──────────────┬───────────────┬─────────────────┤ +│ 容器 1 │ 容器 2 │ 容器 3 │ +│ 应用 1 │ 应用 2 │ 应用 3 │ +│ (按需分配) │ (按需分配) │ (按需分配) │ +└──────────────┴───────────────┴─────────────────┘ +实际可用于应用:约 60GB ✅ +``` + +### 4. 持续交付和部署 + +Docker 完美契合 DevOps 的工作流程: + +``` +代码提交 ──→ 自动构建镜像 ──→ 自动测试 ──→ 自动部署 + │ │ │ │ + ▼ ▼ ▼ ▼ + Git docker 容器内 容器滚动 + push build 运行测试 更新 +``` + +使用 [Dockerfile](../image/build.md) 定义镜像构建过程,使得: +- 构建过程**可重复、可追溯** +- 任何人都能从代码重建完全相同的镜像 +- 配合 [GitHub Actions](../cases/ci/actions/README.md) 等 CI 系统实现自动化 + +### 5. 轻松迁移 + +Docker 可以在几乎任何平台上运行: +- ✅ 本地开发机(macOS、Windows、Linux) +- ✅ 公有云(AWS、Azure、GCP、阿里云、腾讯云) +- ✅ 私有云和自建数据中心 +- ✅ 边缘设备和 IoT + +**同一个镜像,在任何地方运行结果都一致。** 这让应用迁移变得前所未有的简单。 + +### 6. 微服务架构的基石 + +现代微服务架构几乎都依赖容器技术。Docker 让你可以: + +- **隔离服务**:每个服务运行在独立容器中,互不干扰 +- **独立扩展**:哪个服务负载高,就单独扩展哪个 +- **独立部署**:更新一个服务不影响其他服务 +- **技术多样**:不同服务可以用不同语言和框架 + +``` +┌───────────────────────────────────────────────────┐ +│ 微服务架构示例 │ +├─────────────┬─────────────┬───────────────────────┤ +│ 前端容器 │ API 容器 │ Worker 容器 │ +│ (Node.js) │ (Python) │ (Go) │ +├─────────────┴─────────────┴───────────────────────┤ +│ Redis 容器 │ +├───────────────────────────────────────────────────┤ +│ PostgreSQL 容器 │ +└───────────────────────────────────────────────────┘ +``` + +## Docker 不适合的场景 + +笔者认为,技术选型要客观。Docker 并非银弹,以下场景可能不太适合: + +### 1. 需要完全隔离的场景 + +容器共享宿主机内核,隔离性不如虚拟机。如果你需要运行不受信任的代码,虚拟机可能更安全。 + +### 2. 需要特殊内核的场景 + +容器使用宿主机内核。如果应用需要特定版本的内核或内核模块,可能需要虚拟机。 + +### 3. Windows 原生应用 + +虽然 Docker 支持 Windows 容器,但生态不如 Linux 容器成熟。传统 Windows 应用的容器化仍有挑战。 + +### 4. 桌面应用 + +Docker 主要面向服务端应用。桌面 GUI 应用的容器化虽然可行,但通常得不偿失。 + +## 与传统虚拟机的对比总结 + +| 特性 | Docker 容器 | 传统虚拟机 | +|:------|:-----------|:-----------| +| 启动速度 | 秒级 | 分钟级 | +| 磁盘占用 | MB 级别 | GB 级别 | +| 性能 | 接近原生 | 有 5-20% 损耗 | +| 单机支持量 | 上千个容器 | 几十个虚拟机 | +| 隔离性 | 进程级别 | 完全隔离 | +| 最佳场景 | 微服务、CI/CD、开发环境 | 多租户、高安全需求 | + +## 本章小结 + +Docker 的核心价值可以用一句话概括:**让应用的开发、测试、部署保持一致,同时极大提高资源利用效率。** + +笔者认为,对于现代软件开发者来说,Docker 已经不是"要不要学"的问题,而是**必备技能**。无论你是前端、后端、运维还是全栈开发者,掌握 Docker 都能让你的工作更高效。 + +接下来,让我们学习 Docker 的[基本概念](../basic_concept/README.md)。 diff --git a/network/README.md b/network/README.md index b081d5a..d13c68f 100644 --- a/network/README.md +++ b/network/README.md @@ -1,48 +1,293 @@ # 网络配置 -当 Docker 启动时,会自动在主机上创建一个 `docker0` 虚拟网桥,实际上是 Linux 的一个 bridge,可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。 +## Docker 网络概述 -同时,Docker 随机分配一个本地未占用的私有网段(在 [RFC1918](https://datatracker.ietf.org/doc/html/rfc1918) 中定义)中的一个地址给 `docker0` 接口。比如典型的 `172.17.42.1`,掩码为 `255.255.0.0`。此后启动的容器内的网口也会自动分配一个同一网段(`172.17.0.0/16`)的地址。 +Docker 容器需要网络来: +- 与外部世界通信(访问互联网、被外部访问) +- 容器之间相互通信 +- 与宿主机通信 -当创建一个 Docker 容器的时候,同时会创建了一对 `veth pair` 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包)。这对接口一端在容器内,即 `eth0`;另一端在本地并被挂载到 `docker0` 网桥,名称以 `veth` 开头(例如 `vethAQI2QT`)。通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。Docker 就创建了在主机和所有容器之间一个虚拟共享网络。 +Docker 在安装时会自动配置网络基础设施,大多数情况下开箱即用。 +## 默认网络架构 +Docker 启动时自动创建以下网络组件: -## 用户自定义网络 - -虽然默认的 `bridge` 网络可以满足大部分需求,但为了更好地隔离容器、或满足特定的网络需求,我们推荐使用用户自定义网络。 - -用户可以创建 `bridge`、`overlay` 或 `macvlan` 等不同类型的自定义网络。 - -### 创建一个自定义 bridge 网络 - -```bash -$ docker network create my-net +``` +┌────────────────────────────────────────────────────────────────┐ +│ 宿主机 │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ docker0 网桥 │ │ +│ │ (172.17.0.1/16) │ │ +│ │ ┌────────────┬────────────┬────────────┐ │ │ +│ │ │ │ │ │ │ │ +│ └────┼────────────┼────────────┼────────────┼──────────┘ │ +│ │ │ │ │ │ +│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │ +│ │ veth │ │ veth │ │ veth │ │ veth │ │ +│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ +│ │ │ │ │ │ +│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │ +│ │ 容器 A │ │ 容器 B │ │ 容器 C │ │ 容器 D │ │ +│ │.17.0.2 │ │.17.0.3 │ │.17.0.4 │ │.17.0.5 │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +│ eth0 ◄──────────────────────────────────────────► 外部网络 │ +│ (192.168.1.100) │ +└────────────────────────────────────────────────────────────────┘ ``` -### 连接容器到自定义网络 +### 核心组件 -在启动容器时,可以使用 `--network` 选项来指定网络。 +| 组件 | 说明 | +|------|------| +| **docker0** | 虚拟网桥,充当交换机角色 | +| **veth pair** | 虚拟网卡对,一端在容器内,一端连接网桥 | +| **容器 eth0** | 容器内的网卡 | +| **IP 地址** | 自动从 172.17.0.0/16 网段分配 | -```bash -$ docker run -it --rm --name busybox1 --network my-net busybox sh -$ docker run -it --rm --name busybox2 --network my-net busybox sh +### 数据流向 + +``` +容器 A (172.17.0.2) → docker0 → 容器 B (172.17.0.3) (容器间通信) +容器 A (172.17.0.2) → docker0 → eth0 → 互联网 (访问外网) +外部请求 → eth0 → docker0 → 容器 A (被外部访问,需端口映射) ``` -在 `busybox1` 的终端中,可以 `ping` 通 `busybox2`。 +--- + +## Docker 网络类型 + +查看默认网络: ```bash -/ # ping busybox2 -PING busybox2 (172.19.0.3): 56 data bytes -64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.083 ms +$ docker network ls +NETWORK ID NAME DRIVER SCOPE +abc123... bridge bridge local +def456... host host local +ghi789... none null local ``` -### 容器互联的废弃与替代 +| 网络类型 | 说明 | 适用场景 | +|---------|------|---------| +| **bridge** | 默认类型,容器连接到虚拟网桥 | 大多数单机场景 | +| **host** | 容器直接使用宿主机网络栈 | 需要最高网络性能时 | +| **none** | 禁用网络 | 完全隔离的容器 | +| **overlay** | 跨主机网络 | Docker Swarm 集群 | +| **macvlan** | 容器拥有独立 MAC 地址 | 需要直接接入物理网络 | -在 Docker 的早期版本中,`--link` 选项被用来连接容器。然而,这个功能现在已经被废弃,并且不推荐在生产环境中使用。 +--- -**注意:`--link` 是一个遗留功能。它可能会在未来的版本中被移除。我们强烈建议使用用户自定义网络来连接多个容器。** +## 用户自定义网络(推荐) -使用自定义网络,容器之间可以通过容器名直接进行通信,这比使用 `--link` 更加灵活和强大。 +### 为什么要用自定义网络 -接下来的部分将介绍 Docker 的一些高级网络配置,包括 DNS 配置和端口映射等内容。 +默认 bridge 网络的局限: + +| 问题 | 自定义网络的优势 | +|------|-----------------| +| 只能用 IP 通信 | 支持容器名 DNS 解析 | +| 所有容器在同一网络 | 更好的隔离性 | +| 需要 --link(已废弃) | 原生支持服务发现 | + +### 创建自定义网络 + +```bash +# 创建网络 +$ docker network create mynet + +# 查看网络详情 +$ docker network inspect mynet +``` + +### 使用自定义网络 + +```bash +# 启动容器并连接到自定义网络 +$ docker run -d --name web --network mynet nginx +$ docker run -d --name db --network mynet postgres + +# 在 web 容器中可以直接用容器名访问 db +$ docker exec web ping db +PING db (172.18.0.3): 56 data bytes +64 bytes from 172.18.0.3: seq=0 ttl=64 time=0.083 ms +``` + +### 容器名 DNS 解析 + +自定义网络自动提供 DNS 服务: + +``` +┌─────────────────────────────────────────────────────────┐ +│ mynet 网络 │ +│ │ +│ ┌─────────┐ DNS ┌─────────┐ │ +│ │ web │ ──── "db" → 172.18.0.3 ───► │ db │ │ +│ │172.18.0.2│ │172.18.0.3│ │ +│ └─────────┘ └─────────┘ │ +│ │ +│ web 容器可以用 "db" 作为主机名访问 db 容器 │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 容器互联 + +### 同一网络内的容器 + +同一自定义网络内的容器可以直接通信: + +```bash +# 创建网络 +$ docker network create app-net + +# 启动应用和数据库 +$ docker run -d --name redis --network app-net redis +$ docker run -d --name app --network app-net myapp + +# app 容器中可以用 redis:6379 连接 Redis +``` + +### 连接到多个网络 + +一个容器可以连接到多个网络: + +```bash +# 启动容器 +$ docker run -d --name multi-net-container --network frontend nginx + +# 再连接到另一个网络 +$ docker network connect backend multi-net-container + +# 查看容器的网络 +$ docker inspect multi-net-container --format '{{json .NetworkSettings.Networks}}' +``` + +### ⚠️ --link 已废弃 + +```bash +# 旧方式(不推荐) +$ docker run --link db:database myapp + +# 新方式(推荐) +$ docker network create mynet +$ docker run --network mynet --name db postgres +$ docker run --network mynet --name app myapp +``` + +--- + +## 端口映射 + +容器默认只能在 Docker 网络内访问。要从外部访问容器,需要端口映射: + +### 基本语法 + +```bash +# -p 宿主机端口:容器端口 +$ docker run -d -p 8080:80 nginx +``` + +### 映射方式 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `-p 8080:80` | 指定端口映射 | 宿主机 8080 → 容器 80 | +| `-p 80` | 随机宿主机端口 | 随机端口 → 容器 80 | +| `-P` | 自动映射所有暴露端口 | 随机端口 → 所有 EXPOSE 端口 | +| `-p 127.0.0.1:8080:80` | 只绑定本地 | 仅本机可访问 | +| `-p 8080:80/udp` | UDP 端口 | UDP 协议 | + +### 查看端口映射 + +```bash +$ docker port mycontainer +80/tcp -> 0.0.0.0:8080 +``` + +### 端口映射示意图 + +``` +外部请求 http://宿主机IP:8080 + │ + ▼ + ┌─────────────┐ + │ 宿主机:8080 │ ─── iptables NAT ───┐ + └─────────────┘ │ + ▼ + ┌───────────────┐ + │ 容器 nginx:80 │ + └───────────────┘ +``` + +--- + +## 网络隔离 + +不同网络之间默认隔离: + +```bash +# 创建两个网络 +$ docker network create frontend +$ docker network create backend + +# 容器 A 在 frontend +$ docker run -d --name web --network frontend nginx + +# 容器 B 在 backend +$ docker run -d --name db --network backend postgres + +# web 无法直接访问 db(不同网络) +$ docker exec web ping db +ping: db: Name or service not known +``` + +这种隔离有助于安全:前端容器无法直接访问数据库网络。 + +--- + +## 常用命令 + +```bash +# 列出网络 +$ docker network ls + +# 创建网络 +$ docker network create mynet + +# 查看网络详情 +$ docker network inspect mynet + +# 连接容器到网络 +$ docker network connect mynet mycontainer + +# 断开网络连接 +$ docker network disconnect mynet mycontainer + +# 删除网络 +$ docker network rm mynet + +# 清理未使用的网络 +$ docker network prune +``` + +--- + +## 本章小结 + +| 概念 | 要点 | +|------|------| +| **默认网络** | docker0 网桥,172.17.0.0/16 网段 | +| **自定义网络** | 推荐使用,支持容器名 DNS 解析 | +| **端口映射** | `-p 宿主机端口:容器端口` 暴露服务 | +| **网络隔离** | 不同网络默认隔离,增强安全性 | +| **--link** | 已废弃,使用自定义网络替代 | + +## 延伸阅读 + +- [高级网络配置](linking.md):容器互联详解 +- [配置 DNS](dns.md):自定义 DNS 设置 +- [端口映射](port_bindbindbindport.md):高级端口配置 +- [Compose 网络](../compose/compose_file.md):Compose 中的网络配置 diff --git a/security/README.md b/security/README.md index 1660aae..e504e59 100644 --- a/security/README.md +++ b/security/README.md @@ -1,59 +1,314 @@ # 安全 -容器安全是生产环境部署的核心考量。评估 Docker 的安全性时,主要考虑以下几个方面: +容器安全是生产环境部署的核心考量。本章介绍 Docker 的安全机制和最佳实践。 + +## 容器安全的本质 + +> **核心问题**:容器共享宿主机内核,隔离性弱于虚拟机。如何在便利性和安全性之间取得平衡? + +``` +虚拟机安全模型: 容器安全模型: +┌─────────────────┐ ┌─────────────────┐ +│ Guest OS │ │ 容器进程 │ +├─────────────────┤ │ (共享内核) │ +│ Hypervisor │◄── 隔离边界└────────┬────────┘ +├─────────────────┤ │ +│ Host OS │ ┌────────┴────────┐ +└─────────────────┘ │ Namespace │◄── 隔离边界 + │ Cgroups │ +完全隔离(性能损耗) │ Capabilities │ + └─────────────────┘ + 进程隔离(轻量但需加固) +``` + +--- ## 核心安全机制 -* **内核命名空间(Namespace)**:提供进程、网络、文件系统等资源的隔离 -* **控制组(Cgroups)**:限制容器的 CPU、内存、I/O 等资源使用 -* **Docker 守护进程安全**:服务端的访问控制和防护 -* **内核能力机制(Capabilities)**:细粒度的权限控制 +### 1. 命名空间(Namespace) -## 现代安全实践 +提供进程、网络、文件系统等资源的隔离: -### 镜像安全扫描 +| Namespace | 隔离内容 | 安全作用 | +|-----------|---------|---------| +| PID | 进程 | 容器看不到其他进程 | +| NET | 网络 | 独立网络栈 | +| MNT | 文件系统 | 独立的根目录 | +| USER | 用户 | 容器 root ≠ 宿主机 root | +| IPC | 进程通信 | 隔离共享内存 | +| UTS | 主机名 | 独立主机名 | -使用工具扫描镜像中的已知漏洞: +详见 [命名空间](../underly/namespace.md) 章节。 -* **Docker Scout**:Docker 官方集成的安全扫描工具,提供 SBOM 分析 -* **Trivy**:开源的全面漏洞扫描器 -* **Snyk**:商业级安全平台 +### 2. 控制组(Cgroups) + +限制容器的资源使用,防止资源耗尽攻击: ```bash -# 使用 Docker Scout 扫描镜像 -$ docker scout cves myimage:latest +# 限制内存(超出会被 OOM Kill) +$ docker run -m 512m myapp -# 使用 Trivy 扫描 -$ trivy image myimage:latest +# 限制 CPU +$ docker run --cpus=1.5 myapp + +# 限制磁盘 I/O +$ docker run --device-write-bps /dev/sda:10mb myapp ``` -### 非 root 用户运行 +### 3. 能力机制(Capabilities) -避免以 root 用户运行容器,降低权限逃逸风险: +Linux 将 root 权限拆分为多个细粒度的能力。Docker 默认禁用危险能力: -```dockerfile -FROM node:20-alpine -RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -D appuser -USER appuser -``` - -### 只读文件系统 - -使用只读根文件系统增强安全性: +| 能力 | 说明 | 默认状态 | +|------|------|---------| +| `CAP_NET_ADMIN` | 网络管理 | ❌ 禁用 | +| `CAP_SYS_ADMIN` | 系统管理 | ❌ 禁用 | +| `CAP_SYS_PTRACE` | 进程追踪 | ❌ 禁用 | +| `CAP_CHOWN` | 更改文件所有者 | ✅ 启用 | +| `CAP_NET_BIND_SERVICE` | 绑定低端口 | ✅ 启用 | ```bash -$ docker run --read-only --tmpfs /tmp myimage +# 删除所有能力,只添加需要的 +$ docker run --cap-drop=all --cap-add=NET_BIND_SERVICE myapp + +# 查看容器的能力 +$ docker exec myapp cat /proc/1/status | grep Cap ``` -### Docker Content Trust(DCT) +--- -启用镜像签名验证,确保镜像来源可信: +## 镜像安全 + +### 使用可信镜像 ```bash +# ✅ 使用官方镜像 +$ docker pull nginx + +# ✅ 使用经过验证的镜像 +$ docker pull bitnami/nginx + +# ⚠️ 谨慎使用未知来源镜像 +$ docker pull randomuser/suspicious-image +``` + +### 漏洞扫描 + +扫描镜像中的已知安全漏洞: + +```bash +# Docker Scout(官方工具) +$ docker scout cves nginx:latest +$ docker scout recommendations nginx:latest + +# Trivy(开源工具) +$ trivy image nginx:latest + +# Snyk(商业工具) +$ snyk container test nginx:latest +``` + +### 镜像签名验证 + +使用 Docker Content Trust (DCT) 验证镜像来源: + +```bash +# 启用镜像签名验证 $ export DOCKER_CONTENT_TRUST=1 + +# 此后的 pull/push 会验证签名 $ docker pull myregistry/myimage:latest ``` -## 本章内容 +--- -本章将详细介绍各安全机制的原理和配置方法。 +## 运行时安全 + +### 1. 非 root 用户运行 + +> 笔者强调:这是最重要的安全实践之一。 + +```dockerfile +FROM node:20-alpine + +# 创建非 root 用户 +RUN addgroup -g 1001 appgroup && \ + adduser -u 1001 -G appgroup -D appuser + +# 设置工作目录权限 +WORKDIR /app +COPY --chown=appuser:appgroup . . + +# 切换用户 +USER appuser + +CMD ["node", "server.js"] +``` + +或在运行时指定: + +```bash +$ docker run -u 1001:1001 myapp +``` + +### 2. 只读文件系统 + +```bash +# 根文件系统只读 +$ docker run --read-only myapp + +# 需要写入的目录使用 tmpfs +$ docker run --read-only --tmpfs /tmp --tmpfs /var/run myapp +``` + +### 3. 禁用特权模式 + +```bash +# ❌ 绝对不要在生产环境使用 +$ docker run --privileged myapp + +# ✅ 只添加必要的能力 +$ docker run --cap-add=SYS_TIME myapp +``` + +### 4. 限制资源 + +```bash +$ docker run \ + -m 512m \ # 内存限制 + --cpus=1 \ # CPU 限制 + --pids-limit=100 \ # 进程数限制 + --ulimit nofile=1024:1024 \ # 文件描述符限制 + myapp +``` + +### 5. 网络隔离 + +```bash +# 禁用网络(适用于不需要网络的任务) +$ docker run --network=none myapp + +# 使用自定义网络隔离 +$ docker network create --internal isolated_net +$ docker run --network=isolated_net myapp +``` + +--- + +## Dockerfile 安全实践 + +### 1. 使用精简基础镜像 + +```dockerfile +# ✅ 好:使用精简镜像 +FROM node:20-alpine # ~50MB +FROM gcr.io/distroless/nodejs # ~20MB + +# ❌ 差:使用完整镜像 +FROM node:20 # ~1GB +FROM ubuntu:24.04 # ~78MB +``` + +### 2. 多阶段构建 + +```dockerfile +# 构建阶段 +FROM node:20 AS builder +WORKDIR /app +COPY . . +RUN npm install && npm run build + +# 生产阶段(不包含开发依赖和源码) +FROM node:20-alpine +COPY --from=builder /app/dist /app +USER node +CMD ["node", "/app/server.js"] +``` + +### 3. 不存储敏感信息 + +```dockerfile +# ❌ 错误:敏感信息写入镜像 +ENV DB_PASSWORD=secret123 +COPY .env /app/ + +# ✅ 正确:运行时传入 +# docker run -e DB_PASSWORD=xxx 或使用 Docker Secrets +``` + +### 4. 固定依赖版本 + +```dockerfile +# ✅ 固定版本 +FROM node:20.10.0-alpine3.19 +RUN apk add --no-cache curl=8.5.0-r0 + +# ❌ 使用 latest +FROM node:latest +RUN apk add curl +``` + +--- + +## 安全扫描清单 + +部署前检查: + +| 检查项 | 命令/方法 | +|--------|----------| +| 漏洞扫描 | `docker scout cves` 或 `trivy` | +| 非 root 运行 | 检查 Dockerfile 中的 `USER` | +| 资源限制 | 检查 `-m`, `--cpus` 参数 | +| 只读文件系统 | 检查 `--read-only` | +| 无特权模式 | 确认没有 `--privileged` | +| 最小能力 | 检查 `--cap-drop=all` | +| 网络隔离 | 检查网络配置 | +| 敏感信息 | 确认无硬编码密码 | + +--- + +## 高级安全方案 + +### Seccomp 系统调用过滤 + +限制容器可以使用的系统调用: + +```bash +$ docker run --security-opt seccomp=/path/to/profile.json myapp +``` + +### AppArmor / SELinux + +使用强制访问控制: + +```bash +$ docker run --security-opt apparmor=docker-default myapp +``` + +### 安全容器(gVisor / Kata) + +需要更强隔离时: + +```bash +# 使用 gVisor 运行时 +$ docker run --runtime=runsc myapp +``` + +--- + +## 本章小结 + +| 安全措施 | 重要程度 | 实现方式 | +|---------|---------|---------| +| 非 root 运行 | ⭐⭐⭐ | `USER` 指令 | +| 漏洞扫描 | ⭐⭐⭐ | `docker scout`, `trivy` | +| 资源限制 | ⭐⭐⭐ | `-m`, `--cpus` | +| 只读文件系统 | ⭐⭐ | `--read-only` | +| 最小能力 | ⭐⭐ | `--cap-drop=all` | +| 镜像签名 | ⭐⭐ | Docker Content Trust | + +## 延伸阅读 + +- [命名空间](../underly/namespace.md):隔离机制详解 +- [控制组](../underly/cgroups.md):资源限制详解 +- [最佳实践](../appendix/best_practices.md):Dockerfile 安全配置 diff --git a/underly/arch.md b/underly/arch.md index 9811cbc..5b26def 100644 --- a/underly/arch.md +++ b/underly/arch.md @@ -1,51 +1,137 @@ # 基本架构 -Docker 采用了 `C/S`(客户端/服务端)架构,包括客户端和服务端。Docker 守护进程(`Daemon`)作为服务端接受来自客户端的请求,并处理这些请求(创建、运行、分发容器)。 +## 核心架构图 -客户端和服务端既可以运行在一个机器上,也可通过 `socket` 或者 `RESTful API` 来进行通信。 - -![Docker 基本架构](../.gitbook/assets/docker_arch.png) - -## 核心组件 - -Docker 的核心组件形成了一个层次化的架构: +Docker 采用了 **C/S (客户端/服务端)** 架构。Client 向 Daemon 发送请求,Daemon 负责构建、运行和分发容器。 ``` -┌─────────────────────────────────────────────────┐ -│ Docker CLI │ -│ (docker 命令行工具) │ -├─────────────────────────────────────────────────┤ -│ dockerd │ -│ (Docker 守护进程/引擎) │ -├─────────────────────────────────────────────────┤ -│ containerd │ -│ (容器生命周期管理器) │ -├─────────────────────────────────────────────────┤ -│ runc │ -│ (OCI 容器运行时) │ -└─────────────────────────────────────────────────┘ +┌───────────────┐ ┌────────────────────────────────────┐ +│ 客户端 │ │ Docker Host │ +│ (Docker CLI) │ │ │ +│ │ │ ┌──────────┐ ┌────────────┐ │ +│ $ docker run ├────►│ │ dockerd │ │ Containers │ │ +│ $ docker pull│ │ │ (Daemon) │─────►│ │ │ +│ $ docker ps │ │ └────┬─────┘ │ □ □ │ │ +└───────────────┘ │ │ └────────────┘ │ + │ │ ┌────────────┐ │ + │ └───────────►│ Images │ │ + │ │ │ │ + └────────────────────┴────────────┘ │ ``` -* **Docker CLI**:用户与 Docker 交互的命令行工具 -* **dockerd**:Docker 守护进程,提供 Docker API,管理镜像、网络、存储等 -* **containerd**:高级容器运行时,管理容器的完整生命周期 -* **runc**:低级容器运行时,根据 OCI 规范创建和运行容器 +--- + +## 组件详解 + +Docker 的内部架构如同洋葱一样分层,每一层专注解决特定问题: + +### 1. Docker CLI (客户端) +用户与 Docker 交互的主要方式。它将用户命令(如 `docker run`)转换为 API 请求发送给 dockerd。 + +### 2. Dockerd (守护进程) +Docker 的大脑。 +- 监听 API 请求 +- 管理 Docker 对象(镜像、容器、网络、卷) +- 编排下层组件完成工作 + +### 3. Containerd (高级运行时) +行业标准的容器运行时(CNCF 毕业项目)。 +- 管理容器的完整生命周期(启动、停止) +- 镜像拉取与存储 +- **不包含** 复杂的与容器无关的功能(如构建、API) +- Kubernetes 也可以直接使用 containerd(跳过 Docker) + +### 4. Runc (低级运行时) +用于创建和运行容器的 CLI 工具。 +- 直接与内核交互(Namespaces, Cgroups) +- 遵循 OCI (Open Container Initiative) 规范 +- **主要职责**:根据配置启动一个容器,然后退出(将控制权交给容器进程) + +### 5. Shim +每个容器都有一个 shim 进程。 +- **解耦**:允许 dockerd 重启而不影响容器运行 +- **保持 IO**:维持容器的标准输入输出 +- **状态汇报**:向 containerd 汇报容器退出状态 + +--- + +## 容器启动流程 + +当执行 `docker run -d nginx` 时,内部发生了什么? + +``` +User + │ + ▼ +Docker CLI ──(REST API)──> Dockerd + │ + ▼ + Containerd ──(gRPC)──> Containerd-shim + │ + ▼ + Runc + │ + ▼ + Kernel (创建容器) + (Start & Exit) +``` + +1. **CLI** 发送请求给 **Dockerd** +2. **Dockerd** 解析请求,调用 **Containerd** +3. **Containerd** 准备镜像,转换为 OCI Bundle +4. **Containerd** 创建 **Shim** 进程 +5. **Shim** 调用 **Runc** +6. **Runc** 与系统内核交互,创建 Namespaces 和 Cgroups +7. **Runc** 启动 nginx 进程后退出 +8. **Shim** 接管容器 IO 和生命周期监控 + +--- + +## Docker Engine v29+ 变化 + +从 Docker Engine v29 (2025/2026) 开始,架构进一步简化和标准化: + +- **Containerd 镜像存储 (Image Store)**:默认启用。Docker 直接使用 Containerd 的镜像管理能力,不再维护自己的一套 graphdriver。 + - **优势**:多平台镜像支持更好、镜像拉取更快(lazy pulling)、与 K8s 共享镜像。 + +--- ## Docker Desktop 架构 -在 macOS 和 Windows 上,Docker Desktop 使用轻量级虚拟机运行 Linux 内核: +在 macOS 和 Windows 上,因为内核差异,架构稍微复杂: -* **macOS**:使用 Apple Hypervisor Framework 或 QEMU -* **Windows**:使用 WSL 2(推荐)或 Hyper-V +``` +┌────────────── MacOS / Windows ──────────────┐ +│ Docker CLI │ +│ │ │ +├──────┼──────────────────────────────────────┤ +│ ▼ (Socket 映射) │ +│ ┌────────── Linux VM (虚拟机) ───────────┐ │ +│ │ │ │ +│ │ Dockerd <--> Containerd <--> Runc │ │ +│ │ │ │ +│ └────────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` -这意味着容器实际运行在虚拟机内的 Linux 环境中,而非直接运行在宿主系统上。 +- 使用轻量级虚拟机(Apple Virtualization / WSL 2)运行 Linux 内核 +- 文件挂载(Bind Mount)需要跨越 VM 边界(这也是文件 I/O 慢的原因) +- 网络端口需要从宿主机转发到 VM -## Docker Engine v29 重要变化 +--- -自 Docker Engine v29 起,**Containerd 镜像存储**成为新安装的默认配置。这一变化: +## 总结 -* 简化了 Docker 的内部架构 -* 提升了与 Kubernetes 等 containerd 平台的互操作性 -* 为 Lazy Pulling 等新特性奠定基础 +| 组件 | 角色 | 关键职责 | +|------|------|----------| +| **CLI** | 指挥官 | 发送指令,展示结果 | +| **Dockerd** | 大管家 | API 接口,整体调度 | +| **Containerd** | 经理 | 容器生命周期,镜像管理 | +| **Shim** | 监工 | 保持 IO,允许无守护进程重启 | +| **Runc** | 工人 | 真正干活(创建容器),干完就走 | -Docker 守护进程一般在宿主主机后台运行,等待接收来自客户端的消息。Docker 客户端则为用户提供一系列可执行命令,用户用这些命令实现跟 Docker 守护进程交互。 +## 延伸阅读 + +- [命名空间](./namespace.md):Runc 如何隔离容器 +- [控制组](./cgroups.md):Runc 如何限制资源 +- [联合文件系统](./ufs.md):镜像如何存储 diff --git a/underly/cgroups.md b/underly/cgroups.md index e7d5fd8..e30a8e9 100644 --- a/underly/cgroups.md +++ b/underly/cgroups.md @@ -1,7 +1,246 @@ # 控制组 -控制组([cgroups](https://en.wikipedia.org/wiki/Cgroups))是 Linux 内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。只有能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞争。 +## 什么是控制组 -控制组技术最早是由 Google 的程序员在 2006 年提出,Linux 内核自 2.6.24 开始支持。 +控制组(Control Groups,简称 cgroups)是 Linux 内核的一个特性,用于**限制、记录和隔离**进程组的资源使用(CPU、内存、磁盘 I/O、网络等)。 -控制组可以提供对容器的内存、CPU、磁盘 IO 等资源的限制和审计管理。 +> **核心作用**:让多个容器公平共享宿主机资源,防止单个容器耗尽系统资源。 + +``` +无 cgroups 限制: 有 cgroups 限制: +┌──────────────────────┐ ┌──────────────────────┐ +│ 宿主机资源 │ │ 宿主机资源 │ +│ ┌─────────────┐ │ │ ┌───┬───┬───┐ │ +│ │ 容器 A │ │ │ │ A │ B │ C │ │ +│ │ 占用所有 │ │ │ │1GB│1GB│1GB│ ← 限制│ +│ │ 内存和 CPU │ │ │ ├───┼───┼───┤ │ +│ └─────────────┘ │ │ │2核│1核│1核│ │ +│ 容器 B、C 饥饿 │ │ └───┴───┴───┘ │ +└──────────────────────┘ └──────────────────────┘ +``` + +--- + +## cgroups 的历史 + +| 时间 | 事件 | +|------|------| +| 2006 | Google 工程师提出 cgroups 概念 | +| 2008 | Linux 2.6.24 正式支持 cgroups v1 | +| 2016 | Linux 4.5 引入 cgroups v2 | +| 现在 | Docker 默认使用 cgroups v2(如系统支持) | + +--- + +## cgroups 可以限制的资源 + +| 资源类型 | 子系统 | 说明 | +|---------|--------|------| +| **CPU** | `cpu`, `cpuset` | CPU 使用时间和核心分配 | +| **内存** | `memory` | 内存使用上限和 swap | +| **块设备 I/O** | `blkio` | 磁盘读写速度限制 | +| **网络** | `net_cls`, `net_prio` | 网络带宽优先级 | +| **进程数** | `pids` | 限制进程/线程数量 | + +--- + +## Docker 中的资源限制 + +### 内存限制 + +```bash +# 限制容器最多使用 512MB 内存 +$ docker run -m 512m myapp + +# 限制内存 + swap +$ docker run -m 512m --memory-swap 1g myapp + +# 软限制(超过时警告,不会 OOM Kill) +$ docker run --memory-reservation 256m myapp +``` + +| 参数 | 说明 | +|------|------| +| `-m` / `--memory` | 硬限制(超过会 OOM Kill) | +| `--memory-swap` | 内存 + swap 总限制 | +| `--memory-reservation` | 软限制(内存竞争时生效) | +| `--oom-kill-disable` | 禁用 OOM Killer(谨慎使用) | + +### CPU 限制 + +```bash +# 限制使用 1.5 个 CPU 核心 +$ docker run --cpus=1.5 myapp + +# 限制使用 CPU 0 和 1 +$ docker run --cpuset-cpus="0,1" myapp + +# 设置 CPU 使用权重(相对值,默认 1024) +$ docker run --cpu-shares=512 myapp +``` + +| 参数 | 说明 | +|------|------| +| `--cpus` | 限制 CPU 核心数(如 1.5) | +| `--cpuset-cpus` | 绑定到特定 CPU 核心 | +| `--cpu-shares` | CPU 时间片权重(相对值) | +| `--cpu-period` / `--cpu-quota` | 精细控制 CPU 配额 | + +### 磁盘 I/O 限制 + +```bash +# 限制设备写入速度为 10MB/s +$ docker run --device-write-bps /dev/sda:10mb myapp + +# 限制设备读取速度 +$ docker run --device-read-bps /dev/sda:10mb myapp + +# 限制 IOPS +$ docker run --device-write-iops /dev/sda:100 myapp +``` + +### 进程数限制 + +```bash +# 限制最多 100 个进程 +$ docker run --pids-limit=100 myapp +``` + +--- + +## 查看容器资源使用 + +```bash +# 实时监控所有容器的资源使用 +$ docker stats +CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O +abc123 web 0.50% 45.5MiB / 512MiB 8.89% 1.2kB / 0B 0B / 0B +def456 db 2.30% 256MiB / 1GiB 25.00% 5.6kB / 3.2kB 4.1MB / 2.3MB + +# 查看特定容器 +$ docker stats mycontainer + +# 查看容器的 cgroup 配置 +$ docker inspect mycontainer --format '{{json .HostConfig}}' | jq +``` + +--- + +## 资源限制的效果 + +### 内存超限 + +```bash +# 启动限制 100MB 内存的容器 +$ docker run -m 100m stress --vm 1 --vm-bytes 200M + +# 容器会被 OOM Killer 杀死 +$ docker ps -a +CONTAINER ID STATUS NAMES +abc123 Exited (137) 5 seconds ago hopeful_darwin + +# 137 = 128 + 9,表示被 SIGKILL (9) 杀死 +``` + +### CPU 限制验证 + +```bash +# 不限制 CPU +$ docker run --rm stress --cpu 4 +# 占满所有 CPU + +# 限制为 1 个核心 +$ docker run --rm --cpus=1 stress --cpu 4 +# 只能使用约 100% CPU(1 个核心) +``` + +--- + +## cgroups v1 vs v2 + +| 特性 | cgroups v1 | cgroups v2 | +|------|-----------|-----------| +| 层级结构 | 多层级(每个资源单独) | 统一层级 | +| 管理复杂度 | 复杂 | 简化 | +| 资源分配 | 基于层级 | 基于子树 | +| PSI(压力监控) | ❌ | ✅ | +| rootless 容器 | 部分支持 | 完整支持 | + +### 检查系统使用的版本 + +```bash +# 查看 cgroup 版本 +$ mount | grep cgroup +cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime) +# 如果显示 cgroup2 表示 v2 + +# 或者 +$ cat /proc/filesystems | grep cgroup +nodev cgroup +nodev cgroup2 +``` + +--- + +## 在 Compose 中设置限制 + +```yaml +services: + web: + image: nginx + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M +``` + +--- + +## 最佳实践 + +### 1. 始终设置内存限制 + +```bash +# 防止 OOM 影响宿主机 +$ docker run -m 1g myapp +``` + +### 2. 为关键应用设置 CPU 保证 + +```bash +$ docker run --cpus=2 --cpu-shares=2048 critical-app +``` + +### 3. 监控资源使用 + +```bash +# 配合 Prometheus + cAdvisor 监控 +$ docker run -d --name cadvisor \ + -v /:/rootfs:ro \ + -v /var/run:/var/run:ro \ + -v /sys:/sys:ro \ + -v /var/lib/docker:/var/lib/docker:ro \ + gcr.io/cadvisor/cadvisor +``` + +--- + +## 本章小结 + +| 资源 | 限制参数 | 示例 | +|------|---------|------| +| **内存** | `-m` | `-m 512m` | +| **CPU 核心数** | `--cpus` | `--cpus=1.5` | +| **CPU 绑定** | `--cpuset-cpus` | `--cpuset-cpus="0,1"` | +| **磁盘 I/O** | `--device-write-bps` | `--device-write-bps /dev/sda:10mb` | +| **进程数** | `--pids-limit` | `--pids-limit=100` | + +## 延伸阅读 + +- [命名空间](namespace.md):资源隔离 +- [安全](../security/README.md):容器安全概述 +- [Docker Stats](../container/README.md):监控容器资源 diff --git a/underly/namespace.md b/underly/namespace.md index ef81aae..1246127 100644 --- a/underly/namespace.md +++ b/underly/namespace.md @@ -1,23 +1,267 @@ # 命名空间 -命名空间是 Linux 内核一个强大的特性。每个容器都有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统中运行一样。命名空间保证了容器之间彼此互不影响。 +## 什么是 Namespace -## pid 命名空间 -不同用户的进程就是通过 pid 命名空间隔离开的,且不同命名空间中可以有相同 pid。所有的 LXC 进程在 Docker 中的父进程为 Docker 进程,每个 LXC 进程具有不同的命名空间。同时由于允许嵌套,因此可以很方便的实现嵌套的 Docker 容器。 +> **Namespace 是 Linux 内核提供的资源隔离机制,它让容器内的进程仿佛运行在独立的操作系统中。** -## net 命名空间 -有了 pid 命名空间,每个命名空间中的 pid 能够相互隔离,但是网络端口还是共享 host 的端口。网络隔离是通过 net 命名空间实现的, 每个 net 命名空间有独立的 网络设备,IP 地址,路由表,/proc/net 目录。这样每个容器的网络就能隔离开来。Docker 默认采用 veth 的方式,将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docker0 连接在一起。 +Namespace 是容器技术的核心基础之一。它回答了一个关键问题:**如何让一个进程"以为"自己独占整个系统?** -## ipc 命名空间 -容器中进程交互还是采用了 Linux 常见的进程间交互方法(interprocess communication - IPC), 包括信号量、消息队列和共享内存等。然而同 VM 不同的是,容器的进程间交互实际上还是 host 上具有相同 pid 命名空间中的进程间交互,因此需要在 IPC 资源申请时加入命名空间信息,每个 IPC 资源有一个唯一的 32 位 id。 +``` +宿主机视角: 容器内视角: +┌─────────────────────────┐ ┌─────────────────────────┐ +│ PID 1: systemd │ │ PID 1: nginx │ ← 容器认为自己是 PID 1 +│ PID 2: sshd │ │ PID 2: nginx worker │ +│ PID 3: dockerd │ │ │ +│ PID 1234: nginx ←──────│─────│ (实际是宿主机的 1234) │ +│ PID 1235: nginx worker │ │ │ +└─────────────────────────┘ └─────────────────────────┘ +``` -## mnt 命名空间 -类似 chroot,将一个进程放到一个特定的目录执行。mnt 命名空间允许不同命名空间的进程看到的文件结构不同,这样每个命名空间 中的进程所看到的文件目录就被隔离开了。同 chroot 不同,每个命名空间中的容器在 /proc/mounts 的信息只包含所在命名空间的 mount point。 +## Namespace 的类型 -## uts 命名空间 -UTS("UNIX Time-sharing System") 命名空间允许每个容器拥有独立的 hostname 和 domain name, 使其在网络上可以被视作一个独立的节点而非 主机上的一个进程。 +Linux 内核提供了以下几种 Namespace,Docker 容器使用了全部: -## user 命名空间 -每个容器可以有不同的用户和组 id, 也就是说可以在容器内用容器内部的用户执行程序而非主机上的用户。 +| Namespace | 隔离内容 | 容器中的效果 | +|-----------|---------|-------------| +| **PID** | 进程 ID | 容器内 PID 从 1 开始,看不到其他容器和宿主机进程 | +| **NET** | 网络栈 | 独立的网卡、IP 地址、端口、路由表 | +| **MNT** | 挂载点 | 独立的文件系统视图,自己的根目录 | +| **UTS** | 主机名 | 独立的主机名和域名 | +| **IPC** | 进程间通信 | 独立的信号量、消息队列、共享内存 | +| **USER** | 用户/组 ID | 容器内的 root 可以映射为宿主机的普通用户 | +| **Cgroup** | Cgroup 根目录 | 隔离 cgroup 层级视图(Linux 4.6+) | -*注:更多关于 Linux 上命名空间的信息,请阅读 [这篇文章](https://blog.scottlowe.org/2013/09/04/introducing-linux-network-namespaces/)。 +--- + +## PID Namespace + +### 作用 + +隔离进程 ID,让每个容器有自己的进程编号空间。 + +### 效果 + +```bash +# 宿主机上查看进程 +$ ps aux | grep nginx +root 12345 0.0 0.1 nginx: master process +root 12346 0.0 0.1 nginx: worker process + +# 容器内查看进程 +$ docker exec mycontainer ps aux +PID USER COMMAND + 1 root nginx: master process ← 在容器内是 PID 1 + 2 root nginx: worker process +``` + +### 关键点 + +- 容器内的 PID 1 进程特殊重要——它是容器的主进程,退出则容器停止 +- 容器内无法看到宿主机或其他容器的进程 +- 宿主机可以看到所有容器内的进程(但 PID 不同) + +--- + +## NET Namespace + +### 作用 + +隔离网络栈,每个容器拥有独立的网络环境。 + +### 效果 + +``` +宿主机 容器 +┌─────────────────────┐ ┌─────────────────────┐ +│ eth0: 192.168.1.10 │ │ eth0: 172.17.0.2 │ ← 不同的 IP +│ docker0: 172.17.0.1│◄───────►│ (veth pair 连接) │ +│ 端口 80 可用 │ │ 端口 80 可用 │ ← 可以使用相同端口 +└─────────────────────┘ └─────────────────────┘ +``` + +### 关键点 + +- 每个容器有独立的网卡、IP、路由表、iptables 规则 +- 多个容器可以监听相同端口(如都监听 80) +- Docker 使用 veth pair 连接容器网络和宿主机网桥 + +--- + +## MNT Namespace + +### 作用 + +隔离文件系统挂载点,每个容器有自己的根目录。 + +### 效果 + +``` +宿主机文件系统: 容器内看到的: +/ / ← 容器的根目录 +├── bin/ ├── bin/ +├── home/ ├── home/ +├── var/ ├── var/ +│ └── lib/ │ └── lib/ +│ └── docker/ │ +│ └── overlay2/ │ +│ └── merged/ ────┼─── 这个目录成为容器的 / +└── ... └── ... +``` + +### 与 chroot 的区别 + +| 特性 | chroot | MNT Namespace | +|------|--------|---------------| +| 安全性 | 可以逃逸 | 更安全 | +| 挂载隔离 | 无 | 完全隔离 | +| /proc/mounts | 共享 | 独立 | + +--- + +## UTS Namespace + +### 作用 + +隔离主机名和域名,让每个容器可以有自己的主机名。 + +### 效果 + +```bash +# 宿主机 +$ hostname +my-server + +# 容器内 +$ docker run --hostname mycontainer ubuntu hostname +mycontainer +``` + +UTS = "UNIX Time-sharing System",是历史遗留的名称。 + +--- + +## IPC Namespace + +### 作用 + +隔离 System V IPC 和 POSIX 消息队列。 + +### 隔离的资源 + +- 信号量(semaphores) +- 消息队列(message queues) +- 共享内存(shared memory) + +### 关键点 + +- 同一容器内的进程可以通过 IPC 通信 +- 不同容器的进程无法通过 IPC 通信(除非显式共享) + +--- + +## USER Namespace + +### 作用 + +隔离用户和组 ID,实现权限隔离。 + +### 效果 + +``` +容器内 宿主机 +┌─────────────────┐ ┌─────────────────┐ +│ UID 0 (root) │───映射────►│ UID 100000 │ ← 非特权用户 +│ UID 1 (daemon) │───映射────►│ UID 100001 │ +└─────────────────┘ └─────────────────┘ +``` + +### 安全意义 + +容器内的 root 用户可以映射为宿主机上的普通用户,即使容器被突破,攻击者在宿主机上也只有普通权限。 + +> 💡 笔者建议:生产环境建议启用 User Namespace,增强安全性。 + +--- + +## 动手实验:体验 Namespace + +使用 `unshare` 命令可以在不使用 Docker 的情况下体验 Namespace: + +### 实验 1:UTS Namespace + +```bash +# 创建新的 UTS namespace 并启动 shell +$ sudo unshare --uts /bin/bash + +# 修改主机名(只影响这个 namespace) +$ hostname container-test +$ hostname +container-test + +# 退出后查看宿主机主机名(未改变) +$ exit +$ hostname +my-server +``` + +### 实验 2:PID Namespace + +```bash +# 创建新的 PID 和 MNT namespace +$ sudo unshare --pid --mount --fork /bin/bash + +# 挂载新的 /proc +$ mount -t proc proc /proc + +# 查看进程(只能看到当前 shell) +$ ps aux +USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND +root 1 0.0 0.0 8960 4516 pts/0 S 10:00 0:00 /bin/bash +root 8 0.0 0.0 10072 3200 pts/0 R+ 10:00 0:00 ps aux +``` + +### 实验 3:NET Namespace + +```bash +# 创建新的网络 namespace +$ sudo unshare --net /bin/bash + +# 查看网络接口(只有 lo) +$ ip addr +1: lo: mtu 65536 qdisc noop state DOWN + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 +``` + +--- + +## Namespace 的局限性 + +Namespace 提供了隔离但不是安全边界: + +| 方面 | 说明 | +|------|------| +| **共享内核** | 所有容器共享宿主机内核,内核漏洞可能影响所有容器 | +| **部分资源未隔离** | /proc、/sys 部分内容仍可见;时间无法隔离 | +| **非虚拟化** | 比虚拟机隔离性弱 | + +> 需要更强隔离时,可考虑 gVisor、Kata Containers 等安全容器方案。 + +--- + +## 本章小结 + +| Namespace | 隔离内容 | 一句话说明 | +|-----------|---------|-----------| +| PID | 进程 ID | 容器有自己的进程树 | +| NET | 网络 | 容器有自己的 IP 和端口 | +| MNT | 文件系统 | 容器有自己的根目录 | +| UTS | 主机名 | 容器有自己的 hostname | +| IPC | 进程间通信 | 容器间 IPC 隔离 | +| USER | 用户 ID | 容器 root ≠ 宿主机 root | + +## 延伸阅读 + +- [控制组(Cgroups)](cgroups.md):资源限制机制 +- [联合文件系统](ufs.md):分层存储的实现 +- [安全](../security/README.md):容器安全实践 +- [Linux Namespace 官方文档](https://man7.org/linux/man-pages/man7/namespaces.7.html) diff --git a/underly/ufs.md b/underly/ufs.md index fb01da1..13f1646 100644 --- a/underly/ufs.md +++ b/underly/ufs.md @@ -1,22 +1,230 @@ # 联合文件系统 -联合文件系统([UnionFS](https://en.wikipedia.org/wiki/UnionFS))是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。 +## 什么是联合文件系统 -联合文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。 +联合文件系统(UnionFS)是一种**分层、轻量级**的文件系统,它将多个目录"联合"挂载到同一个虚拟目录,形成一个统一的文件系统视图。 -另外,不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。 +> **核心思想**:将多个只读层叠加,最上层可写,形成完整的文件系统。 -Docker 中使用的 AUFS(Advanced Multi-Layered Unification Filesystem)就是一种联合文件系统。 `AUFS` 支持为每一个成员目录(类似 Git 的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限, 同时 `AUFS` 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。 +``` +┌─────────────────────────────────────────────────────────┐ +│ 容器看到的文件系统 │ +│ /bin /etc /lib /usr /var /app /data │ +└────────────────────────┬────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + │ UnionFS 联合挂载 │ + └───────────────┬───────────────┘ + │ +┌────────────────────────┴────────────────────────────────┐ +│ 容器层 (读写) │ /app/data/log.txt (新写入) │ +├────────────────────┼────────────────────────────────────│ +│ 镜像层3 (只读) │ /app/app.py │ +├────────────────────┼────────────────────────────────────│ +│ 镜像层2 (只读) │ /usr/local/bin/python │ +├────────────────────┼────────────────────────────────────│ +│ 镜像层1 (只读) │ /bin /etc /lib (基础系统) │ +└────────────────────┴────────────────────────────────────┘ +``` -Docker 目前支持的联合文件系统包括 `OverlayFS`, `AUFS`, `Btrfs`, `VFS`, `ZFS` 和 `Device Mapper`。 +--- -各 Linux 发行版 Docker 推荐使用的存储驱动如下表。 +## 为什么 Docker 使用联合文件系统 -|Linux 发行版 | Docker 推荐使用的存储驱动 | -| :-- | :-- | -|Docker on Ubuntu | `overlay2` (16.04 +) | -|Docker on Debian | `overlay2` (Debian Stretch), `aufs`, `devicemapper` | -|Docker on CentOS | `overlay2` | -|Docker on Fedora | `overlay2` | +### 1. 镜像分层复用 -在可能的情况下,[推荐](https://docs.docker.com/storage/storagedriver/select-storage-driver/) 使用 `overlay2` 存储驱动,`overlay2` 是目前 Docker 默认的存储驱动,以前则是 `aufs`。你可以通过配置来使用以上提到的其他类型的存储驱动。 +``` +nginx:alpine myapp:latest + │ │ + └────────┬────────────┘ + │ + ▼ + alpine:3.19 (共享基础层) +``` + +多个镜像共享相同的底层,节省磁盘空间。 + +### 2. 快速构建 + +每个 Dockerfile 指令创建一层,只有变化的层需要重建: + +```docker +FROM node:20 # 层1:基础镜像 +COPY package.json ./ # 层2:依赖定义 +RUN npm install # 层3:安装依赖 +COPY . . # 层4:应用代码 +``` + +代码变化时,只需重建层4,层1-3 使用缓存。 + +### 3. 容器启动快 + +容器启动时不需要复制镜像,只需: +1. 在镜像层上创建一个薄的可写层 +2. 联合挂载所有层 + +--- + +## Copy-on-Write(写时复制) + +当容器修改只读层中的文件时: + +``` +修改前: 修改后: +┌─────────────────────┐ ┌─────────────────────┐ +│ 容器层 (空) │ │ 容器层 │ +├─────────────────────┤ │ /etc/nginx.conf ←──┼── 复制到容器层后修改 +│ 镜像层 │ ├─────────────────────┤ +│ /etc/nginx.conf │ │ 镜像层 │ +└─────────────────────┘ │ /etc/nginx.conf │ (原文件仍在,但被遮蔽) + └─────────────────────┘ +``` + +**流程**: +1. 从只读层读取文件 +2. 复制到容器的可写层 +3. 在可写层中修改 +4. 后续读取使用可写层的版本 + +--- + +## Docker 支持的存储驱动 + +Docker 可使用多种联合文件系统实现: + +| 存储驱动 | 说明 | 推荐程度 | +|---------|------|---------| +| **overlay2** | 现代 Linux 默认驱动,性能优秀 | ✅ **推荐** | +| **aufs** | 早期默认,兼容性好 | 遗留系统 | +| **btrfs** | 使用 Btrfs 子卷 | 特定场景 | +| **zfs** | 使用 ZFS 数据集 | 特定场景 | +| **devicemapper** | 块设备级存储 | 遗留系统 | +| **vfs** | 不使用 CoW,每层完整复制 | 仅测试 | + +### 各发行版推荐 + +| Linux 发行版 | 推荐存储驱动 | +|-------------|-------------| +| Ubuntu 16.04+ | overlay2 | +| Debian Stretch+ | overlay2 | +| CentOS 7+ | overlay2 | +| RHEL 8+ | overlay2 | +| Fedora | overlay2 | + +### 查看当前存储驱动 + +```bash +$ docker info | grep "Storage Driver" +Storage Driver: overlay2 +``` + +--- + +## overlay2 工作原理 + +overlay2 是目前最推荐的存储驱动: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ merged(合并视图) │ +│ 容器看到的完整文件系统 │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + │ OverlayFS │ + └───────────────┬───────────────┘ + │ + ┌────────────────┼────────────────┐ + ▼ ▼ ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ upper │ │ lower2 │ │ lower1 │ +│ (容器层) │ │ (镜像层) │ │ (基础层) │ +│ 读写 │ │ 只读 │ │ 只读 │ +└─────────────┘ └─────────────┘ └─────────────┘ +``` + +- **lowerdir**:只读的镜像层(可以有多个) +- **upperdir**:可写的容器层 +- **workdir**:OverlayFS 的工作目录 +- **merged**:联合挂载后的视图 + +### 文件操作行为 + +| 操作 | 行为 | +|------|------| +| **读取** | 从上到下查找第一个匹配的文件 | +| **创建** | 在 upper 层创建 | +| **修改** | 如果在 lower 层,先复制到 upper 层再修改 | +| **删除** | 在 upper 层创建 whiteout 文件标记删除 | + +--- + +## 查看镜像层 + +```bash +# 查看镜像的层信息 +$ docker history nginx:alpine +IMAGE CREATED CREATED BY SIZE +a6eb2a334a9f 2 weeks ago CMD ["nginx" "-g" "daemon off;"] 0B + 2 weeks ago STOPSIGNAL SIGQUIT 0B + 2 weeks ago EXPOSE map[80/tcp:{}] 0B + 2 weeks ago ENTRYPOINT ["/docker-entrypoint.sh"] 0B + 2 weeks ago COPY 30-tune-worker-processes.sh /docker-ent… 4.62kB +... + +# 查看层的存储位置 +$ docker inspect nginx:alpine --format '{{json .GraphDriver.Data}}' | jq +{ + "LowerDir": "/var/lib/docker/overlay2/.../diff:/var/lib/docker/overlay2/.../diff", + "MergedDir": "/var/lib/docker/overlay2/.../merged", + "UpperDir": "/var/lib/docker/overlay2/.../diff", + "WorkDir": "/var/lib/docker/overlay2/.../work" +} +``` + +--- + +## 最佳实践 + +### 1. 减少镜像层数 + +```docker +# ❌ 每条命令创建一层 +RUN apt-get update +RUN apt-get install -y nginx +RUN rm -rf /var/lib/apt/lists/* + +# ✅ 合并为一层 +RUN apt-get update && \ + apt-get install -y nginx && \ + rm -rf /var/lib/apt/lists/* +``` + +### 2. 避免在容器中写入大量数据 + +容器层的写入性能低于直接写入。大量数据应使用: +- 数据卷(Volume) +- 绑定挂载(Bind Mount) + +### 3. 使用 .dockerignore + +排除不需要的文件可以: +- 减小构建上下文 +- 避免创建不必要的层 + +--- + +## 本章小结 + +| 概念 | 说明 | +|------|------| +| **UnionFS** | 将多层目录联合挂载为一个文件系统 | +| **Copy-on-Write** | 写时复制,修改时才复制到可写层 | +| **overlay2** | Docker 默认推荐的存储驱动 | +| **分层好处** | 镜像复用、快速构建、快速启动 | + +## 延伸阅读 + +- [镜像](../basic_concept/image.md):理解镜像分层 +- [容器](../basic_concept/container.md):容器存储层 +- [构建镜像](../image/build.md):Dockerfile 层的创建