Add more content

This commit is contained in:
Baohua Yang
2026-01-30 16:48:39 -08:00
parent c58f61dbed
commit fec2e506d9
39 changed files with 8202 additions and 1063 deletions

View File

@@ -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] [![](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可以让应用的部署、测试和分发都变得前所未有的高效和轻松 [Docker](https://www.docker.com) 是个划时代的开源项目,它彻底释放了计算虚拟化的威力,极大提高了应用的维护效率,降低了云计算应用开发的成本!使用 Docker可以让应用的部署、测试和分发都变得前所未有的高效和轻松

View File

@@ -1,13 +1,246 @@
# Docker 容器 # 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 和数据持久化详解

View File

@@ -1,15 +1,222 @@
# Docker 镜像 # 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 解释器 |
| **库文件** | libcOpenSSL各种依赖库 |
| **配置文件** | nginx.confmy.cnf |
| **环境变量** | PATHLANG 等预设值 |
| **元数据** | 启动命令暴露端口数据卷定义 |
**关键特性**
- 镜像是**只读**
- 镜像**不包含**动态数据
- 镜像构建后**内容不会改变**
## 分层存储镜像的核心设计
### 为什么需要分层
笔者认为分层存储是 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
<missing> 2 weeks ago STOPSIGNAL SIGQUIT 0B
<missing> 2 weeks ago EXPOSE map[80/tcp:{}] 0B
<missing> 2 weeks ago ENTRYPOINT ["/docker-entrypoint.sh"] 0B
<missing> 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. 镜像 IDContent-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)深入理解分层存储的技术原理

View File

@@ -1,29 +1,250 @@
# Docker Registry # 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 Hubghcr.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 RegistryDocker 官方提供了 [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)** | 支持多种制品类型DockerMavennpm |
| **云厂商服务** | 阿里云 ACR腾讯云 TCRAWS 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)配置镜像加速

View File

@@ -2,41 +2,114 @@
> 本小节内容适合 `Python` 开发人员阅读 > 本小节内容适合 `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 ```docker
FROM python:3 FROM python:3.12-slim
ENV PYTHONUNBUFFERED 1
RUN mkdir /code # 防止 Python 缓冲 stdout/stderr让日志实时输出
ENV PYTHONUNBUFFERED=1
# 设置工作目录
WORKDIR /code WORKDIR /code
# 先复制依赖文件,利用 Docker 缓存加速构建
COPY requirements.txt /code/ COPY requirements.txt /code/
RUN pip install -r requirements.txt
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目代码
COPY . /code/ 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 > 💡 **笔者建议**总是将变化频率低的文件先复制变化频率高的后复制这样可以最大化利用 Docker 的构建缓存
Django>=4.0,<5.0
psycopg2-binary>=2.9,<3.0 ## 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 ```yaml
services: services:
db: db:
image: postgres image: postgres:16
environment: 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: web:
build: . build: .
@@ -45,76 +118,222 @@ services:
- .:/code - .:/code
ports: ports:
- "8000:8000" - "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 ```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 django-docker/
Dockerfile docker-compose.yml django_example manage.py requirements.txt ├── 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 ## Step 5: 配置数据库连接
$ sudo chown -R $USER:$USER .
```
首先我们要为应用设置好数据库的连接信息用以下内容替换 `django_example/settings.py` 文件中 `DATABASES = ...` 定义的节点内容 修改 `mysite/settings.py`配置数据库连接
```python ```python
import os
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'postgres', 'NAME': os.environ.get('POSTGRES_DB', 'django_db'),
'USER': 'postgres', 'USER': os.environ.get('POSTGRES_USER', 'django_user'),
'HOST': 'db', 'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'django_password'),
'HOST': 'db', # Docker Compose 服务名
'PORT': 5432, '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 ```bash
$ docker compose up $ 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 ```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 和数据持久化详解

View File

@@ -1,117 +1,268 @@
# 使用 Rails # 使用 Rails
> 本小节内容适合 `Ruby` 开发人员阅读 > 本小节内容适合 Ruby 开发人员阅读
我们现在将使用 `Compose` 配置并运行一个 `Rails/PostgreSQL` 应用 本节使用 Docker Compose 配置并运行一个 **Rails + PostgreSQL** 应用
在一切工作开始前需要先设置好三个必要的文件 ## 架构概览
首先因为应用将要运行在一个满足所有环境依赖的 Docker 容器里面那么我们可以通过编辑 `Dockerfile` 文件来指定 Docker 容器要安装内容内容如下 ```
┌─────────────────────────────────────────────────────────────┐
```docker │ Docker Compose 网络 │
FROM ruby │ │
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev │ ┌─────────────────────┐ ┌─────────────────────┐ │
RUN mkdir /myapp │ │ web 服务 │ │ db 服务 │ │
WORKDIR /myapp │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
ADD Gemfile /myapp/Gemfile │ │ │ Rails │ │──────│ │ PostgreSQL │ │ │
RUN bundle install │ │ │ 应用 │ │ :5432│ │ 数据库 │ │ │
ADD . /myapp │ │ └───────────────┘ │ │ └───────────────┘ │ │
│ │ :3000 │ │ │ │
│ └──────────┬──────────┘ └─────────────────────┘ │
│ │ │
└─────────────┼───────────────────────────────────────────────┘
localhost:3000
``` ```
以上内容指定应用将使用安装了 RubyBundler 以及其依赖件的镜像更多关于如何编写 Dockerfile 文件的信息可以查看 [Dockerfile 使用](../image/dockerfile/README.md) ## 准备工作
下一步我们需要一个引导加载 Rails 的文件 `Gemfile` 等一会儿它还会被 `rails new` 命令覆盖重写 创建项目目录
```bash ```bash
source 'https://rubygems.org' $ mkdir rails-docker && cd rails-docker
gem 'rails', '4.0.2'
``` ```
最后`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 ```yaml
services: services:
db: db:
image: postgres image: postgres:16
ports: environment:
- "5432" POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
web: web:
build: . 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: volumes:
- .:/myapp - .:/myapp
ports: ports:
- "3000:3000" - "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 ```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 ```bash
$ ls $ ls
Dockerfile app docker-compose.yml tmp Dockerfile Gemfile Rakefile config lib tmp
Gemfile bin lib vendor Gemfile.lock README.md app config.ru log vendor
Gemfile.lock condocker-compose log docker-compose.yml bin db public
README.rdoc condocker-compose.ru public
Rakefile db test
``` ```
在新的 `Gemfile` 文件去掉加载 `therubyracer` 的行的注释这样我们便可以使用 Javascript 运行环境 > **Linux 用户**如遇权限问题执行 `sudo chown -R $USER:$USER .`
```bash ## Step 5: 重新构建镜像
gem 'therubyracer', platforms: :ruby
```
现在我们已经有一个新的 `Gemfile` 文件需要重新建镜像这个会步骤会改变 Dockerfile 文件本身所以需要重建一次 由于生成了新的 Gemfile需要重新建镜像以安装完整依赖
```bash ```bash
$ docker compose build $ docker compose build
``` ```
应用现在就可以启动了但配置还未完成Rails 默认读取的数据库目标是 `localhost` 我们需要手动指定容器的 `db` 同样的还需要把用户名修改成和 postgres 镜像预定的一致 ## Step 6: 配置数据库连接
打开最新生成的 `database.yml` 文件用以下内容替换
```bash 修改 `config/database.yml`
development: &default
```yaml
default: &default
adapter: postgresql adapter: postgresql
encoding: unicode encoding: unicode
database: postgres pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
pool: 5 url: <%= ENV['DATABASE_URL'] %>
username: postgres
password: development:
host: db <<: *default
test: test:
<<: *default <<: *default
database: myapp_test database: myapp_test
production:
<<: *default
``` ```
现在就可以启动应用了 > 💡 使用 `DATABASE_URL` 环境变量配置数据库符合 12-factor 应用原则便于在不同环境间切换
## Step 7: 启动应用
```bash ```bash
$ docker compose up $ docker compose up
``` ```
如果一切正常你应该可以看到 PostgreSQL 的输出几秒后可以看到这样的重复信息 输出示例
```bash ```
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick 1.3.1 db-1 | PostgreSQL init process complete; ready for start up.
myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.0.0 (2013-11-22) [x86_64-linux-gnu] db-1 | LOG: database system is ready to accept connections
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port=3000 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 ```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)数据持久化

View File

@@ -1,51 +1,210 @@
# 使用 WordPress # 实战 WordPress
> 本小节内容适合 `PHP` 开发人员阅读 ## 简介
`Compose` 可以很便捷的让 `Wordpress` 运行在一个独立的环境 WordPress 是全球最流行的内容管理系统CMS使用 Docker Compose 可以在几分钟内搭建一个包含数据库Web 服务和持久化存储的生产级 WordPress 环境
## 创建空文件夹 ---
假设新建一个名为 `wordpress` 的文件夹然后进入这个文件夹 ## 项目结构
## 创建 `docker-compose.yml` 文件 ```
wordpress/
[`docker-compose.yml`](https://github.com/yeasy/docker_practice/blob/master/compose/demo/wordpress/docker-compose.yml) 文件将开启一个 `wordpress` 服务和一个独立的 `MySQL` 实例: ├── docker-compose.yml
├── .env # 环境变量(敏感信息)
```yaml └── nginx/ # 可选:反向代理配置
└── nginx.conf
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:
``` ```
## 构建并运行项目 ---
运行 `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):官方镜像文档

View File

@@ -1,56 +1,264 @@
# 进入容器 # 进入容器
在使用 `-d` 参数时容器启动后会进入后台 ## 为什么需要进入容器
某些时候需要进入容器进行操作包括使用 `docker attach` 命令或 `docker exec` 命令推荐大家使用 `docker exec` 命令原因会在下面说明 使用 `-d` 参数启动容器后容器在后台运行以下场景需要进入容器内部操作
## `attach` 命令 | 场景 | 示例 |
|------|------|
| **调试问题** | 查看日志检查配置排查错误 |
| **临时操作** | 执行数据库迁移清理缓存 |
| **检查状态** | 查看进程网络连接文件系统 |
| **开发测试** | 交互式测试命令验证环境 |
下面示例如何使用 `docker attach` 命令 ## 两种进入方式
Docker 提供两种进入容器的命令
| 命令 | 推荐程度 | 特点 |
|------|---------|------|
| `docker exec` | **推荐** | 启动新进程退出不影响容器 |
| `docker attach` | 谨慎使用 | 附加到主进程退出可能停止容器 |
---
## docker exec推荐
### 基本用法
```bash ```bash
$ docker run -dit ubuntu # 进入容器并启动交互式 shell
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550 $ docker exec -it 容器名 /bin/bash
$ docker container ls # 或使用 sh适用于 Alpine 等精简镜像)
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $ docker exec -it 容器名 /bin/sh
243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia
$ docker attach 243c
root@243c32535da7:/#
``` ```
*注意* 如果从这个 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 ```bash
$ docker run -dit ubuntu # 启动一个后台容器
69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6 $ docker run -dit --name myubuntu ubuntu
69d137adef7a...
$ docker container ls # 进入容器(交互式 shell
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $ docker exec -it myubuntu bash
69d137adef7a ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds zealous_swirles root@69d137adef7a:/# ls
bin boot dev etc home lib ...
root@69d137adef7a:/# exit
$ docker exec -i 69d1 bash # 容器仍在运行!
ls $ docker ps
bin 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 boot
dev dev
... ...
$ docker exec -it 69d1 bash # 用 -it有完整的终端体验
root@69d137adef7a:/# $ 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)查看容器输出

View File

@@ -1,10 +1,19 @@
# 后台运行 # 后台运行
更多的时候需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下此时可以通过添加 `-d` 参数来实现 在生产环境中我们通常需要容器持续运行不受终端关闭的影响本节将深入讲解如何让容器在后台运行以及理解容器生命周期的核心概念
下面举两个例子来说明一下 ## 核心概念前台 vs 后台
如果不使用 `-d` 参数运行容器 当你在终端运行一个程序时有两种模式
- **前台运行**程序占用当前终端输出直接显示关闭终端程序就停止
- **后台运行**程序在后台执行不占用终端终端关闭也不影响程序
Docker 容器默认是**前台运行**使用 `-d`detach参数可以让容器在后台运行
## 基本使用
### 前台运行默认
```bash ```bash
$ docker run ubuntu:24.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" $ 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 hello world
``` ```
容器会把输出的结果 (STDOUT) 打印到宿主机上面 容器会把输出的结果STDOUT打印到宿主机上面此时
- 终端被占用无法执行其他命令
- `Ctrl+C` 会终止容器
- 关闭终端窗口容器也会停止
如果使用 `-d` 参数运行容器 ### 后台运行使用 -d 参数
```bash ```bash
$ docker run -d ubuntu:24.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" $ docker run -d ubuntu:24.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a 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 $ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 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 77b2dc01fe0f ubuntu:24.04 /bin/sh -c 'while tr 2 minutes ago Up 1 minute agitated_wright
``` ```
要获取容器的输出信息可以通过 `docker container logs` 命令 ### 查看容器输出日志
```bash ```bash
$ docker container logs [container ID or NAMES] $ docker container logs 77b2dc01fe0f
hello world hello world
hello world 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)管理多个后台容器的更好方式

View File

@@ -1,18 +1,238 @@
# 删除容器 # 删除容器
可以使用 `docker container rm` 来删除一个处于终止状态的容器例如 ## 基本用法
使用 `docker rm` 删除已停止的容器
```bash ```bash
$ docker container rm trusting_newton $ docker rm 容器名或ID
trusting_newton
``` ```
如果要删除一个运行中的容器可以添加 `-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 ```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 $ 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)数据卷管理

View File

@@ -1,55 +1,166 @@
# 启动容器 # 启动容器
启动容器有两种方式一种是基于镜像新建一个容器并启动另外一个是将在终止状态`exited`的容器重新启动 ## 启动方式概述
因为 Docker 的容器实在太轻量级了很多时候用户都是随时删除和新创建容器 启动容器有两种方式
- **新建并启动**基于镜像创建新容器
- **重新启动**将已终止的容器重新运行
由于 Docker 容器非常轻量实际使用中常常是随时删除和新建容器而不是反复重启同一个容器
## 新建并启动 ## 新建并启动
所需要的命令主要为 `docker run` ### 基本语法
例如下面的命令输出一个 Hello World之后终止容器 ```bash
docker run [选项] 镜像 [命令] [参数...]
```
### 最简单的例子
输出 "Hello World" 后容器自动终止
```bash ```bash
$ docker run ubuntu:24.04 /bin/echo 'Hello world' $ docker run ubuntu:24.04 /bin/echo 'Hello world'
Hello world Hello world
``` ```
跟在本地直接执行 `/bin/echo 'hello world'` 几乎感觉不出任何区别 直接执行 `/bin/echo 'Hello world'` 几乎没有区别但实际上已经启动了一个完整的 Ubuntu 容器来执行这条命令
下面的命令则启动一个 bash 终端允许用户进行交互 ### 交互式容器
启动一个可以交互的 bash 终端
```bash ```bash
$ docker run -t -i ubuntu:24.04 /bin/bash $ docker run -it ubuntu:24.04 /bin/bash
root@af8bae53bdd3:/# root@af8bae53bdd3:/#
``` ```
其中`-t` 选项让Docker分配一个伪终端pseudo-tty并绑定到容器的标准输入上 `-i` 则让容器的标准输入保持打开 **参数说明**
在交互模式下用户可以通过所创建的终端来输入命令例如 | 参数 | 作用 |
|------|------|
| `-i` | 保持标准输入stdin打开允许输入 |
| `-t` | 分配伪终端pseudo-TTY提供终端界面 |
| `-it` | 两者组合使用获得交互式终端 |
在交互模式下可以执行命令
```bash ```bash
root@af8bae53bdd3:/# pwd root@af8bae53bdd3:/# pwd
/ /
root@af8bae53bdd3:/# ls root@af8bae53bdd3:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var 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) 下载 执行 `docker run` Docker 在后台完成以下操作
* 利用镜像创建并启动一个容器
* 分配一个文件系统并在只读的镜像层外面挂载一层可读写层 ```
* 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去 ┌─────────────────────────────────────────────────────────────────────┐
* 从地址池配置一个 ip 地址给容器 │ 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 ```bash
root@ba267838cc1b:/# ps root@ba267838cc1b:/# ps
@@ -58,4 +169,61 @@ root@ba267838cc1b:/# ps
11 ? 00:00:00 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)数据持久化方案

View File

@@ -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 ```bash
$ docker container ls -a $ docker stop 容器名或ID
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 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)排查停止原因

View File

@@ -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 \ │ /home/user/code/ │ │ │
--mount type=bind,source=/src/webapp,target=/usr/share/nginx/html \ ├── index.html │◄───────►│ /usr/share/nginx/
nginx:alpine │ ├── style.css │ Bind │ html/ │
│ └── app.js │ Mount │ (同一份文件) │
└─────────────────────┘ └─────────────────────┘
``` ```
上面的命令加载主机的 `/src/webapp` 目录到容器的 `/usr/share/nginx/html`目录这个功能在进行测试的时候十分方便比如用户可以放置一些程序到本地目录中来查看容器是否正常工作本地目录的路径必须是绝对路径以前使用 `-v` 参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹现在使用 `--mount` 参数时如果本地目录不存在Docker 会报错 ---
Docker 挂载主机目录的默认权限是 `读写`用户也可以通过增加 `readonly` 指定为 `只读` ## Bind Mount vs Volume
```bash | 特性 | Bind Mount | Volume |
$ docker run -d -P \ |------|------------|--------|
--name web \ | **数据位置** | 宿主机任意路径 | Docker 管理的目录 |
# -v /src/webapp:/usr/share/nginx/html:ro \ | **路径指定** | 必须是绝对路径 | 卷名 |
--mount type=bind,source=/src/webapp,target=/usr/share/nginx/html,readonly \ | **可移植性** | 依赖宿主机路径 | 更好Docker 管理 |
nginx:alpine | **性能** | 依赖宿主机文件系统 | 优化的存储驱动 |
| **适用场景** | 开发环境配置文件 | 生产数据持久化 |
| **备份** | 直接访问文件 | 需要通过 Docker |
### 选择建议
```
需求 推荐方案
─────────────────────────────────────────
开发时同步代码 → Bind Mount
持久化数据库数据 → Volume
共享配置文件 → Bind Mount
容器间共享数据 → Volume
备份方便 → Bind Mount直接访问
生产环境 → Volume
``` ```
加了 `readonly` 之后就挂载为 `只读` 如果你在容器内 `/usr/share/nginx/html` 目录新建文件会显示如下错误 ---
## 基本语法
### 使用 --mount推荐
```bash ```bash
/usr/share/nginx/html # touch new.txt $ docker run -d \
touch: new.txt: Read-only file system --mount type=bind,source=/宿主机路径,target=/容器路径 \
nginx
``` ```
## 查看数据卷的具体信息 ### 使用 -v简写
在主机里使用以下命令可以查看 `web` 容器的信息
```bash ```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 ```json
"Mounts": [ [
{ {
"Type": "bind", "Type": "bind",
"Source": "/src/webapp", "Source": "/home/user/code",
"Destination": "/usr/share/nginx/html", "Destination": "/app",
"Mode": "", "Mode": "",
"RW": true, "RW": true,
"Propagation": "rprivate" "Propagation": "rprivate"
} }
], ]
``` ```
## 挂载一个本地主机文件作为数据卷 | 字段 | 说明 |
|------|------|
| `Type` | 挂载类型bind |
| `Source` | 宿主机路径 |
| `Destination` | 容器内路径 |
| `RW` | 是否可读写 |
| `Propagation` | 挂载传播模式 |
`--mount` 标记也可以从主机挂载单个文件到容器中 ---
## 常见问题
### Q: 路径不存在报错
```bash ```bash
$ docker run --rm -it \ $ docker run --mount type=bind,source=/not/exist,target=/app nginx
# -v $HOME/.bash_history:/root/.bash_history \ docker: Error response from daemon: invalid mount config for type "bind":
--mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \ bind source path does not exist: /not/exist
ubuntu:24.04 \
bash
root@2affd44b4667:/# history
1 ls
2 diskutil list
``` ```
这样就可以记录在容器输入过的命令了 **解决**确保源路径存在或改用 `-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 中的挂载配置

View File

@@ -1,38 +1,83 @@
# 数据卷 # 数据卷
`数据卷` 是一个可供一个或多个容器使用的特殊目录它绕过 UnionFS可以提供很多有用的特性 ## 为什么需要数据卷
* `数据卷` 可以在容器之间共享和重用 容器的存储层有一个关键问题**容器删除后数据就没了**
* `数据卷` 的修改会立马生效 ```
┌─────────────────────────────────────────────────────────────────┐
│ 容器存储层问题 │
│ │
│ 容器运行 ─────► 写入数据 ─────► 容器删除 ─────► 数据丢失! │
│ │
└─────────────────────────────────────────────────────────────────┘
```
* `数据卷` 的更新不会影响镜像 数据卷Volume解决了这个问题它的生命周期独立于容器
* `数据卷` 默认会一直存在即使容器被删除 ---
>注意`数据卷` 的使用类似于 Linux 下对目录或文件进行 mount镜像中的被指定为挂载点的目录中的文件会复制到数据卷中仅数据卷为空时会复制 ## 数据卷的特性
## 创建一个数据卷 | 特性 | 说明 |
|------|------|
| **持久化** | 容器删除后数据仍然保留 |
| **共享** | 多个容器可以挂载同一个数据卷 |
| **即时生效** | 对数据卷的修改立即可见 |
| **不影响镜像** | 数据卷中的数据不会打包进镜像 |
| **性能更好** | 绕过 UnionFS直接读写 |
---
## 数据卷 vs 容器存储层
```
容器存储层(不推荐存储重要数据):
┌─────────────────────────────────────────┐
│ 容器存储层(可读写) │
├─────────────────────────────────────────┤
│ 镜像层(只读) │
└─────────────────────────────────────────┘
生命周期 = 容器生命周期
容器删除 → 数据丢失
数据卷(推荐):
┌─────────────────────────────────────────┐
│ 容器 │
│ ┌─────────────────────────────────┐ │
│ │ /app/data ──────────────────│────┼──► 数据卷 my-data
│ └─────────────────────────────────┘ │ (独立于容器)
└─────────────────────────────────────────┘
容器删除 → 数据卷保留
```
---
## 数据卷基本操作
### 创建数据卷
```bash ```bash
$ docker volume create my-vol $ docker volume create my-vol
``` ```
查看所有的 `数据卷` ### 列出所有数据卷
```bash ```bash
$ docker volume ls $ docker volume ls
DRIVER VOLUME NAME
DRIVER VOLUME NAME local my-vol
local my-vol local postgres_data
local redis_data
``` ```
在主机里使用以下命令可以查看指定 `数据卷` 的信息 ### 查看数据卷详情
```bash ```bash
$ docker volume inspect my-vol $ docker volume inspect my-vol
[ [
{ {
"CreatedAt": "2024-01-15T10:00:00Z",
"Driver": "local", "Driver": "local",
"Labels": {}, "Labels": {},
"Mountpoint": "/var/lib/docker/volumes/my-vol/_data", "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 ```bash
$ docker run -d -P \ $ docker run -d \
--name web \ --name web \
# -v my-vol:/usr/share/nginx/html \
--mount source=my-vol,target=/usr/share/nginx/html \ --mount source=my-vol,target=/usr/share/nginx/html \
nginx:alpine nginx
``` ```
## 查看数据卷的具体信息 **参数说明**
在主机里使用以下命令可以查看 `web` 容器的信息 | 参数 | 说明 |
|------|------|
| `source` | 数据卷名称不存在会自动创建 |
| `target` | 容器内挂载路径 |
| `readonly` | 可选只读挂载 |
### 方式二-v简写
```bash ```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 ```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 volume rm my-vol
# 删除容器时同时删除数据卷
$ docker rm -v container_name
``` ```
`数据卷` 是被设计用来持久化数据的它的生命周期独立于容器Docker 不会在容器被删除后自动删除 `数据卷`并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 `数据卷`如果需要在删除容器的同时移除数据卷可以在删除容器的时候使用 `docker rm -v` 这个命令 ### 清理未使用的数据卷
无主的数据卷可能会占据很多空间要清理请使用以下命令
```bash ```bash
# 查看未被任何容器使用的数据卷
$ docker volume ls -f dangling=true
# 删除所有未使用的数据卷
$ docker volume prune $ 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 存储的底层原理

View File

@@ -1,32 +1,221 @@
# ADD 更高级的复制文件 # 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 ```docker
FROM scratch 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=<user>:<group>` 选项来改变文件的所属用户及所属组 ## URL 下载功能不推荐
### 基本用法
```docker ```docker
ADD --chown=55:mygroup files* /mydir/ # 从 URL 下载文件
ADD --chown=bin files* /mydir/ ADD https://example.com/app.zip /app/app.zip
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/
``` ```
### 为什么不推荐
| 问题 | 说明 |
|------|------|
| 权限固定 | 下载的文件权限为 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 编写指南

View File

@@ -1,68 +1,238 @@
# ARG 构建参数 # ARG 构建参数
格式`ARG <参数名>[=<默认值>]` ## 基本语法
构建参数和 `ENV` 的效果一样都是设置环境变量所不同的是`ARG` 所设置的构建环境的环境变量在将来容器运行时是不会存在这些环境变量的但是不要因此就使用 `ARG` 保存密码之类的信息因为 `docker history` 还是可以看到所有值的
`Dockerfile` 中的 `ARG` 指令是定义参数名称以及定义其默认值该默认值可以在构建命令 `docker build` 中用 `--build-arg <参数名>=<值>` 来覆盖
灵活的使用 `ARG` 指令能够在不修改 Dockerfile 的情况下构建出不同的镜像
ARG 指令有生效范围如果在 `FROM` 指令之前指定那么只能用于 `FROM` 指令中
```docker ```docker
ARG DOCKER_USERNAME=library ARG <参数名>[=<默认值>]
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo ${DOCKER_USERNAME}
``` ```
使用上述 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 ```docker
# 只在 FROM 中生效 # 定义有默认值的 ARG
ARG DOCKER_USERNAME=library ARG NODE_VERSION=20
FROM ${DOCKER_USERNAME}/alpine # 使用 ARG
FROM node:${NODE_VERSION}-alpine
# 要想在 FROM 之后使用,必须再次指定 RUN echo "Using Node.js $NODE_VERSION"
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
``` ```
对于多阶段构建尤其要注意这个问题 ### 构建时覆盖
```bash
# 使用默认值
$ docker build -t myapp .
# 覆盖默认值
$ docker build --build-arg NODE_VERSION=18 -t myapp .
```
---
## ARG 的作用域
### FROM 之前的 ARG
```docker ```docker
# 这个变量在每个 FROM 中都生效 # FROM 之前的 ARG 只能用于 FROM 指令
ARG DOCKER_USERNAME=library ARG REGISTRY=docker.io
ARG IMAGE_NAME=node
FROM ${DOCKER_USERNAME}/alpine FROM ${REGISTRY}/${IMAGE_NAME}:20
RUN set -x ; echo 1 # ❌ 这里无法使用上面的 ARG
RUN echo $REGISTRY # 输出空
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo 2
``` ```
对于上述 Dockerfile 两个 `FROM` 指令都可以使用 `${DOCKER_USERNAME}`对于在各个阶段中使用的变量都必须在每个阶段分别指定 ### FROM 之后重新声明
```docker ```docker
ARG DOCKER_USERNAME=library ARG NODE_VERSION=20
FROM ${DOCKER_USERNAME}/alpine FROM node:${NODE_VERSION}-alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定 # 需要再次声明才能使用
ARG DOCKER_USERNAME=library ARG NODE_VERSION
RUN echo "Node version: $NODE_VERSION"
RUN set -x ; echo ${DOCKER_USERNAME}
FROM ${DOCKER_USERNAME}/alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
``` ```
### 多阶段构建中的 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)复杂构建场景

View File

@@ -1,49 +1,268 @@
# CMD 容器启动命令 # CMD 容器启动命令
`CMD` 指令的格式和 `RUN` 相似也是两种格式 ## 什么是 CMD
* `shell` 格式`CMD <命令>` `CMD` 指令用于指定容器启动时默认执行的命令它定义了容器的"主进程"
* `exec` 格式`CMD ["可执行文件", "参数1", "参数2"...]`
* 参数列表格式`CMD ["参数1", "参数2"...]`在指定了 `ENTRYPOINT` 指令后 `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 |
在实际执行中会将其变更为 ### exec 格式推荐
```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` 可执行文件并且要求以前台形式运行比如
```docker ```docker
CMD ["nginx", "-g", "daemon off;"] 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 编写指南

View File

@@ -1,46 +1,261 @@
# COPY 复制文件 # COPY 复制文件
格式 ## 基本语法
* `COPY [--chown=<user>:<group>] <源路径>... <目标路径>`
* `COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]`
`RUN` 指令一样也有两种格式一种类似于命令行一种类似于函数调用
`COPY` 指令将从构建上下文目录中 `<源路径>` 的文件/目录复制到新的一层的镜像内的 `<目标路径>` 位置比如
```docker ```docker
COPY package.json /usr/src/app/ COPY [选项] <源路径>... <目标路径>
COPY [选项] ["<源路径1>", "<源路径2>", ... "<目标路径>"]
``` ```
`<源路径>` 可以是多个甚至可以是通配符其通配符规则要满足 Go [`filepath.Match`](https://golang.org/pkg/path/filepath/#Match) 规则,如: `COPY` 指令将构建上下文中的文件或目录复制到镜像内
---
## 基本用法
### 复制单个文件
```docker ```docker
COPY hom* /mydir/ # 复制文件到指定目录
COPY hom?.txt /mydir/ COPY package.json /app/
# 复制文件并重命名
COPY config.json /app/settings.json
``` ```
`<目标路径>` 可以是容器内的绝对路径也可以是相对于工作目录的相对路径工作目录可以用 `WORKDIR` 指令来指定目标路径不需要事先创建如果目录不存在会在复制文件前先行创建缺失目录 ### 复制多个文件
此外还需要注意一点使用 `COPY` 指令源文件的各种元数据都会保留比如读执行权限文件变更时间等这个特性对于镜像定制很有用特别是构建相关文件都在使用 Git 进行管理的时候
在使用该指令的时候还可以加上 `--chown=<user>:<group>` 选项来改变文件的所属用户及所属组
```docker ```docker
COPY --chown=55:mygroup files* /mydir/ # 复制多个指定文件
COPY --chown=bin files* /mydir/ COPY package.json package-lock.json /app/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/ # 使用通配符
COPY *.json /app/
COPY src/*.js /app/src/
``` ```
如果源路径为文件夹复制的时候不是直接复制该文件夹而是将文件夹中的内容复制到目标路径 ### 复制目录
## 使用 `--link` 优化多阶段构建
BuildKit 可以使用 `--link` 选项来优化多阶段构建的性能使用 `--link` 文件会以独立层的形式添加无需依赖前序指令的结果
```docker ```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 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 编写指南

View File

@@ -1,124 +1,306 @@
# ENTRYPOINT 入口点 # 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 ---
<ENTRYPOINT> "<CMD>"
## 语法格式
| 格式 | 语法 | 推荐程度 |
|------|------|---------|
| **exec 格式** | `ENTRYPOINT ["可执行文件", "参数1"]` | **推荐** |
| **shell 格式** | `ENTRYPOINT 命令 参数` | 不推荐 |
```docker
# exec 格式(推荐)
ENTRYPOINT ["nginx", "-g", "daemon off;"]
# shell 格式(不推荐)
ENTRYPOINT nginx -g "daemon off;"
``` ```
那么有了 `CMD` 为什么还要有 `ENTRYPOINT` 这种 `<ENTRYPOINT> "<CMD>"` 有什么好处么让我们来看几个场景 ---
#### 场景一让镜像变成像命令一样使用 ## 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 ```docker
FROM ubuntu:24.04 FROM ubuntu:24.04
RUN apt-get update \ RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
&& apt-get install -y curl \ CMD ["curl", "-s", "http://myip.ipip.net"]
&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://myip.ipip.net" ]
``` ```
假如我们使用 `docker build -t myip .` 来构建镜像的话如果我们需要查询当前公网 IP只需要执行
```bash ```bash
$ docker run myip $ docker run myip # ✓ 正常工作
当前 IP61.148.226.66 来自:北京市 联通 当前 IP61.148.226.66
$ docker run myip -i # ✗ 错误!
exec: "-i": executable file not found
# -i 替换了整个 CMD被当作可执行文件
``` ```
这么看起来好像可以直接把镜像当做命令使用了不过命令总有参数如果我们希望加参数呢比如从上面的 `CMD` 中可以看到实质的命令是 `curl`那么如果我们希望显示 HTTP 头信息就需要加上 `-i` 参数那么我们可以直接加 `-i` 参数给 `docker run myip` ### 使用 ENTRYPOINT 解决
```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` 来实现这个镜像
```docker ```docker
FROM ubuntu:24.04 FROM ubuntu:24.04
RUN apt-get update \ RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
&& apt-get install -y curl \ ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"]
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]
``` ```
这次我们再来尝试直接使用 `docker run myip -i`
```bash ```bash
$ docker run myip $ docker run myip # ✓ 正常工作
当前 IP61.148.226.66 来自:北京市 联通 当前 IP61.148.226.66
$ docker run myip -i $ docker run myip -i # ✓ 添加 -i 参数
HTTP/1.1 200 OK HTTP/1.1 200 OK
Server: nginx/1.8.0 ...
Date: Tue, 22 Nov 2016 05:12:40 GMT 当前 IP61.148.226.66
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
当前 IP61.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` 中去执行而这个脚本会将接到的参数也就是 `<CMD>`作为命令在脚本最后执行比如官方镜像 `redis` 中就是这么做的 在启动主服务前执行初始化脚本如数据库迁移权限设置
### 实现方式
```docker ```docker
FROM alpine:3.4 FROM redis:7-alpine
... COPY docker-entrypoint.sh /usr/local/bin/
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"] ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["redis-server"]
EXPOSE 6379
CMD [ "redis-server" ]
``` ```
可以看到其中为了 redis 服务创建了 redis 用户并在最后指定了 `ENTRYPOINT` `docker-entrypoint.sh` 脚本 **docker-entrypoint.sh**
```bash ```bash
#!/bin/sh #!/bin/sh
... set -e
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then # 准备工作
find . \! -user redis -exec chown redis '{}' + echo "Initializing..."
exec gosu redis "$0" "$@"
# 如果第一个参数是 redis-server以 redis 用户运行
if [ "$1" = 'redis-server' ]; then
chown -R redis:redis /data
exec gosu redis "$@"
fi fi
# 其他命令直接执行
exec "$@" 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 ```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)前台/后台概念

View File

@@ -1,35 +1,248 @@
# ENV 设置环境变量 # ENV 设置环境变量
格式有两种 ## 基本语法
* `ENV <key> <value>`
* `ENV <key1>=<value1> <key2>=<value2>...`
这个指令很简单就是设置环境变量而已无论是后面的其它指令 `RUN`还是运行时的应用都可以直接使用这里定义的环境变量
```docker ```docker
ENV VERSION=1.0 DEBUG=on \ # 格式一:单个变量
NAME="Happy Feet" ENV <key> <value>
# 格式二:多个变量(推荐)
ENV <key1>=<value1> <key2>=<value2> ...
``` ```
这个例子中演示了如何换行以及对含有空格的值用双引号括起来的办法这和 Shell 下的行为是一致的 ---
定义了环境变量那么在后续的指令中就可以使用这个环境变量比如在官方 `node` 镜像 `Dockerfile` 就有类似这样的代码 ## 基本用法
### 设置单个变量
```docker ```docker
ENV NODE_VERSION 7.2.0 ENV NODE_VERSION 20.10.0
ENV APP_ENV production
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
``` ```
在这里先定义了环境变量 `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 编写指南

View File

@@ -1,7 +1,219 @@
# EXPOSE 声明端口 # 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 中的端口配置

View File

@@ -1,83 +1,206 @@
# HEALTHCHECK 健康检查 # HEALTHCHECK 健康检查
格式 ## 基本语法
* `HEALTHCHECK [选项] CMD <命令>`设置检查容器健康状况的命令 ```docker
* `HEALTHCHECK NONE`如果基础镜像有健康检查指令使用这行可以屏蔽掉其健康检查指令 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 **引入 HEALTHCHECK **
* `--timeout=<时长>`健康检查命令运行超时时间如果超过这个时间本次健康检查就被视为失败默认 30 Docker 定期执行指定的检查命令根据返回值判断容器是否"健康"
* `--retries=<次数>`当连续失败指定次数后则将容器状态视为 `unhealthy`默认 3
`CMD`, `ENTRYPOINT` 一样`HEALTHCHECK` 只可以出现一次如果写了多个只有最后一个生效 ```
容器状态转换:
Starting ──成功──> Healthy ──失败N次──> Unhealthy
▲ │
└──────成功──────┘
```
`HEALTHCHECK [选项] CMD` 后面的命令格式和 `ENTRYPOINT` 一样分为 `shell` 格式 `exec` 格式命令的返回值决定了该次健康检查的成功与否`0`成功`1`失败`2`保留不要使用这个值 ---
假设我们有个镜像是个最简单的 Web 服务我们希望增加健康检查来判断其 Web 服务是否在正常工作我们可以用 `curl` 来帮助判断 `Dockerfile` `HEALTHCHECK` 可以这么写 ## 基本用法
### Web 服务检查
```docker ```docker
FROM nginx FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* 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 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
$ docker container ls # MySQL
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES HEALTHCHECK CMD mysqladmin ping -h localhost || exit 1
03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 3 seconds ago Up 2 seconds (health: starting) 80/tcp, 443/tcp web
# Redis
HEALTHCHECK CMD redis-cli ping || exit 1
``` ```
在等待几秒钟后再次 `docker container ls`就会看到健康状态变化为了 `(healthy)` ### 自定义脚本
```bash ```docker
$ docker container ls COPY healthcheck.sh /usr/local/bin/
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES HEALTHCHECK CMD ["healthcheck.sh"]
03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 18 seconds ago Up 16 seconds (healthy) 80/tcp, 443/tcp web
``` ```
如果健康检查连续失败超过了重试次数状态就会变为 `(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 ```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, "Status": "healthy",
"Log": [ "FailingStreak": 0,
{ "Log": [
"End": "2016-11-25T14:35:37.940957051Z", {
"ExitCode": 0, "Start": "...",
"Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n", "End": "...",
"Start": "2016-11-25T14:35:37.780192565Z" "ExitCode": 0,
} "Output": "..."
], }
"Status": "healthy" ]
} }
``` ```
---
## 最佳实践
### 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)容器排障

View File

@@ -1,17 +1,154 @@
# LABEL 指令 # LABEL 为镜像添加元数据
`LABEL` 指令用来给镜像以键值对的形式添加一些元数据metadata ## 基本语法
```docker ```docker
LABEL <key>=<value> <key>=<value> <key>=<value> ... LABEL <key>=<value> <key>=<value> ...
``` ```
我们还可以用一些标签来申明镜像的作者文档地址等 `LABEL` 指令以键值对的形式给镜像添加元数据这些数据不会影响镜像的功能但可以帮助用户理解镜像或被自动化工具使用
---
## 为什么需要 LABEL
1. **版本管理**记录版本号构建时间Git Commit ID
2. **联系信息**维护者邮箱文档地址支持渠道
3. **自动化工具** CI/CD 工具可以读取标签触发操作
4. **许可证信息**声明开源协议
---
## 基本用法
### 定义单个标签
```docker ```docker
LABEL org.opencontainers.image.authors="yeasy" LABEL version="1.0"
LABEL description="这是一个 Web 应用服务器"
LABEL org.opencontainers.image.documentation="https://yeasy.gitbooks.io"
``` ```
具体可以参考 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 <docker-maint@nginx.com>"
}
```
### 过滤器
可以使用标签过滤镜像
```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)

View File

@@ -1,65 +1,151 @@
# ONBUILD 为他人做嫁衣裳 # ONBUILD 为他人做嫁衣裳
格式`ONBUILD <其它指令>` ## 基本语法
`ONBUILD` 是一个特殊的指令它后面跟的是其它指令比如 `RUN`, `COPY` 而这些指令在当前镜像构建时并不会被执行只有当以当前镜像为基础镜像去构建下一级镜像的时候才会被执行
`Dockerfile` 中的其它指令都是为了定制当前镜像而准备的唯有 `ONBUILD` 是为了帮助别人定制自己而准备的
假设我们要制作 Node.js 所写的应用的镜像我们都知道 Node.js 使用 `npm` 进行包管理所有依赖配置启动信息等会放到 `package.json` 文件里在拿到程序代码后需要先进行 `npm install` 才可以获得所有需要的依赖然后就可以通过 `npm start` 来启动应用因此一般来说会这样写 `Dockerfile`
```docker ```docker
FROM node:slim ONBUILD <其它指令>
RUN mkdir /app ```
`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 WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ] # 这些指令将在子镜像构建时执行
COPY . /app/ ONBUILD COPY package*.json ./
CMD [ "npm", "start" ] ONBUILD RUN npm install
ONBUILD COPY . .
CMD ["npm", "start"]
``` ```
把这个 `Dockerfile` 放到 Node.js 项目的根目录构建好镜像后就可以直接拿来启动容器运行但是如果我们还有第二个 Node.js 项目也差不多呢好吧那就再把这个 `Dockerfile` 复制到第二个项目里那如果有第三个项目呢再复制么文件的副本越多版本控制就越困难让我们继续看这样的场景维护的问题 **子项目 Dockerfile**
如果第一个 Node.js 项目在开发过程中发现这个 `Dockerfile` 里存在问题比如敲错字了或者需要安装额外的包然后开发人员修复了这个 `Dockerfile`再次构建问题解决第一个项目没问题了但是第二个项目呢虽然最初 `Dockerfile` 是复制粘贴自第一个项目的但是并不会因为第一个项目修复了他们的 `Dockerfile`而第二个项目的 `Dockerfile` 就会被自动修复
那么我们可不可以做一个基础镜像然后各个项目使用这个基础镜像呢这样基础镜像更新各个项目不用同步 `Dockerfile` 的变化重新构建后就继承了基础镜像的更新好吧可以让我们看看这样的结果那么上面的这个 `Dockerfile` 就会变为
```docker ```docker
FROM node:slim FROM my-node-base
RUN mkdir /app # 只需要一行!
WORKDIR /app # 构建时会自动执行 COPY 和 RUN
CMD [ "npm", "start" ]
``` ```
这里我们把项目相关的构建指令拿出来放到子项目里去假设这个基础镜像的名字为 `my-node` 的话各个项目内的自己的 `Dockerfile` 就变为 ---
## 执行机制
```
基础镜像构建:
Dockerfile (含 ONBUILD) ──build──> 基础镜像 (记录了 ONBUILD 触发器)
(指令未执行)
子镜像构建:
FROM 基础镜像 ──build──> 读取基础镜像触发器 ──> 执行触发器指令 ──> 继续执行子 Dockerfile
```
---
## 常见使用场景
### 1. 自动处理依赖安装
```docker ```docker
FROM my-node # Python 基础镜像
COPY ./package.json /app ONBUILD COPY requirements.txt ./
RUN [ "npm", "install" ] ONBUILD RUN pip install -r requirements.txt
COPY . /app/
``` ```
基础镜像变化后各个项目都用这个 `Dockerfile` 重新构建镜像会继承基础镜像的更新 ### 2. 自动编译代码
那么问题解决了么没有准确说只解决了一半如果这个 `Dockerfile` 里面有些东西需要调整呢比如 `npm install` 都需要加一些参数那怎么办这一行 `RUN` 是不可能放入基础镜像的因为涉及到了当前项目的 `./package.json`难道又要一个个修改么所以说这样制作基础镜像只解决了原来的 `Dockerfile` 的前4条指令的变化问题而后面三条指令的变化则完全没办法处理
`ONBUILD` 可以解决这个问题让我们用 `ONBUILD` 重新写一下基础镜像的 `Dockerfile`:
```docker ```docker
FROM node:slim # Go 基础镜像
RUN mkdir /app ONBUILD COPY . .
WORKDIR /app ONBUILD RUN go build -o app main.go
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]
``` ```
这次我们回到原始的 `Dockerfile`但是这次将项目相关的指令加上 `ONBUILD`这样在构建基础镜像的时候这三行并不会被执行然后各个项目的 `Dockerfile` 就变成了简单地 ### 3. 处理静态资源
```docker ```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)基础镜像设计

View File

@@ -1,33 +1,141 @@
# SHELL 指令 # SHELL 指令
格式`SHELL ["executable", "parameters"]` ## 基本语法
`SHELL` 指令可以指定 `RUN` `ENTRYPOINT` `CMD` 指令的 shellLinux 中默认为 `["/bin/sh", "-c"]`
```docker ```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"] SHELL ["/bin/sh", "-c"]
# 回到 sh
RUN lll ; ls RUN echo "Using sh again"
SHELL ["/bin/sh", "-cex"]
RUN lll ; ls
``` ```
两个 `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 ```docker
SHELL ["/bin/sh", "-cex"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# /bin/sh -cex "nginx"
ENTRYPOINT nginx
``` ```
```docker ### 2. 明确意图
SHELL ["/bin/sh", "-cex"]
# /bin/sh -cex "nginx" 如果由于脚本需求必须更改 shell最好在 Dockerfile 中显式声明而不是依赖默认行为
CMD nginx
``` ### 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)错误处理与调试

View File

@@ -1,26 +1,273 @@
# USER 指定当前用户 # USER 指定当前用户
格式`USER <用户名>[:<用户组>]` ## 基本语法
`USER` 指令和 `WORKDIR` 相似都是改变环境状态并影响以后的层`WORKDIR` 是改变工作目录`USER` 则是改变之后层的执行 `RUN`, `CMD` 以及 `ENTRYPOINT` 这类命令的身份
注意`USER` 只是帮助你切换到指定用户而已这个用户必须是事先建立好的否则无法切换
```docker ```docker
RUN groupadd -r redis && useradd -r -g redis redis USER <用户名>[:<用户组>]
USER redis USER <UID>[:<GID>]
RUN [ "redis-server" ]
``` ```
如果以 `root` 执行的脚本在执行期间希望改变身份比如希望以某个已经建立好的用户来运行某个服务进程不要使用 `su` 或者 `sudo`这些都需要比较麻烦的配置而且在 TTY 缺失的环境下经常出错建议使用 [`gosu`](https://github.com/tianon/gosu) `USER` 指令切换后续指令RUNCMDENTRYPOINT的执行用户
---
## 为什么要使用 USER
> 笔者强调以非 root 用户运行容器是最重要的安全实践之一
```
root 用户运行的风险:
┌────────────────────────────────────────────────────────┐
│ 容器内 root ←─ 可能逃逸 ─→ 宿主机 root │
│ │ │ │
│ └── 漏洞利用 ───────────────→ 完全控制宿主机 │
└────────────────────────────────────────────────────────┘
非 root 用户运行:
┌────────────────────────────────────────────────────────┐
│ 容器内普通用户 ──逃逸后──→ 宿主机普通用户 │
│ │ │ │
│ └── 权限受限,危害降低 ─────→ 无法控制系统 │
└────────────────────────────────────────────────────────┘
```
---
## 基本用法
### 创建并切换用户
```docker ```docker
# 建立 redis 用户,并使用 gosu 换另一个用户执行命令 FROM node:20-alpine
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu # 1. 创建用户和组
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \ RUN addgroup -g 1001 appgroup && \
&& chmod +x /usr/local/bin/gosu \ adduser -u 1001 -G appgroup -D appuser
&& gosu nobody true
# 设置 CMD并以另外的用户执行 # 2. 设置目录权限
CMD [ "exec", "gosu", "redis", "redis-server" ] 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 用户无法绑定 80443 等端口
**解决**
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 安全

View File

@@ -1,20 +1,247 @@
# VOLUME 定义匿名卷 # VOLUME 定义匿名卷
格式为 ## 基本语法
* `VOLUME ["<路径1>", "<路径2>"...]`
* `VOLUME <路径>`
之前我们说过容器运行时应该尽量保持容器存储层不发生写操作对于数据库类需要保存动态数据的应用其数据库文件应该保存于卷(volume)后面的章节我们会进一步介绍 Docker 卷的概念为了防止运行时用户忘记将动态文件所保存目录挂载为卷 `Dockerfile` 我们可以事先指定某些目录挂载为匿名卷这样在运行时如果用户不指定挂载其应用也可以正常运行不会向容器存储层写入大量数据
```docker ```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 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 中的卷配置

View File

@@ -1,36 +1,196 @@
# WORKDIR 指定工作目录 # WORKDIR 指定工作目录
格式为 `WORKDIR <工作目录路径>` ## 基本语法
使用 `WORKDIR` 指令可以来指定工作目录或者称为当前目录以后各层的当前目录就被改为指定的目录如该目录不存在`WORKDIR` 会帮你建立目录
之前提到一些初学者常犯的错误是把 `Dockerfile` 等同于 Shell 脚本来书写这种错误的理解还可能会导致出现下面这样的错误
```docker ```docker
RUN cd /app WORKDIR <工作目录路径>
RUN echo "hello" > world.txt
``` ```
如果将这个 `Dockerfile` 进行构建镜像运行后会发现找不到 `/app/world.txt` 文件或者其内容不是 `hello`原因其实很简单 Shell 连续两行是同一个进程执行环境因此前一个命令修改的内存状态会直接影响后一个命令而在 `Dockerfile` 这两行 `RUN` 命令的执行环境根本不同是两个完全不同的容器这就是对 `Dockerfile` 构建分层存储的概念不了解所导致的错误 `WORKDIR` 指定后续指令的工作目录如果目录不存在Docker 会自动创建
之前说过每一个 `RUN` 都是启动一个容器执行命令然后提交存储层文件变更第一层 `RUN cd /app` 的执行仅仅是当前进程的工作目录变更一个内存上的变化而已其结果不会造成任何文件变更而到第二层的时候启动的是一个全新的容器跟第一层的容器更完全没关系自然不可能继承前一层构建过程中的内存变化 ---
因此如果需要改变以后各层的工作目录的位置那么应该使用 `WORKDIR` 指令 ## 基本用法
```docker ```docker
WORKDIR /app 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 ```docker
WORKDIR /a WORKDIR /a
WORKDIR b WORKDIR b
WORKDIR c 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 编写指南

View File

@@ -1,150 +1,258 @@
# 列出镜像 # 列出镜像
要想列出已经下载下来的镜像可以使用 `docker image ls` 命令 ## 基本用法
查看本地已下载的镜像
```bash ```bash
$ docker image ls $ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB redis latest 5f515359c7f8 5 days ago 183MB
nginx latest 05a60462f8ba 5 days ago 181 MB nginx latest 05a60462f8ba 5 days ago 181MB
mongo 3.2 fe9198c04d62 5 days ago 342 MB ubuntu 24.04 329ed837d508 3 days ago 78MB
<none> <none> 00285df0df87 5 days ago 342 MB ubuntu noble 329ed837d508 3 days ago 78MB
ubuntu 18.04 329ed837d508 3 days ago 63.3MB
ubuntu bionic 329ed837d508 3 days ago 63.3MB
``` ```
列表包含了 `仓库名``标签``镜像 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 ```bash
$ docker system df $ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 15 3 2.5GB 1.8GB (72%)
Images 24 0 1.992GB 1.992GB (100%) Containers 5 2 100MB 80MB (80%)
Containers 1 0 62.82MB 62.82MB (100%) Local Volumes 8 2 500MB 400MB (80%)
Local Volumes 9 0 652.2MB 652.2MB (100%) Build Cache 0 0 0B 0B
Build Cache 0B 0B
``` ```
## 虚悬镜像 ---
上面的镜像列表中还可以看到一个特殊的镜像这个镜像既没有仓库名也没有标签均为 `<none>` ## 过滤镜像
### 按仓库名过滤
```bash ```bash
<none> <none> 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` 这个镜像名被转移到了新下载的镜像身上而旧的镜像上的这个名称则被取消从而成为了 `<none>`除了 `docker pull` 可能导致这种情况`docker build` 也同样可以导致这种现象由于新旧镜像同名旧镜像名称被取消从而出现仓库名标签均为 `<none>` 的镜像这类无标签镜像也被称为 **虚悬镜像(dangling image)** 可以用下面的命令专门显示这类镜像 ### 按仓库名和标签过滤
```bash ```bash
$ docker image ls -f dangling=true $ docker images ubuntu:24.04
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID SIZE
<none> <none> 00285df0df87 5 days ago 342 MB 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 ```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
### 什么是虚悬镜像
仓库名和标签都显示为 `<none>` 的镜像
```bash
$ docker images
REPOSITORY TAG IMAGE ID SIZE
<none> <none> 00285df0df87 342MB
```
### 产生原因
1. **镜像重新构建**新镜像使用了旧镜像的标签旧镜像标签被移除
2. **docker pull 更新**拉取更新版本时旧版本失去标签
### 处理虚悬镜像
```bash
# 列出虚悬镜像
$ docker images -f dangling=true
# 删除虚悬镜像
$ docker image prune $ docker image prune
``` ```
---
## 中间层镜像 ## 中间层镜像
为了加速镜像构建重复利用资源Docker 会利用 **中间层镜像**所以在使用一段时间后可能会看到一些依赖的中间层镜像默认的 `docker image ls` 列表中只会显示顶层镜像如果希望显示包括中间层镜像在内的所有镜像的话需要加 `-a` 参数 ### 查看所有镜像包含中间层
```bash ```bash
$ docker image ls -a $ docker images -a
``` ```
这样会看到很多无标签镜像与之前的虚悬镜像不同这些无标签的镜像很多都是中间层镜像是其它镜像依赖的镜像这些无标签镜像不应该删除否则会导致上层镜像因为依赖丢失而出错实际上这些镜像也没必要删除因为之前说过相同的层只会存一遍而这些镜像是别的镜像的依赖因此并不会因为它们被列出来而多存了一份无论如何你也会需要它们只要删除那些依赖它们的镜像后这些依赖的中间层镜像也会被连带删除 会显示很多无标签镜像这些是构建过程中产生的中间层被其他镜像依赖
## 列出部分镜像 > 不要删除中间层镜像它们是其他镜像的依赖删除会导致上层镜像无法使用删除顶层镜像时会自动清理不再需要的中间层
不加任何参数的情况下`docker image ls` 会列出所有顶层镜像但是有时候我们只希望列出部分镜像`docker image ls` 有好几个参数可以帮助做到这个事情 ---
根据仓库名列出镜像 ## 格式化输出
### 只输出 ID
```bash ```bash
$ docker image ls ubuntu $ docker images -q
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
5f515359c7f8 5f515359c7f8
05a60462f8ba 05a60462f8ba
fe9198c04d62
00285df0df87
329ed837d508
329ed837d508 329ed837d508
``` ```
`--filter` 配合 `-q` 产生出指定范围的 ID 列表然后送给另一个 `docker` 命令作为参数从而针对这组实体成批的进行某种操作的做法在 Docker 命令行使用过程中非常常见不仅仅是镜像将来我们会在各个命令中看到这类搭配以完成很强大的功能因此每次在文档看到过滤器后可以多注意一下它们的用法 常用于配合其他命令
另外一些时候我们可能只是对表格的结构不满意希望自己组织列或者不希望有标题这样方便其它程序解析结果等这就用到了 [Go 的模板语法](https://gohugo.io/templates/introduction/)。
比如下面的命令会直接列出镜像结果并且只包含镜像ID和仓库名
```bash ```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 5f515359c7f8: redis
05a60462f8ba: nginx 05a60462f8ba: nginx
fe9198c04d62: mongo
00285df0df87: <none>
329ed837d508: ubuntu
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 ```bash
$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" # 列出所有镜像及其大小,按大小排序(需要系统 sort 命令)
IMAGE ID REPOSITORY TAG $ docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | sort -h
5f515359c7f8 redis latest
05a60462f8ba nginx latest # 查找大于 500MB 的镜像
fe9198c04d62 mongo 3.2 $ docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | grep -E "^[0-9]+GB|^[5-9][0-9]{2}MB"
00285df0df87 <none> <none>
329ed837d508 ubuntu 18.04 # 导出镜像列表
329ed837d508 ubuntu bionic $ 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)理解镜像概念

View File

@@ -1,23 +1,59 @@
# 获取镜像 # 获取镜像
之前提到过[Docker Hub](https://hub.docker.com/search?q=&type=image) 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像。 ## docker pull 命令
Docker 镜像仓库获取镜像的命令是 `docker pull`其命令格式为 从镜像仓库获取镜像的命令是 `docker pull`
```bash ```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 ```bash
$ docker pull ubuntu:24.04 $ docker pull ubuntu:24.04
18.04: Pulling from library/ubuntu 24.04: Pulling from library/ubuntu
92dc2a97ff99: Pull complete 92dc2a97ff99: Pull complete
be13a9d27eb8: Pull complete be13a9d27eb8: Pull complete
c8299583700a: Pull complete c8299583700a: Pull complete
@@ -26,43 +62,171 @@ Status: Downloaded newer image for ubuntu:24.04
docker.io/library/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` 并且进行交互式操作的话可以执行下面的命令 │ ubuntu:24.04 镜像 │
├─────────────────────────────────────────────────────────────┤
```bash │ 第3层 c8299583700a ───────► 已存在,跳过下载 │
$ docker run -it --rm ubuntu:24.04 bash ├─────────────────────────────────────────────────────────────┤
│ 第2层 be13a9d27eb8 ───────► 下载中... 完成 │
root@e7009c6ce357:/# cat /etc/os-release ├─────────────────────────────────────────────────────────────┤
NAME="Ubuntu" │ 第1层 92dc2a97ff99 ───────► 下载中... 完成 │
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
``` ```
`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)官方镜像仓库

View File

@@ -1,87 +1,255 @@
# 删除本地镜像 # 删除本地镜像
如果要删除本地的镜像可以使用 `docker image rm` 命令其格式为 ## 基本用法
使用 `docker image rm` 删除本地镜像
```bash ```bash
$ docker image rm [选项] <镜像1> [<镜像2> ...] $ 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 ```bash
$ docker image ls $ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID SIZE
centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB redis alpine 501ad78535f0 30MB
redis alpine 501ad78535f0 3 weeks ago 21.03 MB nginx latest e43d811ce2f4 142MB
docker latest cf693ec9b5c7 3 weeks ago 105.1 MB
nginx latest e43d811ce2f4 5 weeks ago 181.5 MB # 只需输入足够区分的前几位
$ docker rmi 501
Untagged: redis:alpine
Deleted: sha256:501ad78535f0...
``` ```
我们可以用镜像的完整 ID也称为 `长 ID`来删除镜像使用脚本的时候可能会用长 ID但是人工输入就太累了所以更多的时候是用 `短 ID` 来删除镜像`docker image ls` 默认列出的就已经是短 ID 一般取前3个字符以上只要足够区分于别的镜像就可以了 ### 使用镜像名删除
比如这里如果我们要删除 `redis:alpine` 镜像可以执行
```bash ```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:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7 Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23 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 ```bash
$ docker image rm centos # 查看虚悬镜像
Untagged: centos:latest $ docker images -f dangling=true
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a # 删除虚悬镜像
Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38 $ docker image prune
# 不提示确认
$ docker image prune -f
``` ```
当然更精确的是使用 `镜像摘要` 删除镜像 ### 删除所有未使用的镜像
```bash ```bash
$ docker image ls --digests # 删除所有没有被容器使用的镜像
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE $ docker image prune -a
node slim sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 6e0c4c8e3913 3 weeks ago 214 MB
$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 # 保留最近 24 小时的
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 $ 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 ```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 ```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)清理数据卷

View File

@@ -1,21 +1,124 @@
# 什么是 Docker # 什么是 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 做的事情类似无论你的应用是用 PythonJavaNode.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) ![传统虚拟化](../.gitbook/assets/virtualization.png)
### Docker 容器
Docker 容器内的应用直接运行于宿主的内核容器内没有自己的内核也没有进行硬件虚拟
![Docker](../.gitbook/assets/docker.png) ![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**2013Docker 最初基于 Linux Containers
- **libcontainer**2014v0.7Docker 自研的容器运行时
- **runC**2015v1.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 的成功推动了整个容器生态的发展催生了 KubernetesPodman 等众多相关项目笔者认为Docker 最大的贡献不仅是技术本身更是它**让容器技术从系统管理员的工具变成了每个开发者都能使用的标准工具**
## 本章小结
- Docker 是一种轻量级虚拟化技术核心价值是**环境一致性**
- 与虚拟机相比Docker 更轻量更快速资源利用率更高
- Docker 基于 Linux 内核的 NamespaceCgroups Union FS 技术
- Docker 推动了容器技术的标准化OCI和生态发展
接下来让我们了解[为什么要使用 Docker](why.md)

View File

@@ -1,40 +1,208 @@
# 为什么要使用 Docker # 为什么要使用 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 镜像包含了应用运行所需的**一切**代码运行时系统工具配置这意味着
## 对比传统虚拟机总结 - 开发环境和生产环境完全一致
- 不会再有"在我机器上能跑"的问题
- 新人入职一条命令就能启动开发环境
| 特性 | 容器 | 虚拟机 | ```bash
| :-------- | :-------- | :---------- | # 新同事入职第一天
| 启动 | 秒级 | 分钟级 | $ git clone https://github.com/company/project.git
| 硬盘使用 | 一般为 `MB` | 一般为 `GB` | $ 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 可以在几乎任何平台上运行
- 本地开发机macOSWindowsLinux
- 公有云AWSAzureGCP阿里云腾讯云
- 私有云和自建数据中心
- 边缘设备和 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)

View File

@@ -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` 等不同类型的自定义网络 │ ┌──────────────────────────────────────────────────────┐ │
│ │ docker0 网桥 │ │
### 创建一个自定义 bridge 网络 │ │ (172.17.0.1/16) │ │
│ │ ┌────────────┬────────────┬────────────┐ │ │
```bash │ │ │ │ │ │ │ │
$ docker network create my-net │ └────┼────────────┼────────────┼────────────┼──────────┘ │
│ │ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ 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 ```bash
/ # ping busybox2 $ docker network ls
PING busybox2 (172.19.0.3): 56 data bytes NETWORK ID NAME DRIVER SCOPE
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.083 ms 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 中的网络配置

View File

@@ -1,59 +1,314 @@
# 安全 # 安全
容器安全是生产环境部署的核心考量评估 Docker 的安全性时主要考虑以下几个方面 容器安全是生产环境部署的核心考量本章介绍 Docker 的安全机制和最佳实践
## 容器安全的本质
> **核心问题**容器共享宿主机内核隔离性弱于虚拟机如何在便利性和安全性之间取得平衡
```
虚拟机安全模型: 容器安全模型:
┌─────────────────┐ ┌─────────────────┐
│ Guest OS │ │ 容器进程 │
├─────────────────┤ │ (共享内核) │
│ Hypervisor │◄── 隔离边界└────────┬────────┘
├─────────────────┤ │
│ Host OS │ ┌────────┴────────┐
└─────────────────┘ │ Namespace │◄── 隔离边界
│ Cgroups │
完全隔离(性能损耗) │ Capabilities │
└─────────────────┘
进程隔离(轻量但需加固)
```
---
## 核心安全机制 ## 核心安全机制
* **内核命名空间Namespace**提供进程网络文件系统等资源的隔离 ### 1. 命名空间Namespace
* **控制组Cgroups**限制容器的 CPU内存I/O 等资源使用
* **Docker 守护进程安全**服务端的访问控制和防护
* **内核能力机制Capabilities**细粒度的权限控制
## 现代安全实践 提供进程网络文件系统等资源的隔离
### 镜像安全扫描 | Namespace | 隔离内容 | 安全作用 |
|-----------|---------|---------|
| PID | 进程 | 容器看不到其他进程 |
| NET | 网络 | 独立网络栈 |
| MNT | 文件系统 | 独立的根目录 |
| USER | 用户 | 容器 root 宿主机 root |
| IPC | 进程通信 | 隔离共享内存 |
| UTS | 主机名 | 独立主机名 |
使用工具扫描镜像中的已知漏洞 详见 [命名空间](../underly/namespace.md) 章节
* **Docker Scout**Docker 官方集成的安全扫描工具提供 SBOM 分析 ### 2. 控制组Cgroups
* **Trivy**开源的全面漏洞扫描器
* **Snyk**商业级安全平台 限制容器的资源使用防止资源耗尽攻击
```bash ```bash
# 使用 Docker Scout 扫描镜像 # 限制内存(超出会被 OOM Kill
$ docker scout cves myimage:latest $ docker run -m 512m myapp
# 使用 Trivy 扫描 # 限制 CPU
$ trivy image myimage:latest $ 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 | `CAP_NET_ADMIN` | 网络管理 | 禁用 |
USER appuser | `CAP_SYS_ADMIN` | 系统管理 | 禁用 |
``` | `CAP_SYS_PTRACE` | 进程追踪 | 禁用 |
| `CAP_CHOWN` | 更改文件所有者 | 启用 |
### 只读文件系统 | `CAP_NET_BIND_SERVICE` | 绑定低端口 | 启用 |
使用只读根文件系统增强安全性
```bash ```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 TrustDCT ---
启用镜像签名验证确保镜像来源可信 ## 镜像安全
### 使用可信镜像
```bash ```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 $ export DOCKER_CONTENT_TRUST=1
# 此后的 pull/push 会验证签名
$ docker pull myregistry/myimage:latest $ 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 安全配置

View File

@@ -1,51 +1,137 @@
# 基本架构 # 基本架构
Docker 采用了 `C/S`客户端/服务端架构包括客户端和服务端Docker 守护进程`Daemon`作为服务端接受来自客户端的请求并处理这些请求创建运行分发容器 ## 核心架构图
客户端和服务端既可以运行在一个机器上也可通过 `socket` 或者 `RESTful API` 来进行通信 Docker 采用了 **C/S (客户端/服务端)** 架构Client Daemon 发送请求Daemon 负责构建运行和分发容器
![Docker 基本架构](../.gitbook/assets/docker_arch.png)
## 核心组件
Docker 的核心组件形成了一个层次化的架构
``` ```
┌─────────────────────────────────────────────────┐ ┌───────────────┐ ┌────────────────────────────────────┐
Docker CLI 客户端 Docker Host
(docker 命令行工具) (Docker CLI) │
├─────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌────────────┐ │
dockerd $ docker run ├────►│ dockerd │ Containers │
(Docker 守护进程/引擎) $ docker pull│ (Daemon) │─────►│
─────────────────────────────────────────────────┤ │ $ docker ps │ │ └─────────┘ │ □ □ │ │
│ containerd └───────────────┘ │ └────────────┘
(容器生命周期管理器) │ ┌────────────┐
├─────────────────────────────────────────────────┤ │ └───────────►│ Images │ │
runc
(OCI 容器运行时) └────────────────────┴────────────┘
└─────────────────────────────────────────────────┘
``` ```
* **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 架构 ## 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)镜像如何存储

View File

@@ -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% CPU1 个核心)
```
---
## 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)监控容器资源

View File

@@ -1,23 +1,267 @@
# 命名空间 # 命名空间
命名空间是 Linux 内核一个强大的特性每个容器都有自己单独的命名空间运行在其中的应用都像是在独立的操作系统中运行一样命名空间保证了容器之间彼此互不影响 ## 什么是 Namespace
## pid 命名空间 > **Namespace Linux 内核提供的资源隔离机制它让容器内的进程仿佛运行在独立的操作系统中**
不同用户的进程就是通过 pid 命名空间隔离开的且不同命名空间中可以有相同 pid所有的 LXC 进程在 Docker 中的父进程为 Docker 进程每个 LXC 进程具有不同的命名空间同时由于允许嵌套因此可以很方便的实现嵌套的 Docker 容器
## net 命名空间 Namespace 是容器技术的核心基础之一它回答了一个关键问题**如何让一个进程"以为"自己独占整个系统**
有了 pid 命名空间每个命名空间中的 pid 能够相互隔离但是网络端口还是共享 host 的端口网络隔离是通过 net 命名空间实现的 每个 net 命名空间有独立的 网络设备IP 地址路由表/proc/net 目录这样每个容器的网络就能隔离开来Docker 默认采用 veth 的方式将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docker0 连接在一起
## 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 命名空间 ## Namespace 的类型
类似 chroot将一个进程放到一个特定的目录执行mnt 命名空间允许不同命名空间的进程看到的文件结构不同这样每个命名空间 中的进程所看到的文件目录就被隔离开了 chroot 不同每个命名空间中的容器在 /proc/mounts 的信息只包含所在命名空间的 mount point
## uts 命名空间 Linux 内核提供了以下几种 NamespaceDocker 容器使用了全部
UTS("UNIX Time-sharing System") 命名空间允许每个容器拥有独立的 hostname domain name 使其在网络上可以被视作一个独立的节点而非 主机上的一个进程
## user 命名空间 | Namespace | 隔离内容 | 容器中的效果 |
每个容器可以有不同的用户和组 id 也就是说可以在容器内用容器内部的用户执行程序而非主机上的用户 |-----------|---------|-------------|
| **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
### 实验 1UTS Namespace
```bash
# 创建新的 UTS namespace 并启动 shell
$ sudo unshare --uts /bin/bash
# 修改主机名(只影响这个 namespace
$ hostname container-test
$ hostname
container-test
# 退出后查看宿主机主机名(未改变)
$ exit
$ hostname
my-server
```
### 实验 2PID 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
```
### 实验 3NET Namespace
```bash
# 创建新的网络 namespace
$ sudo unshare --net /bin/bash
# 查看网络接口(只有 lo
$ ip addr
1: lo: <LOOPBACK> 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 部分内容仍可见时间无法隔离 |
| **非虚拟化** | 比虚拟机隔离性弱 |
> 需要更强隔离时可考虑 gVisorKata 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)

View File

@@ -1,22 +1,230 @@
# 联合文件系统 # 联合文件系统
联合文件系统[UnionFS](https://en.wikipedia.org/wiki/UnionFS))是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。 ## 什么是联合文件系统
联合文件系统 Docker 镜像的基础镜像可以通过分层来进行继承基于基础镜像没有父镜像可以制作各种具体的应用镜像 联合文件系统UnionFS是一种**分层轻量级**的文件系统它将多个目录"联合"挂载到同一个虚拟目录形成一个统一的文件系统视图
另外不同 Docker 容器就可以共享一些基础的文件系统层同时再加上自己独有的改动层大大提高了存储的效率 > **核心思想**将多个只读层叠加最上层可写形成完整的文件系统
Docker 中使用的 AUFSAdvanced 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 推荐使用的存储驱动 | ### 1. 镜像分层复用
| :-- | :-- |
|Docker on Ubuntu | `overlay2` (16.04 +) |
|Docker on Debian | `overlay2` (Debian Stretch), `aufs`, `devicemapper` |
|Docker on CentOS | `overlay2` |
|Docker on Fedora | `overlay2` |
在可能的情况下[推荐](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
<missing> 2 weeks ago STOPSIGNAL SIGQUIT 0B
<missing> 2 weeks ago EXPOSE map[80/tcp:{}] 0B
<missing> 2 weeks ago ENTRYPOINT ["/docker-entrypoint.sh"] 0B
<missing> 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 层的创建