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

@@ -1,32 +1,221 @@
# ADD 更高级的复制文件
`ADD` 指令和 `COPY` 的格式和性质基本一致但是在 `COPY` 基础上增加了一些功能
## 基本语法
比如 `<源路径>` 可以是一个 `URL`这种情况下Docker 引擎会试图去下载这个链接的文件放到 `<目标路径>` 下载后的文件权限自动设置为 `600`如果这并不是想要的权限那么还需要增加额外的一层 `RUN` 进行权限调整另外如果下载的是个压缩包需要解压缩也一样还需要额外的一层 `RUN` 指令进行解压缩所以不如直接使用 `RUN` 指令然后使用 `wget` 或者 `curl` 工具下载处理权限解压缩然后清理无用文件更合理因此这个功能其实并不实用而且不推荐使用
```docker
ADD [选项] <源路径>... <目标路径>
ADD [选项] ["<源路径>", ... "<目标路径>"]
```
如果 `<源路径>` 为一个 `tar` 压缩文件的话压缩格式为 `gzip`, `bzip2` 以及 `xz` 的情况下`ADD` 指令将会自动解压缩这个压缩文件到 `<目标路径>`
`ADD` `COPY` 基础上增加了两个功能
1. 自动解压 tar 压缩包
2. 支持从 URL 下载文件不推荐
在某些情况下这个自动解压缩的功能非常有用比如官方镜像 `ubuntu`
---
## ADD vs COPY
| 特性 | COPY | ADD |
|------|------|-----|
| 复制本地文件 | | |
| 自动解压 tar | | |
| 支持 URL | | 不推荐 |
| 行为可预测性 | | |
| 推荐程度 | **优先使用** | 仅解压场景 |
> 笔者建议除非需要自动解压 tar 文件否则始终使用 COPY明确的行为比隐式的魔法更好
---
## 自动解压功能
### 基本用法
```docker
# 自动解压 tar.gz 到目标目录
ADD app.tar.gz /app/
```
ADD 会识别并解压以下格式
- `.tar`
- `.tar.gz` / `.tgz`
- `.tar.bz2` / `.tbz2`
- `.tar.xz` / `.txz`
### 实际应用
官方基础镜像通常使用 ADD 解压根文件系统
```docker
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...
ADD ubuntu-noble-core-cloudimg-amd64-root.tar.gz /
```
但在某些情况下如果我们真的是希望复制个压缩文件进去而不解压缩这时就不可以使用 `ADD` 命令了
### 解压过程
Docker 官方的 [Dockerfile 最佳实践文档](../../appendix/best_practices.md) 中要求尽可能的使用 `COPY`因为 `COPY` 的语义很明确就是复制文件而已 `ADD` 则包含了更复杂的功能其行为也不一定很清晰最适合使用 `ADD` 的场合就是所提及的需要自动解压缩的场合
```
ADD app.tar.gz /app/
├─ 识别 .tar.gz 格式
├─ 自动解压
└─ 内容放入 /app/
另外需要注意的是`ADD` 指令会令镜像构建缓存失效从而可能会令镜像构建变得比较缓慢
app.tar.gz 包含: /app/ 目录结果:
├── src/ ├── src/
│ └── main.py │ └── main.py
└── config.json └── config.json
```
因此在 `COPY` `ADD` 指令中选择的时候可以遵循这样的原则所有的文件复制均使用 `COPY` 指令仅在需要自动解压缩的场合使用 `ADD`
---
在使用该指令的时候还可以加上 `--chown=<user>:<group>` 选项来改变文件的所属用户及所属组
## URL 下载功能不推荐
### 基本用法
```docker
ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/
# 从 URL 下载文件
ADD https://example.com/app.zip /app/app.zip
```
### 为什么不推荐
| 问题 | 说明 |
|------|------|
| 权限固定 | 下载的文件权限为 600通常需要额外 RUN 修改 |
| 不会解压 | URL 下载的压缩包不会自动解压 |
| 缓存问题 | URL 内容变化时不会重新下载 |
| 层数增加 | 需要额外 RUN 清理 |
### 推荐替代方案
```docker
# ❌ 不推荐:使用 ADD 下载
ADD https://example.com/app.tar.gz /tmp/
RUN tar -xzf /tmp/app.tar.gz -C /app && rm /tmp/app.tar.gz
# ✅ 推荐:使用 RUN + curl
RUN curl -fsSL https://example.com/app.tar.gz | tar -xz -C /app
```
优势
- 一条 RUN 完成下载解压清理
- 减少镜像层数
- 更清晰的构建意图
---
## 修改文件所有者
```docker
ADD --chown=node:node app.tar.gz /app/
ADD --chown=1000:1000 files/ /app/
```
---
## 何时使用 ADD
### 适合使用 ADD
```docker
# 解压本地 tar 文件
FROM scratch
ADD rootfs.tar.gz /
# 解压应用包
ADD dist.tar.gz /app/
```
### 不适合使用 ADD
```docker
# 复制普通文件(用 COPY
ADD package.json /app/ # ❌
COPY package.json /app/ # ✅
# 下载文件(用 RUN + curl
ADD https://example.com/file / # ❌
RUN curl -fsSL ... -o /file # ✅
# 需要保留 tar 不解压(用 COPY
ADD archive.tar.gz /archives/ # ❌ 会解压
COPY archive.tar.gz /archives/ # ✅ 保持原样
```
---
## 缓存行为
ADD 可能导致构建缓存失效
```docker
# 如果 app.tar.gz 内容变化,此层及后续层都需重建
ADD app.tar.gz /app/
RUN npm install
```
**优化建议**
```docker
# 先复制依赖文件
COPY package*.json /app/
RUN npm install
# 再添加应用代码
ADD app.tar.gz /app/
```
---
## 最佳实践
### 1. 默认使用 COPY
```docker
# ✅ 大多数场景使用 COPY
COPY . /app/
```
### 2. 仅在需要解压时使用 ADD
```docker
# ✅ 自动解压场景
ADD app.tar.gz /app/
```
### 3. 不要用 ADD 下载文件
```docker
# ❌ 避免
ADD https://example.com/file.tar.gz /tmp/
# ✅ 推荐
RUN curl -fsSL https://example.com/file.tar.gz | tar -xz -C /app
```
### 4. 解压后清理
```docker
# 如果需要控制解压过程
COPY app.tar.gz /tmp/
RUN tar -xzf /tmp/app.tar.gz -C /app && \
rm /tmp/app.tar.gz
```
---
## 本章小结
| 场景 | 推荐指令 |
|------|---------|
| 复制普通文件 | `COPY` |
| 复制目录 | `COPY` |
| 自动解压 tar | `ADD` |
| URL 下载 | `RUN curl` |
| 保持 tar 不解压 | `COPY` |
## 延伸阅读
- [COPY 复制文件](copy.md)基本复制操作
- [多阶段构建](../multistage-builds.md)减少镜像体积
- [最佳实践](../../appendix/best_practices.md)Dockerfile 编写指南

View File

@@ -1,68 +1,238 @@
# ARG 构建参数
格式`ARG <参数名>[=<默认值>]`
构建参数和 `ENV` 的效果一样都是设置环境变量所不同的是`ARG` 所设置的构建环境的环境变量在将来容器运行时是不会存在这些环境变量的但是不要因此就使用 `ARG` 保存密码之类的信息因为 `docker history` 还是可以看到所有值的
`Dockerfile` 中的 `ARG` 指令是定义参数名称以及定义其默认值该默认值可以在构建命令 `docker build` 中用 `--build-arg <参数名>=<值>` 来覆盖
灵活的使用 `ARG` 指令能够在不修改 Dockerfile 的情况下构建出不同的镜像
ARG 指令有生效范围如果在 `FROM` 指令之前指定那么只能用于 `FROM` 指令中
## 基本语法
```docker
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo ${DOCKER_USERNAME}
ARG <参数名>[=<默认值>]
```
使用上述 Dockerfile 会发现无法输出 `${DOCKER_USERNAME}` 变量的值要想正常输出你必须在 `FROM` 之后再次指定 `ARG`
`ARG` 指令定义构建时的变量可以在 `docker build` 时通过 `--build-arg` 传入
---
## ARG vs ENV
| 特性 | ARG | ENV |
|------|-----|-----|
| **生效时间** | 仅构建时 | 构建时 + 运行时 |
| **持久性** | 构建后消失 | 写入镜像 |
| **覆盖方式** | `docker build --build-arg` | `docker run -e` |
| **适用场景** | 构建参数版本号等 | 应用配置 |
| **可见性** | `docker history` 可见 | `docker inspect` 可见 |
```
构建时 运行时
├─ ARG VERSION=1.0 │ ARG 已消失)
├─ ENV APP_ENV=prod │ APP_ENV=prod仍存在
└─ RUN echo $VERSION │
```
> **安全提示**不要用 ARG 传递密码等敏感信息`docker history` 可以查看所有 ARG
---
## 基本用法
### 定义和使用
```docker
# 只在 FROM 中生效
ARG DOCKER_USERNAME=library
# 定义有默认值的 ARG
ARG NODE_VERSION=20
FROM ${DOCKER_USERNAME}/alpine
# 要想在 FROM 之后使用,必须再次指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
# 使用 ARG
FROM node:${NODE_VERSION}-alpine
RUN echo "Using Node.js $NODE_VERSION"
```
对于多阶段构建尤其要注意这个问题
### 构建时覆盖
```bash
# 使用默认值
$ docker build -t myapp .
# 覆盖默认值
$ docker build --build-arg NODE_VERSION=18 -t myapp .
```
---
## ARG 的作用域
### FROM 之前的 ARG
```docker
# 这个变量在每个 FROM 中都生效
ARG DOCKER_USERNAME=library
# FROM 之前的 ARG 只能用于 FROM 指令
ARG REGISTRY=docker.io
ARG IMAGE_NAME=node
FROM ${DOCKER_USERNAME}/alpine
FROM ${REGISTRY}/${IMAGE_NAME}:20
RUN set -x ; echo 1
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo 2
# ❌ 这里无法使用上面的 ARG
RUN echo $REGISTRY # 输出空
```
对于上述 Dockerfile 两个 `FROM` 指令都可以使用 `${DOCKER_USERNAME}`对于在各个阶段中使用的变量都必须在每个阶段分别指定
### FROM 之后重新声明
```docker
ARG DOCKER_USERNAME=library
ARG NODE_VERSION=20
FROM ${DOCKER_USERNAME}/alpine
FROM node:${NODE_VERSION}-alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
FROM ${DOCKER_USERNAME}/alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
# 需要再次声明才能使用
ARG NODE_VERSION
RUN echo "Node version: $NODE_VERSION"
```
### 多阶段构建中的 ARG
```docker
ARG BASE_VERSION=alpine
FROM node:20-${BASE_VERSION} AS builder
# 需要重新声明
ARG NODE_VERSION=20
RUN echo "Building with Node $NODE_VERSION"
FROM node:20-${BASE_VERSION}
# 每个阶段都需要重新声明
ARG NODE_VERSION=20
RUN echo "Running with Node $NODE_VERSION"
```
---
## 常见使用场景
### 1. 控制基础镜像版本
```docker
ARG ALPINE_VERSION=3.19
FROM alpine:${ALPINE_VERSION}
```
```bash
$ docker build --build-arg ALPINE_VERSION=3.18 .
```
### 2. 设置软件版本
```docker
ARG NGINX_VERSION=1.25.0
RUN curl -fsSL https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz | tar -xz
```
### 3. 配置构建环境
```docker
ARG BUILD_ENV=production
ARG ENABLE_DEBUG=false
RUN if [ "$ENABLE_DEBUG" = "true" ]; then \
npm install --include=dev; \
else \
npm install --production; \
fi
```
### 4. 配置私有仓库
```docker
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc && \
npm install && \
rm ~/.npmrc
```
```bash
# 构建时传入 token
$ docker build --build-arg NPM_TOKEN=xxx .
```
---
## ARG 传递给 ENV
如果需要在运行时使用 ARG 的值
```docker
ARG VERSION=1.0.0
# 将 ARG 传递给 ENV
ENV APP_VERSION=$VERSION
# 运行时可用
CMD echo "App version: $APP_VERSION"
```
---
## 预定义 ARG
Docker 提供了一些预定义的 ARG无需声明即可使用
| ARG | 说明 |
|-----|------|
| `HTTP_PROXY` | HTTP 代理 |
| `HTTPS_PROXY` | HTTPS 代理 |
| `NO_PROXY` | 不使用代理的地址 |
| `FTP_PROXY` | FTP 代理 |
```bash
# 构建时使用代理
$ docker build --build-arg HTTP_PROXY=http://proxy:8080 .
```
---
## 最佳实践
### 1. ARG 提供合理默认值
```docker
# ✅ 好:有默认值
ARG NODE_VERSION=20
# ⚠️ 需要每次传入
ARG NODE_VERSION
```
### 2. 不要用 ARG 存储敏感信息
```docker
# ❌ 错误:密码会被记录在镜像历史中
ARG DB_PASSWORD
RUN echo "password=$DB_PASSWORD" > /app/.env
# ✅ 正确:使用 secrets 或运行时环境变量
```
### 3. 使用 ARG 提高构建灵活性
```docker
ARG BASE_IMAGE=python:3.12-slim
FROM ${BASE_IMAGE}
# 可以构建不同基础镜像的版本
# docker build --build-arg BASE_IMAGE=python:3.11-alpine .
```
---
## 本章小结
| 要点 | 说明 |
|------|------|
| **作用** | 定义构建时变量 |
| **语法** | `ARG NAME=value` |
| **覆盖** | `docker build --build-arg NAME=value` |
| **作用域** | FROM 之后需要重新声明 |
| **vs ENV** | ARG 仅构建时ENV 构建+运行时 |
| **安全** | 不要存储敏感信息 |
## 延伸阅读
- [ENV 设置环境变量](env.md)运行时环境变量
- [FROM 指令](../../image/build.md)基础镜像指定
- [多阶段构建](../multistage-builds.md)复杂构建场景

View File

@@ -1,49 +1,268 @@
# CMD 容器启动命令
`CMD` 指令的格式和 `RUN` 相似也是两种格式
## 什么是 CMD
* `shell` 格式`CMD <命令>`
* `exec` 格式`CMD ["可执行文件", "参数1", "参数2"...]`
* 参数列表格式`CMD ["参数1", "参数2"...]`在指定了 `ENTRYPOINT` 指令后 `CMD` 指定具体的参数
`CMD` 指令用于指定容器启动时默认执行的命令它定义了容器的"主进程"
之前介绍容器的时候曾经说过Docker 不是虚拟机容器就是进程既然是进程那么在启动容器的时候需要指定所运行的程序及参数`CMD` 指令就是用于指定默认的容器主进程的启动命令的
> **核心概念**容器的生命周期 = 主进程的生命周期CMD 指定的命令就是这个主进程
在运行时可以指定新的命令来替代镜像设置中的这个默认命令比如`ubuntu` 镜像默认的 `CMD` `/bin/bash`如果我们直接 `docker run -it ubuntu` 的话会直接进入 `bash`我们也可以在运行时指定运行别的命令 `docker run -it ubuntu cat /etc/os-release`这就是用 `cat /etc/os-release` 命令替换了默认的 `/bin/bash` 命令了输出了系统版本信息
---
在指令格式上一般推荐使用 `exec` 格式这类格式在解析时会被解析为 JSON 数组因此一定要使用双引号 `"`而不要使用单引号
## 语法格式
如果使用 `shell` 格式的话实际的命令会被包装为 `sh -c` 的参数的形式进行执行比如
CMD 有三种格式
```docker
CMD echo $HOME
```
| 格式 | 语法 | 推荐程度 |
|------|------|---------|
| **exec 格式** | `CMD ["可执行文件", "参数1", "参数2"]` | **推荐** |
| **shell 格式** | `CMD 命令 参数1 参数2` | 简单场景 |
| **参数格式** | `CMD ["参数1", "参数2"]` | 配合 ENTRYPOINT |
在实际执行中会将其变更为
```docker
CMD [ "sh", "-c", "echo $HOME" ]
```
这就是为什么我们可以使用环境变量的原因因为这些环境变量会被 shell 进行解析处理
提到 `CMD` 就不得不提容器中应用在前台执行和后台执行的问题这是初学者常出现的一个混淆
Docker 不是虚拟机容器中的应用都应该以前台执行而不是像虚拟机物理机里面那样 `systemd` 去启动后台服务容器内没有后台服务的概念
一些初学者将 `CMD` 写为
```docker
CMD service nginx start
```
然后发现容器执行后就立即退出了甚至在容器内去使用 `systemctl` 命令结果却发现根本执行不了这就是因为没有搞明白前台后台的概念没有区分容器和虚拟机的差异依旧在以传统虚拟机的角度去理解容器
对于容器而言其启动程序就是容器应用进程容器就是为了主进程而存在的主进程退出容器就失去了存在的意义从而退出其它辅助进程不是它需要关心的东西
而使用 `service nginx start` 命令则是希望 init 系统以后台守护进程的形式启动 nginx 服务而刚才说了 `CMD service nginx start` 会被理解为 `CMD [ "sh", "-c", "service nginx start"]`因此主进程实际上是 `sh`那么当 `service nginx start` 命令结束后`sh` 也就结束了`sh` 作为主进程退出了自然就会令容器退出
正确的做法是直接执行 `nginx` 可执行文件并且要求以前台形式运行比如
### exec 格式推荐
```docker
CMD ["nginx", "-g", "daemon off;"]
CMD ["python", "app.py"]
CMD ["node", "server.js"]
```
**优点**
- 直接执行指定程序是容器的 PID 1
- 正确接收信号 SIGTERM
- 无需 shell 解析
### shell 格式
```docker
CMD echo "Hello World"
CMD nginx -g "daemon off;"
```
**实际执行**会被包装为 `sh -c`
```docker
# 你写的
CMD echo $HOME
# 实际执行的
CMD ["sh", "-c", "echo $HOME"]
```
**优点**可以使用环境变量管道等 shell 特性
**缺点**主进程是 sh信号无法正确传递给应用
---
## exec 格式 vs shell 格式
| 特性 | exec 格式 | shell 格式 |
|------|----------|-----------|
| 主进程 | 指定的程序 | `/bin/sh` |
| 信号传递 | 正确 | 无法传递 |
| 环境变量 | 需要 shell 包装 | 自动解析 |
| 推荐使用 | 大多数场景 | 需要 shell 特性时 |
### 信号传递问题示例
```docker
# ❌ shell 格式docker stop 会超时
CMD node server.js
# 实际是 sh -c "node server.js"
# SIGTERM 发给 sh不会传递给 node
# ✅ exec 格式docker stop 正常工作
CMD ["node", "server.js"]
# SIGTERM 直接发给 node
```
---
## 运行时覆盖 CMD
`docker run` 后的命令会覆盖 Dockerfile 中的 CMD
```bash
# ubuntu 默认 CMD 是 /bin/bash
$ docker run -it ubuntu # 进入 bash
$ docker run ubuntu cat /etc/os-release # 覆盖为 cat 命令
```
```
Dockerfile: docker run 命令:
CMD ["/bin/bash"] + cat /etc/os-release
│ │
└───────► 被覆盖 ◄───────┘
执行: cat /etc/os-release
```
---
## 经典错误容器立即退出
### 错误示例
```docker
# ❌ 容器启动后立即退出
CMD service nginx start
```
### 原因分析
```
1. CMD service nginx start
↓ 被转换为
2. CMD ["sh", "-c", "service nginx start"]
3. sh 启动,执行 service 命令
4. service 命令将 nginx 放到后台
5. service 命令结束sh 退出
6. 容器主进程sh退出 → 容器停止
```
### 正确做法
```docker
# ✅ 让 nginx 在前台运行
CMD ["nginx", "-g", "daemon off;"]
```
---
## CMD vs ENTRYPOINT
| 指令 | 用途 | 运行时行为 |
|------|------|-----------|
| **CMD** | 默认命令 | `docker run` 参数会**覆盖** |
| **ENTRYPOINT** | 入口点 | `docker run` 参数会**追加**到它后面 |
### 单独使用 CMD
```docker
# Dockerfile
CMD ["curl", "-s", "http://example.com"]
```
```bash
$ docker run myimage # 执行默认命令
$ docker run myimage curl -v ... # 完全覆盖
```
### 搭配 ENTRYPOINT
```docker
# Dockerfile
ENTRYPOINT ["curl", "-s"]
CMD ["http://example.com"]
```
```bash
$ docker run myimage # curl -s http://example.com
$ docker run myimage http://other.com # curl -s http://other.com参数覆盖
```
详见 [ENTRYPOINT 入口点](entrypoint.md) 章节
---
## 最佳实践
### 1. 优先使用 exec 格式
```docker
# ✅ 推荐
CMD ["python", "app.py"]
# ⚠️ 仅在需要 shell 特性时使用
CMD ["sh", "-c", "echo $PATH && python app.py"]
```
### 2. 确保应用在前台运行
```docker
# ✅ 前台运行
CMD ["nginx", "-g", "daemon off;"]
CMD ["apache2ctl", "-D", "FOREGROUND"]
CMD ["java", "-jar", "app.jar"]
# ❌ 不要使用后台服务命令
CMD service nginx start
CMD systemctl start nginx
```
### 3. 使用双引号
```docker
# ✅ 正确:双引号
CMD ["node", "server.js"]
# ❌ 错误单引号JSON 不支持)
CMD ['node', 'server.js']
```
### 4. 配合 ENTRYPOINT 使用
```docker
# 用于可配置参数的场景
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
# 运行时可以覆盖端口
$ docker run myapp --port 9000
```
---
## 常见问题
### Q: CMD 可以写多个吗
不可以多个 CMD 只有最后一个生效
```docker
CMD ["echo", "first"]
CMD ["echo", "second"] # 只有这个生效
```
### Q: 如何在 CMD 中使用环境变量
```docker
# 方法1使用 shell 格式
CMD echo "Port is $PORT"
# 方法2显式使用 sh -c
CMD ["sh", "-c", "echo Port is $PORT"]
```
### Q: 为什么我的容器不响应 Ctrl+C
可能是使用了 shell 格式信号被 sh 吃掉了
```docker
# ❌ 信号无法传递
CMD python app.py
# ✅ 信号正确传递
CMD ["python", "app.py"]
```
---
## 本章小结
| 要点 | 说明 |
|------|------|
| **作用** | 指定容器启动时的默认命令 |
| **推荐格式** | exec 格式 `CMD ["程序", "参数"]` |
| **覆盖方式** | `docker run image 新命令` |
| ** ENTRYPOINT** | CMD 作为 ENTRYPOINT 的默认参数 |
| **核心原则** | 应用必须在前台运行 |
## 延伸阅读
- [ENTRYPOINT 入口点](entrypoint.md)固定的启动命令
- [后台运行](../../container/daemon.md)容器前台/后台概念
- [最佳实践](../../appendix/best_practices.md)Dockerfile 编写指南

View File

@@ -1,46 +1,261 @@
# COPY 复制文件
格式
* `COPY [--chown=<user>:<group>] <源路径>... <目标路径>`
* `COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]`
`RUN` 指令一样也有两种格式一种类似于命令行一种类似于函数调用
`COPY` 指令将从构建上下文目录中 `<源路径>` 的文件/目录复制到新的一层的镜像内的 `<目标路径>` 位置比如
## 基本语法
```docker
COPY package.json /usr/src/app/
COPY [选项] <源路径>... <目标路径>
COPY [选项] ["<源路径1>", "<源路径2>", ... "<目标路径>"]
```
`<源路径>` 可以是多个甚至可以是通配符其通配符规则要满足 Go [`filepath.Match`](https://golang.org/pkg/path/filepath/#Match) 规则,如:
`COPY` 指令将构建上下文中的文件或目录复制到镜像内
---
## 基本用法
### 复制单个文件
```docker
COPY hom* /mydir/
COPY hom?.txt /mydir/
# 复制文件到指定目录
COPY package.json /app/
# 复制文件并重命名
COPY config.json /app/settings.json
```
`<目标路径>` 可以是容器内的绝对路径也可以是相对于工作目录的相对路径工作目录可以用 `WORKDIR` 指令来指定目标路径不需要事先创建如果目录不存在会在复制文件前先行创建缺失目录
此外还需要注意一点使用 `COPY` 指令源文件的各种元数据都会保留比如读执行权限文件变更时间等这个特性对于镜像定制很有用特别是构建相关文件都在使用 Git 进行管理的时候
在使用该指令的时候还可以加上 `--chown=<user>:<group>` 选项来改变文件的所属用户及所属组
### 复制多个文件
```docker
COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/
# 复制多个指定文件
COPY package.json package-lock.json /app/
# 使用通配符
COPY *.json /app/
COPY src/*.js /app/src/
```
如果源路径为文件夹复制的时候不是直接复制该文件夹而是将文件夹中的内容复制到目标路径
## 使用 `--link` 优化多阶段构建
BuildKit 可以使用 `--link` 选项来优化多阶段构建的性能使用 `--link` 文件会以独立层的形式添加无需依赖前序指令的结果
### 复制目录
```docker
# 复制整个目录的内容(不是目录本身)
COPY src/ /app/src/
```
> **注意**复制目录时复制的是目录的**内容**不包含目录本身
```
构建上下文: 镜像内:
src/ /app/src/
├── index.js → ├── index.js
└── utils.js └── utils.js
```
---
## 通配符规则
COPY 支持 Go `filepath.Match` 通配符规则
| 通配符 | 说明 | 示例 |
|--------|------|------|
| `*` | 匹配任意字符序列 | `*.json` |
| `?` | 匹配单个字符 | `config?.json` |
| `[abc]` | 匹配括号内任一字符 | `[abc].txt` |
| `[a-z]` | 匹配范围内字符 | `file[0-9].txt` |
```docker
COPY hom* /mydir/ # home.txt, homework.md 等
COPY hom?.txt /mydir/ # home.txt, homy.txt 等
COPY app[0-9].js /app/ # app0.js ~ app9.js
```
---
## 目标路径
### 绝对路径
```docker
COPY app.js /usr/src/app/
```
### 相对路径基于 WORKDIR
```docker
WORKDIR /app
COPY package.json ./ # 复制到 /app/package.json
COPY src/ ./src/ # 复制到 /app/src/
```
### 自动创建目录
如果目标目录不存在Docker 会自动创建
```docker
# /app/config/ 不存在也会自动创建
COPY settings.json /app/config/
```
---
## 修改文件所有者
使用 `--chown` 选项设置文件的用户和组
```docker
# 使用用户名和组名
COPY --chown=node:node package.json /app/
# 使用 UID 和 GID
COPY --chown=1000:1000 . /app/
# 只指定用户
COPY --chown=node . /app/
```
> 💡 结合 `USER` 指令使用确保应用以非 root 用户运行
---
## 保留文件元数据
COPY 会保留源文件的元数据
- 执行权限
- 修改时间
这对于脚本文件特别重要
```docker
# start.sh 的可执行权限会被保留
COPY start.sh /app/
```
---
## COPY vs ADD
| 特性 | COPY | ADD |
|------|------|-----|
| 复制本地文件 | | |
| 自动解压 tar | | |
| 支持 URL | | 不推荐 |
| 推荐程度 | **推荐** | 特殊场景使用 |
```docker
# 推荐:使用 COPY
COPY app.tar.gz /app/
RUN tar -xzf /app/app.tar.gz
# ADD 会自动解压(行为不明显,不推荐)
ADD app.tar.gz /app/
```
> 笔者建议除非需要自动解压 tar 文件否则始终使用 COPY明确的行为比隐式的魔法更好
---
## 多阶段构建中的 COPY
### 从其他构建阶段复制
```docker
# 构建阶段
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 生产阶段
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
```
### 使用 --link 优化缓存BuildKit
```docker
# 使用 --link 后,文件以独立层添加,不依赖前序指令
COPY --link --from=builder /app/dist /usr/share/nginx/html
```
这样可以更高效地利用缓存加速构建过程
`--link` 的优势
- 更高效利用构建缓存
- 并行化构建过程
- 加速多阶段构建
---
## .dockerignore
使用 `.dockerignore` 排除不需要复制的文件
```gitignore
# .dockerignore
node_modules
.git
.env
*.log
Dockerfile
.dockerignore
```
这可以
- 减小构建上下文大小
- 加速构建
- 避免复制敏感文件
---
## 最佳实践
### 1. 利用缓存先复制依赖文件
```docker
# ✅ 好:先复制依赖定义,再安装,最后复制代码
COPY package.json package-lock.json ./
RUN npm install
COPY . .
# ❌ 差:一次性复制所有文件,代码变更会导致重新 npm install
COPY . .
RUN npm install
```
### 2. 使用 .dockerignore
```docker
# 确保 node_modules 不被复制
COPY . .
# .dockerignore 中应包含 node_modules
```
### 3. 明确复制路径
```docker
# ✅ 好:明确的路径
COPY src/ /app/src/
COPY package.json /app/
# ❌ 差:过于宽泛
COPY . .
```
---
## 本章小结
| 操作 | 示例 |
|------|------|
| 复制文件 | `COPY app.js /app/` |
| 复制多个文件 | `COPY *.json /app/` |
| 复制目录内容 | `COPY src/ /app/src/` |
| 修改所有者 | `COPY --chown=node:node . /app/` |
| 从构建阶段复制 | `COPY --from=builder /app/dist ./` |
## 延伸阅读
- [ADD 指令](add.md)复制和解压
- [WORKDIR 指令](workdir.md)设置工作目录
- [多阶段构建](../multistage-builds.md)优化镜像大小
- [最佳实践](../../appendix/best_practices.md)Dockerfile 编写指南

View File

@@ -1,124 +1,306 @@
# ENTRYPOINT 入口点
`ENTRYPOINT` 的格式和 `RUN` 指令格式一样分为 `exec` 格式和 `shell` 格式
## 什么是 ENTRYPOINT
`ENTRYPOINT` 的目的和 `CMD` 一样都是在指定容器启动程序及参数`ENTRYPOINT` 在运行时也可以替代不过比 `CMD` 要略显繁琐需要通过 `docker run` 的参数 `--entrypoint` 来指定
`ENTRYPOINT` 指定容器启动时运行的入口程序 CMD 不同ENTRYPOINT 定义的命令不会被 `docker run` 的参数覆盖而是**接收这些参数**
当指定了 `ENTRYPOINT` `CMD` 的含义就发生了改变不再是直接的运行其命令而是将 `CMD` 的内容作为参数传给 `ENTRYPOINT` 指令换句话说实际执行时将变为
> **核心作用**让镜像像一个可执行程序一样使用`docker run` 的参数作为这个程序的参数
```bash
<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
FROM ubuntu:24.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://myip.ipip.net" ]
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
CMD ["curl", "-s", "http://myip.ipip.net"]
```
假如我们使用 `docker build -t myip .` 来构建镜像的话如果我们需要查询当前公网 IP只需要执行
```bash
$ docker run myip
当前 IP61.148.226.66 来自:北京市 联通
$ docker run myip # ✓ 正常工作
当前 IP61.148.226.66
$ docker run myip -i # ✗ 错误!
exec: "-i": executable file not found
# -i 替换了整个 CMD被当作可执行文件
```
这么看起来好像可以直接把镜像当做命令使用了不过命令总有参数如果我们希望加参数呢比如从上面的 `CMD` 中可以看到实质的命令是 `curl`那么如果我们希望显示 HTTP 头信息就需要加上 `-i` 参数那么我们可以直接加 `-i` 参数给 `docker run myip`
```bash
$ docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".
```
我们可以看到可执行文件找不到的报错`executable file not found`之前我们说过跟在镜像名后面的是 `command`运行时会替换 `CMD` 的默认值因此这里的 `-i` 替换了原来的 `CMD`而不是添加在原来的 `curl -s http://myip.ipip.net` 后面 `-i` 根本不是命令所以自然找不到
那么如果我们希望加入 `-i` 这参数我们就必须重新完整的输入这个命令
```bash
$ docker run myip curl -s http://myip.ipip.net -i
```
这显然不是很好的解决方案而使用 `ENTRYPOINT` 就可以解决这个问题现在我们重新用 `ENTRYPOINT` 来实现这个镜像
### 使用 ENTRYPOINT 解决
```docker
FROM ubuntu:24.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"]
```
这次我们再来尝试直接使用 `docker run myip -i`
```bash
$ docker run myip
当前 IP61.148.226.66 来自:北京市 联通
$ docker run myip # ✓ 正常工作
当前 IP61.148.226.66
$ docker run myip -i
$ docker run myip -i # ✓ 添加 -i 参数
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive
当前 IP61.148.226.66 来自:北京市 联通
...
当前 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
FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
FROM redis:7-alpine
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 6379
CMD [ "redis-server" ]
CMD ["redis-server"]
```
可以看到其中为了 redis 服务创建了 redis 用户并在最后指定了 `ENTRYPOINT` `docker-entrypoint.sh` 脚本
**docker-entrypoint.sh**
```bash
#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "$@"
set -e
# 准备工作
echo "Initializing..."
# 如果第一个参数是 redis-server以 redis 用户运行
if [ "$1" = 'redis-server' ]; then
chown -R redis:redis /data
exec gosu redis "$@"
fi
# 其他命令直接执行
exec "$@"
```
该脚本的内容就是根据 `CMD` 的内容来判断如果是 `redis-server` 的话则切换到 `redis` 用户身份启动服务器否则依旧使用 `root` 身份执行比如
### 工作流程
```
docker run redis docker run redis bash
│ │
▼ ▼
docker-entrypoint.sh redis-server docker-entrypoint.sh bash
│ │
├─ 初始化 ├─ 初始化
├─ chown -R redis:redis /data │
└─ exec gosu redis redis-server └─ exec bash
(以 redis 用户运行) (以 root 用户运行)
```
### 关键点
1. **exec "$@"**用传入的参数替换当前进程确保信号正确传递
2. **条件判断**根据 CMD 不同执行不同逻辑
3. **用户切换**使用 `gosu` 切换用户 `su` 更适合容器
---
## 场景三带参数的应用
```docker
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
ENTRYPOINT ["python", "app.py"]
CMD ["--host", "0.0.0.0", "--port", "8080"]
```
```bash
$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)
# 使用默认参数
$ docker run myapp
# 执行: python app.py --host 0.0.0.0 --port 8080
# 覆盖参数
$ docker run myapp --host 0.0.0.0 --port 9000
# 执行: python app.py --host 0.0.0.0 --port 9000
# 完全不同的参数
$ docker run myapp --help
# 执行: python app.py --help
```
---
## 覆盖 ENTRYPOINT
使用 `--entrypoint` 参数覆盖
```bash
# 正常运行
$ docker run myimage
# 覆盖 ENTRYPOINT 进入 shell 调试
$ docker run --entrypoint /bin/sh myimage
# 覆盖 ENTRYPOINT 并传入参数
$ docker run --entrypoint /bin/cat myimage /etc/os-release
```
---
## ENTRYPOINT CMD 组合表
| ENTRYPOINT | CMD | 最终执行命令 |
|------------|-----|-------------|
| | | 容器无法启动 |
| | `["cmd", "p1"]` | `cmd p1` |
| `["ep", "p1"]` | | `ep p1` |
| `["ep", "p1"]` | `["cmd", "p2"]` | `ep p1 cmd p2` |
| `ep p1`shell | `["cmd", "p2"]` | `/bin/sh -c "ep p1"`CMD 被忽略 |
> **注意**shell 格式的 ENTRYPOINT 会忽略 CMD
---
## 最佳实践
### 1. 使用 exec 格式
```docker
# ✅ 推荐
ENTRYPOINT ["python", "app.py"]
# ❌ 避免 shell 格式
ENTRYPOINT python app.py
```
### 2. 提供有意义的默认参数
```docker
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
```
### 3. 入口脚本使用 exec
```bash
#!/bin/sh
# 准备工作...
# 使用 exec 替换当前进程
exec "$@"
```
### 4. 处理信号
确保 ENTRYPOINT 脚本能正确传递信号
```bash
#!/bin/bash
trap 'kill -TERM $PID' TERM INT
# 启动应用
app "$@" &
PID=$!
# 等待应用退出
wait $PID
```
---
## 本章小结
| ENTRYPOINT | CMD | 适用场景 |
|------------|-----|---------|
| | | 镜像作为固定命令使用 |
| | | 简单的默认命令 |
| | | **推荐**固定命令 + 可配置参数 |
## 延伸阅读
- [CMD 容器启动命令](cmd.md)默认命令
- [最佳实践](../../appendix/best_practices.md)启动命令设计
- [后台运行](../../container/daemon.md)前台/后台概念

View File

@@ -1,35 +1,248 @@
# ENV 设置环境变量
格式有两种
* `ENV <key> <value>`
* `ENV <key1>=<value1> <key2>=<value2>...`
这个指令很简单就是设置环境变量而已无论是后面的其它指令 `RUN`还是运行时的应用都可以直接使用这里定义的环境变量
## 基本语法
```docker
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
# 格式一:单个变量
ENV <key> <value>
# 格式二:多个变量(推荐)
ENV <key1>=<value1> <key2>=<value2> ...
```
这个例子中演示了如何换行以及对含有空格的值用双引号括起来的办法这和 Shell 下的行为是一致的
---
定义了环境变量那么在后续的指令中就可以使用这个环境变量比如在官方 `node` 镜像 `Dockerfile` 就有类似这样的代码
## 基本用法
### 设置单个变量
```docker
ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
ENV NODE_VERSION 20.10.0
ENV APP_ENV production
```
在这里先定义了环境变量 `NODE_VERSION`其后的 `RUN` 这层里多次使用 `$NODE_VERSION` 来进行操作定制可以看到将来升级镜像构建版本的时候只需要更新 `7.2.0` 即可`Dockerfile` 构建维护变得更轻松了
### 设置多个变量
下列指令可以支持环境变量展开 `ADD``COPY``ENV``EXPOSE``FROM``LABEL``USER``WORKDIR``VOLUME``STOPSIGNAL``ONBUILD``RUN`
```docker
ENV NODE_VERSION=20.10.0 \
APP_ENV=production \
APP_NAME="My Application"
```
可以从这个指令列表里感觉到环境变量可以使用的地方很多很强大通过环境变量我们可以让一份 `Dockerfile` 制作更多的镜像只需使用不同的环境变量即可
> 💡 包含空格的值用双引号括起来
---
## 环境变量的作用
### 1. 后续指令中使用
```docker
ENV NODE_VERSION=20.10.0
# 在 RUN 中使用
RUN curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz \
| tar -xJ -C /usr/local --strip-components=1
# 在 WORKDIR 中使用
ENV APP_HOME=/app
WORKDIR $APP_HOME
# 在 COPY 中使用
COPY . $APP_HOME
```
### 2. 容器运行时使用
```docker
ENV DATABASE_URL=postgres://localhost/mydb
```
应用代码中可以读取
```python
import os
db_url = os.environ.get('DATABASE_URL')
```
```javascript
const dbUrl = process.env.DATABASE_URL;
```
---
## 支持环境变量的指令
以下指令可以使用 `$变量名` `${变量名}` 格式
| 指令 | 示例 |
|------|------|
| `RUN` | `RUN echo $VERSION` |
| `CMD` | `CMD ["sh", "-c", "echo $HOME"]` |
| `ENTRYPOINT` | 同上 |
| `COPY` | `COPY . $APP_HOME` |
| `ADD` | `ADD app.tar.gz $APP_HOME` |
| `WORKDIR` | `WORKDIR $APP_HOME` |
| `EXPOSE` | `EXPOSE $PORT` |
| `VOLUME` | `VOLUME $DATA_DIR` |
| `USER` | `USER $USERNAME` |
| `LABEL` | `LABEL version=$VERSION` |
| `FROM` | `FROM node:$NODE_VERSION` |
---
## 运行时覆盖
使用 `-e` `--env` 覆盖 Dockerfile 中定义的环境变量
```bash
# 覆盖单个变量
$ docker run -e APP_ENV=development myimage
# 覆盖多个变量
$ docker run -e APP_ENV=development -e DEBUG=true myimage
# 从环境变量文件读取
$ docker run --env-file .env myimage
```
### .env 文件格式
```bash
# .env
APP_ENV=development
DEBUG=true
DATABASE_URL=postgres://localhost/mydb
```
---
## ENV vs ARG
| 特性 | ENV | ARG |
|------|-----|-----|
| **生效时间** | 构建时 + 运行时 | 仅构建时 |
| **持久性** | 写入镜像运行时可用 | 构建后消失 |
| **覆盖方式** | `docker run -e` | `docker build --build-arg` |
| **适用场景** | 应用配置 | 构建参数如版本号 |
### 组合使用
```docker
# ARG 接收构建时参数
ARG NODE_VERSION=20
# ENV 保存到运行时
ENV NODE_VERSION=$NODE_VERSION
# 后续指令使用
RUN curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/...
```
```bash
# 构建时指定版本
$ docker build --build-arg NODE_VERSION=18 -t myapp .
```
---
## 最佳实践
### 1. 统一管理版本号
```docker
# ✅ 好:版本集中管理
ENV NGINX_VERSION=1.25.0 \
NODE_VERSION=20.10.0 \
PYTHON_VERSION=3.12.0
RUN apt-get install nginx=${NGINX_VERSION}
# ❌ 差:版本分散在各处
RUN apt-get install nginx=1.25.0
```
### 2. 不要存储敏感信息
```docker
# ❌ 错误:密码写入镜像
ENV DB_PASSWORD=secret123
# ✅ 正确:运行时传入
# docker run -e DB_PASSWORD=xxx myimage
```
### 3. 为应用提供合理默认值
```docker
ENV APP_ENV=production \
APP_PORT=8080 \
LOG_LEVEL=info
```
### 4. 使用有意义的变量名
```docker
# ✅ 好:清晰的命名
ENV REDIS_HOST=localhost \
REDIS_PORT=6379
# ❌ 差:模糊的命名
ENV HOST=localhost \
PORT=6379
```
---
## 常见问题
### Q: 环境变量在 CMD 中不展开
exec 格式不会自动展开环境变量
```docker
# ❌ 不会展开 $PORT
CMD ["python", "app.py", "--port", "$PORT"]
# ✅ 使用 shell 格式或显式调用 sh
CMD ["sh", "-c", "python app.py --port $PORT"]
```
### Q: 如何查看容器的环境变量
```bash
$ docker inspect mycontainer --format '{{json .Config.Env}}'
$ docker exec mycontainer env
```
### Q: 多行 ENV 还是多个 ENV
```docker
# ✅ 推荐:减少层数
ENV VAR1=value1 \
VAR2=value2 \
VAR3=value3
# ⚠️ 多个 ENV 会创建多层
ENV VAR1=value1
ENV VAR2=value2
ENV VAR3=value3
```
---
## 本章小结
| 要点 | 说明 |
|------|------|
| **语法** | `ENV KEY=value` |
| **作用范围** | 构建时 + 运行时 |
| **覆盖方式** | `docker run -e KEY=value` |
| ** ARG** | ARG 仅构建时ENV 持久化到运行时 |
| **安全** | 不要存储敏感信息 |
## 延伸阅读
- [ARG 构建参数](arg.md)构建时变量
- [Compose 环境变量](../../compose/compose_file.md)Compose 中的环境变量
- [最佳实践](../../appendix/best_practices.md)Dockerfile 编写指南

View File

@@ -1,7 +1,219 @@
# EXPOSE 声明端口
格式为 `EXPOSE <端口1> [<端口2>...]`
## 基本语法
`EXPOSE` 指令是声明容器运行时提供服务的端口这只是一个声明在容器运行时并不会因为这个声明应用就会开启这个端口的服务 Dockerfile 中写入这样的声明有两个好处一个是帮助镜像使用者理解这个镜像服务的守护端口以方便配置映射另一个用处则是在运行时使用随机端口映射时也就是 `docker run -P` 会自动随机映射 `EXPOSE` 的端口
```docker
EXPOSE <端口> [<端口>/<协议>...]
```
要将 `EXPOSE` 和在运行时使用 `-p <宿主端口>:<容器端口>` 区分开来`-p`是映射宿主端口和容器端口换句话说就是将容器的对应端口服务公开给外界访问 `EXPOSE` 仅仅是声明容器打算使用什么端口而已并不会自动在宿主进行端口映射
`EXPOSE` 声明容器运行时提供服务的端口这是一个**文档性质的声明**告诉使用者容器会监听哪些端口
---
## 基本用法
```docker
# 声明单个端口
EXPOSE 80
# 声明多个端口
EXPOSE 80 443
# 声明 TCP 和 UDP 端口
EXPOSE 80/tcp
EXPOSE 53/udp
```
---
## EXPOSE 的作用
### 1. 文档说明
告诉镜像使用者容器将在哪些端口提供服务
```docker
# 使用者一看就知道这是 web 应用
EXPOSE 80 443
```
```bash
# 查看镜像暴露的端口
$ docker inspect nginx --format '{{.Config.ExposedPorts}}'
map[80/tcp:{}]
```
### 2. 配合 -P 使用
使用 `docker run -P` Docker 会自动映射 EXPOSE 的端口到宿主机随机端口
```docker
# Dockerfile
EXPOSE 80
```
```bash
$ docker run -P nginx
$ docker port $(docker ps -q)
80/tcp -> 0.0.0.0:32768
```
---
## EXPOSE vs -p
| 特性 | EXPOSE | -p |
|------|--------|-----|
| **位置** | Dockerfile | docker run 命令 |
| **作用** | 声明/文档 | 实际端口映射 |
| **是否必需** | | 外部访问时 |
| **映射发生时** | 不发生 | 运行时发生 |
```
┌────────────────────────────────────────────────────────────┐
│ EXPOSE 80 │
│ ↓ │
│ 仅声明意图 │
│ └───────────────────────────────────────┘ │
│ │
│ docker run -p │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 实际端口映射 │ │
│ │ 宿主机 ←→ 容器 │ │
│ └─────────────────────┘ │
└────────────────────────────────────────────────────────────┘
```
### 没有 EXPOSE 也能 -p
```docker
# 即使没有 EXPOSE也可以使用 -p
FROM nginx
# 没有 EXPOSE
```
```bash
# 仍然可以映射端口
$ docker run -p 8080:80 mynginx
```
---
## 常见误解
### 误解EXPOSE 会打开端口
```docker
# ❌ 错误理解:这不会让容器可从外部访问
EXPOSE 80
```
EXPOSE 不会
- 自动进行端口映射
- 让服务可从外部访问
- 在容器启动时开启端口监听
EXPOSE 只是元数据声明容器是否实际监听该端口取决于容器内的应用
### 正确理解
```docker
# Dockerfile
FROM nginx
EXPOSE 80 # 1. 声明:这个容器会在 80 端口提供服务
```
```bash
# 运行:需要 -p 才能从外部访问
$ docker run -p 8080:80 nginx # 2. 映射:宿主机 8080 → 容器 80
```
---
## 最佳实践
### 1. 总是声明应用使用的端口
```docker
# Web 服务
FROM nginx
EXPOSE 80 443
# 数据库
FROM postgres
EXPOSE 5432
# Redis
FROM redis
EXPOSE 6379
```
### 2. 使用明确的协议
```docker
# 默认是 TCP
EXPOSE 80
# 明确指定 UDP
EXPOSE 53/udp
# 同时支持 TCP 和 UDP
EXPOSE 53/tcp 53/udp
```
### 3. 与应用实际端口保持一致
```docker
# ✅ 好EXPOSE 与应用端口一致
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]
# ❌ 差EXPOSE 与应用端口不一致(误导)
EXPOSE 80
CMD ["node", "server.js"] # 实际监听 3000
```
---
## 使用环境变量
```docker
ARG PORT=80
EXPOSE $PORT
```
---
## Compose
```yaml
services:
web:
build: .
ports:
- "8080:80" # 映射端口(类似 -p
expose:
- "80" # 仅声明(类似 EXPOSE
```
`expose` Compose 中仅用于容器间通信的文档说明不进行端口映射
---
## 本章小结
| 要点 | 说明 |
|------|------|
| **作用** | 声明容器提供服务的端口文档 |
| **不会** | 自动映射端口或开放外部访问 |
| **配合** | `docker run -P` 自动映射 |
| **外部访问** | 需要 `-p 宿主机端口:容器端口` |
| **语法** | `EXPOSE 80` `EXPOSE 80/tcp` |
## 延伸阅读
- [网络配置](../../network/README.md)Docker 网络详解
- [端口映射](../../network/port_bindingbindingbinding.md)-p 参数详解
- [Compose 端口](../../compose/compose_file.md)Compose 中的端口配置

View File

@@ -1,83 +1,206 @@
# HEALTHCHECK 健康检查
格式
## 基本语法
* `HEALTHCHECK [选项] CMD <命令>`设置检查容器健康状况的命令
* `HEALTHCHECK NONE`如果基础镜像有健康检查指令使用这行可以屏蔽掉其健康检查指令
```docker
HEALTHCHECK [选项] CMD <命令>
HEALTHCHECK NONE
```
`HEALTHCHECK` 指令告诉 Docker 应该如何进行判断容器状态是否正常这是 Docker 1.12 引入的新指令
`HEALTHCHECK` 指令告诉 Docker 如何判断容器状态是否正常这是保障服务高可用的重要机制
在没有 `HEALTHCHECK` 指令前Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常很多情况下这没问题但是如果程序进入死锁状态或者死循环状态应用进程并不退出但是该容器已经无法提供服务了 1.12 以前Docker 不会检测到容器的这种状态从而不会重新调度导致可能会有部分容器已经无法提供服务了却还在接受用户请求
---
而自 1.12 之后Docker 提供了 `HEALTHCHECK` 指令通过该指令指定一行命令用这行命令来判断容器主进程的服务状态是否还正常从而比较真实的反应容器实际状态
## 为什么需要 HEALTHCHECK
当在一个镜像指定了 `HEALTHCHECK` 指令后用其启动容器初始状态会为 `starting` `HEALTHCHECK` 指令检查成功后变为 `healthy`如果连续一定次数失败则会变为 `unhealthy`
在没有 HEALTHCHECK 之前Docker 只能通过**进程退出码**来判断容器状态
`HEALTHCHECK` 支持下列选项
**问题场景**
- Web 服务死锁无法响应请求但进程仍在运行
- 数据库正在启动中尚未准备好接受连接
- 应用陷入死循环CPU 爆满但进程存活
* `--interval=<间隔>`两次健康检查的间隔默认为 30
* `--timeout=<时长>`健康检查命令运行超时时间如果超过这个时间本次健康检查就被视为失败默认 30
* `--retries=<次数>`当连续失败指定次数后则将容器状态视为 `unhealthy`默认 3
**引入 HEALTHCHECK **
Docker 定期执行指定的检查命令根据返回值判断容器是否"健康"
`CMD`, `ENTRYPOINT` 一样`HEALTHCHECK` 只可以出现一次如果写了多个只有最后一个生效
```
容器状态转换:
Starting ──成功──> Healthy ──失败N次──> Unhealthy
▲ │
└──────成功──────┘
```
`HEALTHCHECK [选项] CMD` 后面的命令格式和 `ENTRYPOINT` 一样分为 `shell` 格式 `exec` 格式命令的返回值决定了该次健康检查的成功与否`0`成功`1`失败`2`保留不要使用这个值
---
假设我们有个镜像是个最简单的 Web 服务我们希望增加健康检查来判断其 Web 服务是否在正常工作我们可以用 `curl` 来帮助判断 `Dockerfile` `HEALTHCHECK` 可以这么写
## 基本用法
### Web 服务检查
```docker
FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -fs http://localhost/ || exit 1
```
这里我们设置了每 5 秒检查一次这里为了试验所以间隔非常短实际应该相对较长如果健康检查命令超过 3 秒没响应就视为失败并且使用 `curl -fs http://localhost/ || exit 1` 作为健康检查命令
### 命令返回值
使用 `docker build` 来构建这个镜像
- `0`: 成功 (healthy)
- `1`: 失败 (unhealthy)
- `2`: 保留值 (不使用)
```bash
$ docker build -t myweb:v1 .
### 常用选项
| 选项 | 说明 | 默认值 |
|------|------|--------|
| `--interval` | 两次检查的间隔 | 30s |
| `--timeout` | 检查命令的超时时间 | 30s |
| `--start-period` | 启动缓冲期期间失败不计入次数 | 0s |
| `--retries` | 连续失败多少次标记为 unhealthy | 3 |
---
## 屏蔽健康检查
如果基础镜像定义了 HEALTHCHECK但你不想使用它
```docker
FROM my-base-image
HEALTHCHECK NONE
```
构建好了后我们启动一个容器
---
```bash
$ docker run -d --name web -p 80:80 myweb:v1
## 常见检查脚本
### HTTP 服务
使用 `curl` `wget`
```docker
# 使用 curl
HEALTHCHECK CMD curl -f http://localhost/ || exit 1
# 使用 wget (Alpine 默认包含)
HEALTHCHECK CMD wget -q --spider http://localhost/ || exit 1
```
当运行该镜像后可以通过 `docker container ls` 看到最初的状态为 `(health: starting)`
### 数据库
```bash
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 3 seconds ago Up 2 seconds (health: starting) 80/tcp, 443/tcp web
```docker
# MySQL
HEALTHCHECK CMD mysqladmin ping -h localhost || exit 1
# Redis
HEALTHCHECK CMD redis-cli ping || exit 1
```
在等待几秒钟后再次 `docker container ls`就会看到健康状态变化为了 `(healthy)`
### 自定义脚本
```bash
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 18 seconds ago Up 16 seconds (healthy) 80/tcp, 443/tcp web
```docker
COPY healthcheck.sh /usr/local/bin/
HEALTHCHECK CMD ["healthcheck.sh"]
```
如果健康检查连续失败超过了重试次数状态就会变为 `(unhealthy)`
---
为了帮助排障健康检查命令的输出包括 `stdout` 以及 `stderr`都会被存储于健康状态里可以用 `docker inspect` 来查看
## Compose 中使用
可以在 `docker-compose.yml` 中覆盖或定义健康检查
```yaml
services:
web:
image: nginx
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 1m30s
timeout: 10s
retries: 3
start_period: 40s
```
带健康检查的依赖启动
```yaml
services:
web:
depends_on:
db:
condition: service_healthy # 等待 db 变健康才启动 web
db:
image: mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
```
---
## 查看健康状态
```bash
$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool
# 查看容器状态(包含健康信息)
$ docker ps
CONTAINER ID STATUS
abc123 Up 1 minute (healthy)
def456 Up 2 minutes (unhealthy)
# 查看详细健康日志
$ docker inspect --format '{{json .State.Health}}' mycontainer | jq
{
"FailingStreak": 0,
"Log": [
{
"End": "2016-11-25T14:35:37.940957051Z",
"ExitCode": 0,
"Output": "<!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",
"Start": "2016-11-25T14:35:37.780192565Z"
}
],
"Status": "healthy"
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "...",
"End": "...",
"ExitCode": 0,
"Output": "..."
}
]
}
```
---
## 最佳实践
### 1. 避免副作用
健康检查会被频繁执行不要在检查脚本中进行写操作或消耗大量资源的操作
### 2. 使用轻量级工具
优先使用镜像中已有的工具 `wget`避免为了健康检查安装庞大的依赖 `curl`
### 3. 设置合理的 Start Period
应用启动可能需要时间 Java 应用设置 `--start-period` 可以防止在启动阶段因检查失败而误判
```docker
# 给应用 1 分钟启动时间
HEALTHCHECK --start-period=60s CMD curl -f http://localhost/ || exit 1
```
### 4. 只检查核心依赖
健康检查应主要关注**当前服务**是否可用而不是检查其下游依赖数据库等下游依赖的检查应由应用逻辑处理
---
## 本章小结
| 要点 | 说明 |
|------|------|
| **作用** | 检测容器应用是否真实可用 |
| **命令** | `HEALTHCHECK [选项] CMD command` |
| **状态** | starting, healthy, unhealthy |
| **Compose** | 支持 `condition: service_healthy` 依赖 |
| **注意** | 避免副作用节省资源 |
## 延伸阅读
- [CMD 容器启动命令](cmd.md)启动主进程
- [Compose 模板文件](../../compose/compose_file.md)Compose 中的健康检查
- [Docker 调试](../../appendix/debug.md)容器排障

View File

@@ -1,17 +1,154 @@
# LABEL 指令
# LABEL 为镜像添加元数据
`LABEL` 指令用来给镜像以键值对的形式添加一些元数据metadata
## 基本语法
```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
LABEL org.opencontainers.image.authors="yeasy"
LABEL org.opencontainers.image.documentation="https://yeasy.gitbooks.io"
LABEL version="1.0"
LABEL description="这是一个 Web 应用服务器"
```
具体可以参考 https://github.com/opencontainers/image-spec/blob/master/annotations.md
### 定义多个标签推荐
```docker
LABEL maintainer="user@example.com" \
version="1.2.0" \
description="My App Description" \
org.opencontainers.image.authors="Yeasy"
```
> 💡 包含空格的值需要用引号括起来
---
## 常用标签规范 (OCI Annotations)
为了标准和互操作性推荐使用 [OCI Image Format Specification](https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys) 定义的标准标签:
| 标签 Key | 说明 | 示例 |
|----------|------|------|
| `org.opencontainers.image.created` | 构建时间(RFC 3339) | `2024-01-01T00:00:00Z` |
| `org.opencontainers.image.authors` | 作者/维护者 | `support@example.com` |
| `org.opencontainers.image.url` | 项目主页 | `https://example.com` |
| `org.opencontainers.image.documentation`| 文档地址 | `https://example.com/docs` |
| `org.opencontainers.image.source` | 源码仓库 | `https://github.com/user/repo` |
| `org.opencontainers.image.version` | 版本号 | `1.0.0` |
| `org.opencontainers.image.licenses` | 许可证 | `MIT` |
| `org.opencontainers.image.title` | 镜像标题 | `My App` |
| `org.opencontainers.image.description` | 描述 | `Production ready web server` |
### 示例
```docker
LABEL org.opencontainers.image.authors="yeasy" \
org.opencontainers.image.documentation="https://yeasy.gitbooks.io" \
org.opencontainers.image.source="https://github.com/yeasy/docker_practice" \
org.opencontainers.image.licenses="MIT"
```
---
## MAINTAINER 指令已废弃
旧版本的 Dockerfile 中常看到 `MAINTAINER` 指令
```docker
# ❌ 已弃用
MAINTAINER user@example.com
```
现在推荐使用 `LABEL`
```docker
# ✅ 推荐
LABEL maintainer="user@example.com"
# 或
LABEL org.opencontainers.image.authors="user@example.com"
```
---
## 动态标签
配合 `ARG` 使用可以在构建时动态注入标签
```docker
ARG BUILD_DATE
ARG VCS_REF
LABEL org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.revision=$VCS_REF
```
构建命令
```bash
$ docker build \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=$(git rev-parse --short HEAD) \
.
```
---
## 查看标签
### docker inspect
查看镜像的标签信息
```bash
$ docker inspect nginx --format '{{json .Config.Labels}}' | jq
{
"maintainer": "NGINX Docker Maintainers <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` 是一个特殊的指令它后面跟的是其它指令比如 `RUN`, `COPY` 而这些指令在当前镜像构建时并不会被执行只有当以当前镜像为基础镜像去构建下一级镜像的时候才会被执行
`Dockerfile` 中的其它指令都是为了定制当前镜像而准备的唯有 `ONBUILD` 是为了帮助别人定制自己而准备的
假设我们要制作 Node.js 所写的应用的镜像我们都知道 Node.js 使用 `npm` 进行包管理所有依赖配置启动信息等会放到 `package.json` 文件里在拿到程序代码后需要先进行 `npm install` 才可以获得所有需要的依赖然后就可以通过 `npm start` 来启动应用因此一般来说会这样写 `Dockerfile`
## 基本语法
```docker
FROM node:slim
RUN mkdir /app
ONBUILD <其它指令>
```
`ONBUILD` 是一个特殊的指令它后面跟的是其它指令 `RUN`, `COPY` 这些指令**在当前镜像构建时不会执行**只有当以当前镜像为基础镜像去构建下一级镜像时才会被执行
---
## 为什么需要 ONBUILD
`ONBUILD` 主要用于制作**语言栈基础镜像****框架基础镜像**
### 场景维护 Node.js 项目
假设你有多个 Node.js 项目它们的构建流程都一样
1. 创建目录
2. 复制 `package.json`
3. 执行 `npm install`
4. 复制源码
5. 启动应用
如果不使用 `ONBUILD`每个项目的 Dockerfile 都要重复这些步骤且通过 `COPY` 复制文件时基础镜像无法预知子项目的文件名
### 使用 ONBUILD 的解决方案
**基础镜像 (my-node-base)**
```docker
FROM node:20-alpine
WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
CMD [ "npm", "start" ]
# 这些指令将在子镜像构建时执行
ONBUILD COPY package*.json ./
ONBUILD RUN npm install
ONBUILD COPY . .
CMD ["npm", "start"]
```
把这个 `Dockerfile` 放到 Node.js 项目的根目录构建好镜像后就可以直接拿来启动容器运行但是如果我们还有第二个 Node.js 项目也差不多呢好吧那就再把这个 `Dockerfile` 复制到第二个项目里那如果有第三个项目呢再复制么文件的副本越多版本控制就越困难让我们继续看这样的场景维护的问题
如果第一个 Node.js 项目在开发过程中发现这个 `Dockerfile` 里存在问题比如敲错字了或者需要安装额外的包然后开发人员修复了这个 `Dockerfile`再次构建问题解决第一个项目没问题了但是第二个项目呢虽然最初 `Dockerfile` 是复制粘贴自第一个项目的但是并不会因为第一个项目修复了他们的 `Dockerfile`而第二个项目的 `Dockerfile` 就会被自动修复
那么我们可不可以做一个基础镜像然后各个项目使用这个基础镜像呢这样基础镜像更新各个项目不用同步 `Dockerfile` 的变化重新构建后就继承了基础镜像的更新好吧可以让我们看看这样的结果那么上面的这个 `Dockerfile` 就会变为
**子项目 Dockerfile**
```docker
FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]
FROM my-node-base
# 只需要一行!
# 构建时会自动执行 COPY 和 RUN
```
这里我们把项目相关的构建指令拿出来放到子项目里去假设这个基础镜像的名字为 `my-node` 的话各个项目内的自己的 `Dockerfile` 就变为
---
## 执行机制
```
基础镜像构建:
Dockerfile (含 ONBUILD) ──build──> 基础镜像 (记录了 ONBUILD 触发器)
(指令未执行)
子镜像构建:
FROM 基础镜像 ──build──> 读取基础镜像触发器 ──> 执行触发器指令 ──> 继续执行子 Dockerfile
```
---
## 常见使用场景
### 1. 自动处理依赖安装
```docker
FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
# Python 基础镜像
ONBUILD COPY requirements.txt ./
ONBUILD RUN pip install -r requirements.txt
```
基础镜像变化后各个项目都用这个 `Dockerfile` 重新构建镜像会继承基础镜像的更新
那么问题解决了么没有准确说只解决了一半如果这个 `Dockerfile` 里面有些东西需要调整呢比如 `npm install` 都需要加一些参数那怎么办这一行 `RUN` 是不可能放入基础镜像的因为涉及到了当前项目的 `./package.json`难道又要一个个修改么所以说这样制作基础镜像只解决了原来的 `Dockerfile` 的前4条指令的变化问题而后面三条指令的变化则完全没办法处理
`ONBUILD` 可以解决这个问题让我们用 `ONBUILD` 重新写一下基础镜像的 `Dockerfile`:
### 2. 自动编译代码
```docker
FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]
# Go 基础镜像
ONBUILD COPY . .
ONBUILD RUN go build -o app main.go
```
这次我们回到原始的 `Dockerfile`但是这次将项目相关的指令加上 `ONBUILD`这样在构建基础镜像的时候这三行并不会被执行然后各个项目的 `Dockerfile` 就变成了简单地
### 3. 处理静态资源
```docker
FROM my-node
# Nginx 静态网站基础镜像
ONBUILD COPY dist/ /usr/share/nginx/html/
```
是的只有这么一行当在各个项目目录中用这个只有一行的 `Dockerfile` 构建镜像时之前基础镜像的那三行 `ONBUILD` 就会开始执行成功的将当前项目的代码复制进镜像并且针对本项目执行 `npm install`生成应用镜像
---
## 注意事项
### 1. 继承性限制
`ONBUILD` 指令**只会继承一次**
- 镜像 A ( ONBUILD)
- 镜像 B (FROM A) -> 触发 ONBUILD
- 镜像 C (FROM B) -> **不会**再次触发 ONBUILD
### 2. 构建上下文
子镜像构建时`ONBUILD COPY . .` 中的 `.` 指的是**子项目**的构建上下文而不是基础镜像的上下文
### 3. 不允许级联
`ONBUILD ONBUILD` 是非法的你不能写 `ONBUILD ONBUILD COPY ...`
### 4. 可能会导致构建失败
由于 `ONBUILD` 实际上是在子镜像中执行指令如果子项目的上下文不满足要求例如缺少 `package.json`会导致子镜像构建失败且错误信息可能比较隐晦
---
## 最佳实践
### 1. 命名规范
建议在镜像标签中添加 `-onbuild` 后缀明确告知使用者该镜像包含触发器
```
node:20-onbuild
python:3.12-onbuild
```
### 2. 避免执行耗时操作
尽量不要在 `ONBUILD` 中执行过于耗时或不确定的操作如更新系统软件这会让子镜像构建变得缓慢且不可控
### 3. 清理工作
如果 `ONBUILD` 指令产生了临时文件最好在同一个指令链中清理或者提供机制让子镜像清理
---
## 本章小结
| 要点 | 说明 |
|------|------|
| **作用** | 定义在子镜像构建时执行的指令 |
| **语法** | `ONBUILD INSTRUCTION` |
| **适用** | 基础架构镜像Node, Python, Go |
| **限制** | 只继承一次不可级联 |
| **规范** | 建议使用 `-onbuild` 标签后缀 |
## 延伸阅读
- [COPY 指令](copy.md)文件复制
- [Dockerfile 最佳实践](../../appendix/best_practices.md)基础镜像设计

View File

@@ -1,33 +1,141 @@
# SHELL 指令
格式`SHELL ["executable", "parameters"]`
`SHELL` 指令可以指定 `RUN` `ENTRYPOINT` `CMD` 指令的 shellLinux 中默认为 `["/bin/sh", "-c"]`
## 基本语法
```docker
SHELL ["executable", "parameters"]
```
`SHELL` 指令允许覆盖 Docker 默认的 shell
- **Linux 默认**`["/bin/sh", "-c"]`
- **Windows 默认**`["cmd", "/S", "/C"]`
该指令会影响后续的 `RUN`, `CMD`, `ENTRYPOINT` 指令当它们使用 shell 格式时
---
## 为什么要用 SHELL 指令
### 1. 使用 bash 特性
默认的 `/bin/sh`通常是 dash alpine ash功能有限如果你需要使用 bash 的特有功能如数组`{}` 扩展`pipefail` 可以切换 shell
```docker
FROM ubuntu:24.04
# 切换到 bash
SHELL ["/bin/bash", "-c"]
# 现在可以使用 bash 特性了
RUN echo {a..z}
```
### 2. 增强错误处理 (pipefail)
默认情况下管道命令 `cmd1 | cmd2` 只要 `cmd2` 成功整个指令就视为成功这可能掩盖构建错误
```docker
# ❌ 这里的 wget 失败了,但构建继续(因为 tar 成功了)
RUN wget -O - https://invalid-url | tar xz
```
使用 `SHELL` 启用 `pipefail`
```docker
# ✅ 启用 pipefail
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# 如果 wget 失败,整个 RUN 就会失败
RUN wget -O - https://invalid-url | tar xz
```
### 3. Windows 环境
Windows 容器中经常需要在 `cmd` `powershell` 之间切换
```docker
FROM mcr.microsoft.com/windows/servercore:ltsc2022
# 默认是 cmd
RUN echo Default shell is cmd
# 切换到 powershell
SHELL ["powershell", "-command"]
RUN Write-Host "Hello from PowerShell"
# 切回 cmd
SHELL ["cmd", "/S", "/C"]
```
---
## 作用范围
`SHELL` 指令可以出现多次每次只影响其后的指令
```docker
FROM ubuntu:24.04
# 使用默认 sh
RUN echo "Using sh"
SHELL ["/bin/bash", "-c"]
# 使用 bash
RUN echo "Using bash"
SHELL ["/bin/sh", "-c"]
RUN lll ; ls
SHELL ["/bin/sh", "-cex"]
RUN lll ; ls
# 回到 sh
RUN echo "Using sh again"
```
两个 `RUN` 运行同一命令第二个 `RUN` 运行的命令会打印出每条命令并当遇到错误时退出
---
`ENTRYPOINT` `CMD` shell 格式指定时`SHELL` 指令所指定的 shell 也会成为这两个指令的 shell
## 对其他指令的影响
`SHELL` 影响的是所有使用 **shell 格式** 的指令
| 指令格式 | 是否受 SHELL 影响 |
|---------|-------------------|
| `RUN command` | |
| `RUN ["exec", "param"]` | |
| `CMD command` | |
| `CMD ["exec", "param"]` | |
| `ENTRYPOINT command` | |
| `ENTRYPOINT ["exec", "param"]` | |
---
## 最佳实践
### 1. 推荐开启 pipefail
对于使用 bash 的镜像强烈建议开启 `pipefail`以确保构建过程中的错误能被及时捕获
```docker
SHELL ["/bin/sh", "-cex"]
# /bin/sh -cex "nginx"
ENTRYPOINT nginx
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
```
```docker
SHELL ["/bin/sh", "-cex"]
### 2. 明确意图
# /bin/sh -cex "nginx"
CMD nginx
```
如果由于脚本需求必须更改 shell最好在 Dockerfile 中显式声明而不是依赖默认行为
### 3. 尽量保持一致
避免在 Dockerfile 中频繁切换 SHELL这会使构建过程难以理解和调试尽量在头部定义一次即可
---
## 本章小结
| 要点 | 说明 |
|------|------|
| **作用** | 更改 RUN/CMD/ENTRYPOINT 的默认 shell |
| **Linux 默认** | `["/bin/sh", "-c"]` |
| **Windows 默认** | `["cmd", "/S", "/C"]` |
| **推荐用法** | `SHELL ["/bin/bash", "-o", "pipefail", "-c"]` |
| **影响范围** | 后续所有使用 shell 格式的指令 |
## 延伸阅读
- [RUN 指令](../../image/build.md)执行命令
- [Dockerfile 最佳实践](../../appendix/best_practices.md)错误处理与调试

View File

@@ -1,26 +1,273 @@
# USER 指定当前用户
格式`USER <用户名>[:<用户组>]`
`USER` 指令和 `WORKDIR` 相似都是改变环境状态并影响以后的层`WORKDIR` 是改变工作目录`USER` 则是改变之后层的执行 `RUN`, `CMD` 以及 `ENTRYPOINT` 这类命令的身份
注意`USER` 只是帮助你切换到指定用户而已这个用户必须是事先建立好的否则无法切换
## 基本语法
```docker
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]
USER <用户名>[:<用户组>]
USER <UID>[:<GID>]
```
如果以 `root` 执行的脚本在执行期间希望改变身份比如希望以某个已经建立好的用户来运行某个服务进程不要使用 `su` 或者 `sudo`这些都需要比较麻烦的配置而且在 TTY 缺失的环境下经常出错建议使用 [`gosu`](https://github.com/tianon/gosu)
`USER` 指令切换后续指令RUNCMDENTRYPOINT的执行用户
---
## 为什么要使用 USER
> 笔者强调以非 root 用户运行容器是最重要的安全实践之一
```
root 用户运行的风险:
┌────────────────────────────────────────────────────────┐
│ 容器内 root ←─ 可能逃逸 ─→ 宿主机 root │
│ │ │ │
│ └── 漏洞利用 ───────────────→ 完全控制宿主机 │
└────────────────────────────────────────────────────────┘
非 root 用户运行:
┌────────────────────────────────────────────────────────┐
│ 容器内普通用户 ──逃逸后──→ 宿主机普通用户 │
│ │ │ │
│ └── 权限受限,危害降低 ─────→ 无法控制系统 │
└────────────────────────────────────────────────────────┘
```
---
## 基本用法
### 创建并切换用户
```docker
# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
# 设置 CMD并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]
FROM node:20-alpine
# 1. 创建用户和组
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -D appuser
# 2. 设置目录权限
WORKDIR /app
COPY --chown=appuser:appgroup . .
# 3. 切换用户
USER appuser
# 4. 后续命令以 appuser 身份运行
CMD ["node", "server.js"]
```
### 使用 UID/GID
```docker
# 也可以使用数字
USER 1001:1001
```
---
## 用户必须已存在
`USER` 指令只能切换到**已存在**的用户
```docker
# ❌ 错误:用户不存在
USER nonexistent
# Error: unable to find user nonexistent
# ✅ 正确:先创建用户
RUN useradd -r -s /bin/false appuser
USER appuser
```
### 创建用户的方式
**Debian/Ubuntu**
```docker
RUN groupadd -r appgroup && \
useradd -r -g appgroup appuser
```
**Alpine**
```docker
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S -G appgroup appuser
```
| 选项 | 说明 |
|------|------|
| `-r` (useradd) / `-S` (adduser) | 创建系统用户 |
| `-g` | 指定主组 |
| `-G` | 指定附加组 |
| `-u` | 指定 UID |
| `-s /bin/false` | 禁用登录 shell |
---
## 运行时切换用户
### 使用 gosu推荐
ENTRYPOINT 脚本中切换用户时不要使用 `su` `sudo`应使用 [gosu](https://github.com/tianon/gosu)
```docker
FROM debian:bookworm
# 创建用户
RUN groupadd -r redis && useradd -r -g redis redis
# 安装 gosu
RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/*
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["redis-server"]
```
**docker-entrypoint.sh**
```bash
#!/bin/bash
set -e
# 以 root 执行初始化
chown -R redis:redis /data
# 用 gosu 切换到 redis 用户运行服务
exec gosu redis "$@"
```
### 为什么不用 su/sudo
| 问题 | su/sudo | gosu |
|------|---------|------|
| TTY 要求 | 需要 | 不需要 |
| 信号传递 | 不正确 | 正确 |
| 子进程 | | exec 替换 |
| 容器中使用 | | |
---
## 运行时覆盖用户
使用 `-u` `--user` 参数
```bash
# 以指定用户运行
$ docker run -u 1001:1001 myimage
# 以 root 运行(调试时)
$ docker run -u root myimage
```
---
## 文件权限处理
切换用户后确保应用有权访问文件
```docker
FROM node:20-alpine
# 创建用户
RUN adduser -D -u 1001 appuser
WORKDIR /app
# 方式1使用 --chown
COPY --chown=appuser:appuser . .
# 方式2手动 chown减少层数
# COPY . .
# RUN chown -R appuser:appuser /app
USER appuser
CMD ["node", "server.js"]
```
---
## 最佳实践
### 1. 始终使用非 root 用户
```docker
# ✅ 推荐
RUN adduser -D appuser
USER appuser
CMD ["myapp"]
# ❌ 避免
CMD ["myapp"] # 以 root 运行
```
### 2. 使用固定 UID/GID
便于在宿主机和容器间共享文件
```docker
# 使用常见的非 root UID
RUN addgroup -g 1000 -S appgroup && \
adduser -u 1000 -S -G appgroup appuser
USER 1000:1000
```
### 3. 多阶段构建中的 USER
```docker
# 构建阶段可以用 root
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
# 生产阶段用非 root
FROM node:20-alpine
RUN adduser -D appuser
WORKDIR /app
COPY --from=builder --chown=appuser:appuser /app/dist .
USER appuser
CMD ["node", "server.js"]
```
---
## 常见问题
### Q: 权限被拒绝
```bash
permission denied: '/app/data.log'
```
**解决**确保目录权限正确
```docker
RUN mkdir -p /app/data && chown appuser:appuser /app/data
```
### Q: 无法绑定低于 1024 的端口
root 用户无法绑定 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 ["<路径1>", "<路径2>"...]`
* `VOLUME <路径>`
之前我们说过容器运行时应该尽量保持容器存储层不发生写操作对于数据库类需要保存动态数据的应用其数据库文件应该保存于卷(volume)后面的章节我们会进一步介绍 Docker 卷的概念为了防止运行时用户忘记将动态文件所保存目录挂载为卷 `Dockerfile` 我们可以事先指定某些目录挂载为匿名卷这样在运行时如果用户不指定挂载其应用也可以正常运行不会向容器存储层写入大量数据
## 基本语法
```docker
VOLUME ["/路径1", "/路径2"]
VOLUME /路径
```
`VOLUME` 指令创建挂载点并标记为外部挂载的卷
---
## 为什么使用 VOLUME
> **核心原则**容器存储层应该保持无状态任何运行时数据都应该存储在卷中
```
没有 VOLUME 使用 VOLUME
┌─────────────────────┐ ┌─────────────────────┐
│ 容器存储层 │ │ 容器存储层 │
│ ┌─────────────┐ │ │ (只读/无状态) │
│ │ 数据库文件 │←─问题 │ │
│ │ 日志文件 │ │ └──────────┬──────────┘
│ │ 上传文件 │ │ │
│ └─────────────┘ │ ┌──────────▼──────────┐
└─────────────────────┘ │ 数据卷 │
容器删除 = 数据丢失 │ ┌─────────────┐ │
│ │ 持久化数据 │←─安全
│ └─────────────┘ │
└─────────────────────┘
容器删除,数据保留
```
---
## 基本用法
### 定义单个卷
```docker
FROM mysql:8.0
VOLUME /var/lib/mysql
```
### 定义多个卷
```docker
FROM myapp
VOLUME ["/data", "/logs", "/config"]
```
---
## VOLUME 的行为
### 1. 自动创建匿名卷
如果运行时未指定挂载Docker 会自动创建匿名卷
```bash
$ docker run mysql:8.0
$ docker volume ls
DRIVER VOLUME NAME
local a1b2c3d4e5f6... # 自动创建的匿名卷
```
### 2. 可被命名卷覆盖
```bash
# 使用命名卷替代匿名卷
$ docker run -v mysql_data:/var/lib/mysql mysql:8.0
```
### 3. 可被 Bind Mount 覆盖
```bash
# 使用宿主机目录替代
$ docker run -v /my/data:/var/lib/mysql mysql:8.0
```
---
## VOLUME 在构建时的特殊行为
> **重要**VOLUME 之后对该目录的修改会被丢弃
```docker
FROM ubuntu
VOLUME /data
# ❌ 这个文件不会出现在镜像中!
RUN echo "hello" > /data/test.txt
```
**原因**VOLUME 指令之后Docker 将该目录视为外部挂载点不再记录对它的修改
### 正确做法
```docker
FROM ubuntu
# ✅ 先写入文件
RUN mkdir -p /data && echo "hello" > /data/test.txt
# 再声明 VOLUME
VOLUME /data
```
这里的 `/data` 目录就会在容器运行时自动挂载为匿名卷任何向 `/data` 中写入的信息都不会记录进容器存储层从而保证了容器存储层的无状态化当然运行容器时可以覆盖这个挂载设置比如
---
```bash
$ docker run -d -v mydata:/data xxxx
## 常见使用场景
### 数据库持久化
```docker
FROM postgres:15
VOLUME /var/lib/postgresql/data
```
在这行命令中就使用了 `mydata` 这个命名卷挂载到了 `/data` 这个位置替代了 `Dockerfile` 中定义的匿名卷的挂载配置
### 日志目录
```docker
FROM nginx
VOLUME /var/log/nginx
```
### 上传文件目录
```docker
FROM myapp
VOLUME /app/uploads
```
---
## 查看 VOLUME 定义
```bash
# 查看镜像定义的 VOLUME
$ docker inspect mysql:8.0 --format '{{json .Config.Volumes}}' | jq
{
"/var/lib/mysql": {}
}
# 查看容器挂载的卷
$ docker inspect mycontainer --format '{{json .Mounts}}' | jq
```
---
## VOLUME vs docker run -v
| 特性 | Dockerfile VOLUME | docker run -v |
|------|-------------------|---------------|
| **定义时机** | 镜像构建时 | 容器运行时 |
| **默认行为** | 创建匿名卷 | 可指定命名卷或路径 |
| **灵活性** | 固定路径 | 可任意指定 |
| **适用场景** | 定义必须持久化的路径 | 灵活的数据管理 |
---
## Compose
```yaml
services:
db:
image: postgres:15
volumes:
# 命名卷(推荐)
- postgres_data:/var/lib/postgresql/data
# Bind Mount
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
volumes:
postgres_data: # 声明命名卷
```
---
## 安全注意事项
### 匿名卷可能导致数据丢失
```bash
# 使用 --rm 运行的容器,匿名卷会在容器删除时一起删除
$ docker run --rm mysql:8.0
# 容器停止后,数据丢失!
```
**解决**始终使用命名卷
```bash
$ docker run -v mysql_data:/var/lib/mysql mysql:8.0
```
---
## 最佳实践
### 1. 定义必须持久化的路径
```docker
# 数据库必须使用卷
FROM postgres:15
VOLUME /var/lib/postgresql/data
```
### 2. 不要在 VOLUME 后修改目录
```docker
# ❌ 避免
VOLUME /app/data
RUN cp init-data.json /app/data/
# ✅ 正确
RUN mkdir -p /app/data && cp init-data.json /app/data/
VOLUME /app/data
```
### 3. 文档中说明 VOLUME 用途
```docker
# 持久化用户上传的文件
VOLUME /app/uploads
# 持久化数据库数据
VOLUME /var/lib/mysql
```
---
## 本章小结
| 要点 | 说明 |
|------|------|
| **作用** | 创建挂载点标记为外部卷 |
| **语法** | `VOLUME /path` |
| **默认行为** | 自动创建匿名卷 |
| **覆盖方式** | `docker run -v name:/path` |
| **注意** | VOLUME 之后的修改会丢失 |
## 延伸阅读
- [数据卷](../../data_management/volume.md)卷的管理和使用
- [挂载主机目录](../../data_management/bind-mounts.md)Bind Mount
- [Compose 数据管理](../../compose/compose_file.md)Compose 中的卷配置

View File

@@ -1,36 +1,196 @@
# WORKDIR 指定工作目录
格式为 `WORKDIR <工作目录路径>`
使用 `WORKDIR` 指令可以来指定工作目录或者称为当前目录以后各层的当前目录就被改为指定的目录如该目录不存在`WORKDIR` 会帮你建立目录
之前提到一些初学者常犯的错误是把 `Dockerfile` 等同于 Shell 脚本来书写这种错误的理解还可能会导致出现下面这样的错误
## 基本语法
```docker
RUN cd /app
RUN echo "hello" > world.txt
WORKDIR <工作目录路径>
```
如果将这个 `Dockerfile` 进行构建镜像运行后会发现找不到 `/app/world.txt` 文件或者其内容不是 `hello`原因其实很简单 Shell 连续两行是同一个进程执行环境因此前一个命令修改的内存状态会直接影响后一个命令而在 `Dockerfile` 这两行 `RUN` 命令的执行环境根本不同是两个完全不同的容器这就是对 `Dockerfile` 构建分层存储的概念不了解所导致的错误
`WORKDIR` 指定后续指令的工作目录如果目录不存在Docker 会自动创建
之前说过每一个 `RUN` 都是启动一个容器执行命令然后提交存储层文件变更第一层 `RUN cd /app` 的执行仅仅是当前进程的工作目录变更一个内存上的变化而已其结果不会造成任何文件变更而到第二层的时候启动的是一个全新的容器跟第一层的容器更完全没关系自然不可能继承前一层构建过程中的内存变化
---
因此如果需要改变以后各层的工作目录的位置那么应该使用 `WORKDIR` 指令
## 基本用法
```docker
WORKDIR /app
RUN echo "hello" > world.txt
RUN pwd # 输出 /app
RUN echo "hello" > world.txt # 创建 /app/world.txt
COPY . . # 复制到 /app/
```
如果你的 `WORKDIR` 指令使用的相对路径那么所切换的路径与之前的 `WORKDIR` 有关
---
## 为什么需要 WORKDIR
### 常见错误
```docker
# ❌ 错误cd 在下一个 RUN 中无效
RUN cd /app
RUN echo "hello" > world.txt # 文件在根目录!
```
### 原因分析
```
RUN cd /app
启动容器 → cd /app仅内存变化→ 提交镜像层 → 容器销毁
↓ 工作目录未改变!
RUN echo "hello" > world.txt
启动新容器(工作目录在 /)→ 创建 /world.txt
```
每个 RUN 都在新容器中执行**前一个 RUN 的内存状态包括工作目录不会保留**
### 正确做法
```docker
# ✅ 正确:使用 WORKDIR
WORKDIR /app
RUN echo "hello" > world.txt # 创建 /app/world.txt
```
---
## 相对路径
WORKDIR 支持相对路径基于上一个 WORKDIR
```docker
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
RUN pwd # 输出 /a/b/c
```
`RUN pwd` 的工作目录为 `/a/b/c`
---
## 使用环境变量
```docker
ENV APP_HOME=/app
WORKDIR $APP_HOME
RUN pwd # 输出 /app
```
---
## 多阶段构建中的 WORKDIR
```docker
# 构建阶段
FROM node:20 AS builder
WORKDIR /build
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 生产阶段
FROM nginx:alpine
WORKDIR /usr/share/nginx/html
COPY --from=builder /build/dist .
```
---
## 最佳实践
### 1. 尽早设置 WORKDIR
```docker
FROM node:20
WORKDIR /app # 尽早设置
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]
```
### 2. 使用绝对路径
```docker
# ✅ 推荐:绝对路径,意图明确
WORKDIR /app
# ⚠️ 避免:相对路径可能造成混淆
WORKDIR app
```
### 3. 不要用 RUN cd
```docker
# ❌ 避免
RUN cd /app && echo "hello" > world.txt
# ✅ 推荐
WORKDIR /app
RUN echo "hello" > world.txt
```
### 4. 适时重置 WORKDIR
```docker
WORKDIR /app
# ... 应用相关操作 ...
WORKDIR /data
# ... 数据相关操作 ...
```
---
## 与其他指令的关系
| 指令 | WORKDIR 的影响 |
|------|---------------|
| `RUN` | WORKDIR 中执行命令 |
| `CMD` | WORKDIR 中启动 |
| `ENTRYPOINT` | WORKDIR 中启动 |
| `COPY` | 相对目标路径基于 WORKDIR |
| `ADD` | 相对目标路径基于 WORKDIR |
```docker
WORKDIR /app
RUN pwd # /app
COPY . . # 复制到 /app
CMD ["./start.sh"] # /app/start.sh
```
---
## 运行时覆盖
使用 `-w` 参数覆盖工作目录
```bash
$ docker run -w /tmp myimage pwd
/tmp
```
---
## 本章小结
| 要点 | 说明 |
|------|------|
| **作用** | 设置后续指令的工作目录 |
| **语法** | `WORKDIR /path` |
| **自动创建** | 目录不存在会自动创建 |
| **持久性** | 影响后续所有指令直到下次 WORKDIR |
| **不要用** | `RUN cd /path`无效 |
## 延伸阅读
- [COPY 复制文件](copy.md)文件复制
- [RUN 执行命令](../../image/build.md)执行构建命令
- [最佳实践](../../appendix/best_practices.md)Dockerfile 编写指南

View File

@@ -1,150 +1,258 @@
# 列出镜像
要想列出已经下载下来的镜像可以使用 `docker image ls` 命令
## 基本用法
查看本地已下载的镜像
```bash
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB
nginx latest 05a60462f8ba 5 days ago 181 MB
mongo 3.2 fe9198c04d62 5 days ago 342 MB
<none> <none> 00285df0df87 5 days ago 342 MB
ubuntu 18.04 329ed837d508 3 days ago 63.3MB
ubuntu bionic 329ed837d508 3 days ago 63.3MB
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183MB
nginx latest 05a60462f8ba 5 days ago 181MB
ubuntu 24.04 329ed837d508 3 days ago 78MB
ubuntu noble 329ed837d508 3 days ago 78MB
```
列表包含了 `仓库名``标签``镜像 ID``创建时间` 以及 `所占用的空间`
> 💡 `docker images` `docker image ls` 的简写两者等效
其中仓库名标签在之前的基础概念章节已经介绍过了**镜像 ID** 则是镜像的唯一标识一个镜像可以对应多个 **标签**因此在上面的例子中我们可以看到 `ubuntu:24.04` `ubuntu:noble` 拥有相同的 ID因为它们对应的是同一个镜像
---
## 镜像体积
## 输出字段说明
如果仔细观察会注意到这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同比如`ubuntu:24.04` 镜像大小在这里是 `78MB`但是在 [Docker Hub](https://hub.docker.com/_/ubuntu) 显示的却是 `29MB`。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 `docker image ls` 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。
| 字段 | 说明 |
|------|------|
| **REPOSITORY** | 仓库名 |
| **TAG** | 标签版本 |
| **IMAGE ID** | 镜像唯一标识 ID 12 |
| **CREATED** | 创建时间 |
| **SIZE** | 本地占用空间 |
另外一个需要注意的问题是`docker image ls` 列表中的镜像体积总和并非是所有镜像实际硬盘消耗由于 Docker 镜像是多层存储结构并且可以继承复用因此不同镜像可能会因为使用相同的基础镜像从而拥有共同的层由于 Docker 使用 Union FS相同的层只需要保存一份即可因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多
### 同一镜像多个标签
你可以通过 `docker system df` 命令来便捷的查看镜像容器数据卷所占用的空间
注意上面的 `ubuntu:24.04` `ubuntu:noble` 拥有相同的 IMAGE ID它们是同一个镜像的不同标签只占用一份存储空间
---
## 理解镜像大小
### 本地大小 vs Hub 显示大小
| 位置 | 显示大小 | 说明 |
|------|---------|------|
| Docker Hub | 29MB | 压缩后的网络传输大小 |
| docker image ls | 78MB | 本地解压后的实际大小 |
### 实际磁盘占用
由于镜像是分层存储不同镜像可能共享相同的层
```
ubuntu:24.04 nginx:latest redis:latest
│ │ │
└───────┬───────┘ │
▼ │
共享基础层 ◄───────────────────┘
```
因此`docker image ls` 中各镜像大小之和 > 实际磁盘占用
### 查看实际空间占用
```bash
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 24 0 1.992GB 1.992GB (100%)
Containers 1 0 62.82MB 62.82MB (100%)
Local Volumes 9 0 652.2MB 652.2MB (100%)
Build Cache 0B 0B
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 15 3 2.5GB 1.8GB (72%)
Containers 5 2 100MB 80MB (80%)
Local Volumes 8 2 500MB 400MB (80%)
Build Cache 0 0 0B 0B
```
## 虚悬镜像
---
上面的镜像列表中还可以看到一个特殊的镜像这个镜像既没有仓库名也没有标签均为 `<none>`
## 过滤镜像
### 按仓库名过滤
```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
$ docker image ls -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB
$ docker images ubuntu:24.04
REPOSITORY TAG IMAGE ID SIZE
ubuntu 24.04 329ed837d508 78MB
```
一般来说虚悬镜像已经失去了存在的价值是可以随意删除的可以用下面的命令删除
### 使用过滤器 --filter
| 过滤条件 | 说明 | 示例 |
|---------|------|------|
| `dangling=true` | 虚悬镜像 | `-f dangling=true` |
| `before=镜像` | 在某镜像之前创建 | `-f before=nginx:latest` |
| `since=镜像` | 在某镜像之后创建 | `-f since=nginx:latest` |
| `label=key=value` | LABEL 过滤 | `-f label=version=1.0` |
| `reference=pattern` | 按名称模式 | `-f reference='*:latest'` |
```bash
# 列出 nginx 之后创建的镜像
$ docker images -f since=nginx:latest
# 列出所有带 latest 标签的镜像
$ docker images -f reference='*:latest'
# 列出带特定 LABEL 的镜像
$ docker images -f label=maintainer=example@email.com
```
---
## 虚悬镜像Dangling Images
### 什么是虚悬镜像
仓库名和标签都显示为 `<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 会利用 **中间层镜像**所以在使用一段时间后可能会看到一些依赖的中间层镜像默认的 `docker image ls` 列表中只会显示顶层镜像如果希望显示包括中间层镜像在内的所有镜像的话需要加 `-a` 参数
### 查看所有镜像包含中间层
```bash
$ docker image ls -a
$ docker images -a
```
这样会看到很多无标签镜像与之前的虚悬镜像不同这些无标签的镜像很多都是中间层镜像是其它镜像依赖的镜像这些无标签镜像不应该删除否则会导致上层镜像因为依赖丢失而出错实际上这些镜像也没必要删除因为之前说过相同的层只会存一遍而这些镜像是别的镜像的依赖因此并不会因为它们被列出来而多存了一份无论如何你也会需要它们只要删除那些依赖它们的镜像后这些依赖的中间层镜像也会被连带删除
会显示很多无标签镜像这些是构建过程中产生的中间层被其他镜像依赖
## 列出部分镜像
> 不要删除中间层镜像它们是其他镜像的依赖删除会导致上层镜像无法使用删除顶层镜像时会自动清理不再需要的中间层
不加任何参数的情况下`docker image ls` 会列出所有顶层镜像但是有时候我们只希望列出部分镜像`docker image ls` 有好几个参数可以帮助做到这个事情
---
根据仓库名列出镜像
## 格式化输出
### 只输出 ID
```bash
$ docker image ls ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 18.04 329ed837d508 3 days ago 63.3MB
ubuntu bionic 329ed837d508 3 days ago 63.3MB
```
列出特定的某个镜像也就是说指定仓库名和标签
```bash
$ docker image ls ubuntu:24.04
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 18.04 329ed837d508 3 days ago 63.3MB
```
除此以外`docker image ls` 还支持强大的过滤器参数 `--filter`或者简写 `-f`之前我们已经看到了使用过滤器来列出虚悬镜像的用法它还有更多的用法比如我们希望看到在 `mongo:3.2` 之后建立的镜像可以用下面的命令
```bash
$ docker image ls -f since=mongo:3.2
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB
nginx latest 05a60462f8ba 5 days ago 181 MB
```
想查看某个位置之前的镜像也可以只需要把 `since` 换成 `before` 即可
此外如果镜像构建时定义了 `LABEL`还可以通过 `LABEL` 来过滤
```bash
$ docker image ls -f label=com.example.version=0.1
...
```
## 以特定格式显示
默认情况下`docker image ls` 会输出一个完整的表格但是我们并非所有时候都会需要这些内容比如刚才删除虚悬镜像的时候我们需要利用 `docker image ls` 把所有的虚悬镜像的 ID 列出来然后才可以交给 `docker image rm` 命令作为参数来删除指定的这些镜像这个时候就用到了 `-q` 参数
```bash
$ docker image ls -q
$ docker images -q
5f515359c7f8
05a60462f8ba
fe9198c04d62
00285df0df87
329ed837d508
329ed837d508
```
`--filter` 配合 `-q` 产生出指定范围的 ID 列表然后送给另一个 `docker` 命令作为参数从而针对这组实体成批的进行某种操作的做法在 Docker 命令行使用过程中非常常见不仅仅是镜像将来我们会在各个命令中看到这类搭配以完成很强大的功能因此每次在文档看到过滤器后可以多注意一下它们的用法
另外一些时候我们可能只是对表格的结构不满意希望自己组织列或者不希望有标题这样方便其它程序解析结果等这就用到了 [Go 的模板语法](https://gohugo.io/templates/introduction/)。
比如下面的命令会直接列出镜像结果并且只包含镜像ID和仓库名
常用于配合其他命令
```bash
$ docker image ls --format "{{.ID}}: {{.Repository}}"
# 删除所有镜像
$ docker rmi $(docker images -q)
# 删除所有 redis 镜像
$ docker rmi $(docker images -q redis)
```
### 显示完整 ID
```bash
$ docker images --no-trunc
```
### 显示摘要
```bash
$ docker images --digests
REPOSITORY TAG DIGEST IMAGE ID
nginx latest sha256:b4f0e0bdeb5... e43d811ce2f4
```
### 自定义格式
使用 Go 模板语法自定义输出
```bash
# 只显示 ID 和仓库名
$ docker images --format "{{.ID}}: {{.Repository}}"
5f515359c7f8: redis
05a60462f8ba: nginx
fe9198c04d62: mongo
00285df0df87: <none>
329ed837d508: ubuntu
329ed837d508: ubuntu
# 表格形式(带标题)
$ docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY TAG SIZE
redis latest 183MB
nginx latest 181MB
ubuntu 24.04 78MB
```
或者打算以表格等距显示并且有标题行和默认一样不过自己定义列
### 可用模板字段
| 字段 | 说明 |
|------|------|
| `.ID` | 镜像 ID |
| `.Repository` | 仓库名 |
| `.Tag` | 标签 |
| `.Digest` | 摘要 |
| `.CreatedSince` | 创建后经过的时间 |
| `.CreatedAt` | 创建时间 |
| `.Size` | 大小 |
---
## 常用命令组合
```bash
$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID REPOSITORY TAG
5f515359c7f8 redis latest
05a60462f8ba nginx latest
fe9198c04d62 mongo 3.2
00285df0df87 <none> <none>
329ed837d508 ubuntu 18.04
329ed837d508 ubuntu bionic
# 列出所有镜像及其大小,按大小排序(需要系统 sort 命令)
$ docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | sort -h
# 查找大于 500MB 的镜像
$ docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | grep -E "^[0-9]+GB|^[5-9][0-9]{2}MB"
# 导出镜像列表
$ docker images --format "{{.Repository}}:{{.Tag}}" > images.txt
```
---
## 本章小结
| 操作 | 命令 |
|------|------|
| 列出所有镜像 | `docker images` |
| 按仓库名过滤 | `docker images nginx` |
| 列出虚悬镜像 | `docker images -f dangling=true` |
| 只输出 ID | `docker images -q` |
| 显示摘要 | `docker images --digests` |
| 自定义格式 | `docker images --format "..."` |
| 查看空间占用 | `docker system df` |
## 延伸阅读
- [获取镜像](pull.md) Registry 拉取镜像
- [删除镜像](rm.md)清理本地镜像
- [镜像](../basic_concept/image.md)理解镜像概念

View File

@@ -1,23 +1,59 @@
# 获取镜像
之前提到过[Docker Hub](https://hub.docker.com/search?q=&type=image) 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像。
## docker pull 命令
Docker 镜像仓库获取镜像的命令是 `docker pull`其命令格式为
从镜像仓库获取镜像的命令是 `docker pull`
```bash
$ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
docker pull [选项] [Registry地址/]仓库名[:标签]
```
具体的选项可以通过 `docker pull --help` 命令看到这里我们说一下镜像名称格式
### 镜像名称格式
* Docker 镜像仓库地址地址的格式一般是 `<域名/IP>[:端口号]`默认地址是 Docker Hub(`docker.io`)
* 仓库名如之前所说这里的仓库名是两段式名称 `<用户名>/<软件名>`对于 Docker Hub如果不给出用户名则默认为 `library`也就是官方镜像
```
docker.io / library / ubuntu : 24.04
────┬──── ───┬─── ──┬─── ──┬──
│ │ │ │
Registry地址 用户名 仓库名 标签
(可省略) (可省略)
```
比如
| 组成部分 | 说明 | 默认值 |
|---------|------|--------|
| Registry 地址 | 镜像仓库地址 | `docker.io`Docker Hub |
| 用户名 | 镜像所属用户/组织 | `library`官方镜像 |
| 仓库名 | 镜像名称 | 必须指定 |
| 标签 | 版本标识 | `latest` |
### 示例
```bash
# 完整格式
$ docker pull docker.io/library/ubuntu:24.04
# 省略 Registry默认 Docker Hub
$ docker pull library/ubuntu:24.04
# 省略 library官方镜像
$ docker pull ubuntu:24.04
# 省略标签(默认 latest
$ docker pull ubuntu
# 拉取第三方镜像
$ docker pull bitnami/redis:latest
# 从其他 Registry 拉取
$ docker pull ghcr.io/username/myapp:v1.0
```
---
## 下载过程解析
```bash
$ docker pull ubuntu:24.04
18.04: Pulling from library/ubuntu
24.04: Pulling from library/ubuntu
92dc2a97ff99: Pull complete
be13a9d27eb8: Pull complete
c8299583700a: Pull complete
@@ -26,43 +62,171 @@ Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04
```
上面的命令中没有给出 Docker 镜像仓库地址因此将会从 Docker Hub `docker.io`获取镜像而镜像名称是 `ubuntu:24.04`因此将会获取官方镜像 `library/ubuntu` 仓库中标签为 `24.04` 的镜像`docker pull` 命令的输出结果最后一行给出了镜像的完整名称 `docker.io/library/ubuntu:24.04`
### 输出解读
从下载过程中可以看到我们之前提及的分层存储的概念镜像是由多层存储所构成下载也是一层层的去下载并非单一文件下载过程中给出了每一层的 ID 的前 12 并且下载结束后给出该镜像完整的 `sha256` 的摘要以确保下载一致性
| 输出内容 | 说明 |
|---------|------|
| `Pulling from library/ubuntu` | 正在从官方 ubuntu 仓库拉取 |
| `92dc2a97ff99: Pull complete` | 各层的下载状态显示层 ID 12 |
| `Digest: sha256:...` | 镜像内容的唯一摘要 |
| `docker.io/library/ubuntu:24.04` | 镜像的完整名称 |
在使用上面命令的时候你可能会发现你所看到的层 ID 以及 `sha256` 的摘要和这里的不一样这是因为官方镜像是一直在维护的有任何新的 bug或者版本更新都会进行修复再以原来的标签发布这样可以确保任何使用这个标签的用户可以获得更安全更稳定的镜像
### 分层下载
*如果从 Docker Hub 下载镜像非常缓慢可以参照 [镜像加速器](/install/mirror.md) 一节配置加速器*
从输出可以看到镜像是**分层下载**
## 运行
有了镜像后我们就能够以这个镜像为基础启动并运行一个容器以上面的 `ubuntu:24.04` 为例如果我们打算启动里面的 `bash` 并且进行交互式操作的话可以执行下面的命令
```bash
$ docker run -it --rm ubuntu:24.04 bash
root@e7009c6ce357:/# cat /etc/os-release
NAME="Ubuntu"
VERSION="24.04 LTS (Noble Numbat)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 24.04 LTS"
VERSION_ID="24.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=noble
UBUNTU_CODENAME=noble
```
┌─────────────────────────────────────────────────────────────┐
│ ubuntu:24.04 镜像 │
├─────────────────────────────────────────────────────────────┤
│ 第3层 c8299583700a ───────► 已存在,跳过下载 │
├─────────────────────────────────────────────────────────────┤
│ 第2层 be13a9d27eb8 ───────► 下载中... 完成 │
├─────────────────────────────────────────────────────────────┤
│ 第1层 92dc2a97ff99 ───────► 下载中... 完成 │
└─────────────────────────────────────────────────────────────┘
```
`docker run` 就是运行容器的命令具体格式我们会在 [容器](../container) 一节进行详细讲解我们这里简要的说明一下上面用到的参数
如果本地已有相同的层Docker 会跳过下载节省带宽和时间
* `-it`这是两个参数一个是 `-i`交互式操作一个是 `-t` 终端我们这里打算进入 `bash` 执行一些命令并查看返回结果因此我们需要交互式终端
* `--rm`这个参数是说容器退出后随之将其删除默认情况下为了排障需求退出的容器并不会立即删除除非手动 `docker rm`我们这里只是随便执行个命令看看结果不需要排障和保留结果因此使用 `--rm` 可以避免浪费空间
* `ubuntu:24.04`这是指用 `ubuntu:24.04` 镜像为基础来启动容器
* `bash`放在镜像名后的是 **命令**这里我们希望有个交互式 Shell因此用的是 `bash`
---
进入容器后我们可以在 Shell 下操作执行任何所需的命令这里我们执行了 `cat /etc/os-release`这是 Linux 常用的查看当前系统版本的命令从返回的结果可以看到容器内是 `Ubuntu 24.04 LTS` 系统
## 常用选项
最后我们通过 `exit` 退出了这个容器
| 选项 | 说明 | 示例 |
|------|------|------|
| `--all-tags, -a` | 拉取所有标签 | `docker pull -a ubuntu` |
| `--platform` | 指定平台架构 | `docker pull --platform linux/arm64 nginx` |
| `--quiet, -q` | 静默模式 | `docker pull -q nginx` |
### 指定平台
Apple Silicon Mac 上拉取 x86 镜像
```bash
$ docker pull --platform linux/amd64 nginx
```
---
## 拉取后运行
拉取镜像后可以基于它启动容器
```bash
# 拉取镜像
$ docker pull ubuntu:24.04
# 运行容器
$ docker run -it --rm ubuntu:24.04 bash
root@e7009c6ce357:/# cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04 LTS"
...
root@e7009c6ce357:/# exit
```
**参数说明**
| 参数 | 说明 |
|------|------|
| `-it` | 交互式终端模式 |
| `--rm` | 退出后自动删除容器 |
| `bash` | 启动命令 |
> 💡 `docker run` 在需要时会自动 `pull` 镜像因此通常不需要单独执行 `docker pull`
---
## 镜像加速
Docker Hub 下载可能较慢可以配置镜像加速器
```json
// /etc/docker/daemon.json (Linux)
// ~/.docker/daemon.json (Docker Desktop)
{
"registry-mirrors": [
"https://your-accelerator-url"
]
}
```
配置后重启 Docker
```bash
$ sudo systemctl restart docker # Linux
# 或在 Docker Desktop 中重启
```
详见 [镜像加速器](../install/mirror.md) 章节
---
## 验证镜像完整性
### 查看镜像摘要
```bash
$ docker images --digests ubuntu
REPOSITORY TAG DIGEST IMAGE ID
ubuntu 24.04 sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26 ca2b0f26964c
```
### 使用摘要拉取
用摘要拉取可确保获取完全相同的镜像
```bash
$ docker pull ubuntu@sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
```
> 笔者建议生产环境使用摘要而非标签因为标签可能被覆盖摘要则是不可变的
---
## 常见问题
### Q: 下载速度很慢
1. 配置镜像加速器
2. 检查网络连接
3. 尝试拉取更小的镜像版本 `alpine` 变体
### Q: 提示镜像不存在
```bash
Error: pull access denied, repository does not exist
```
可能原因
- 镜像名拼写错误
- 私有镜像未登录需要 `docker login`
- 镜像确实不存在
### Q: 磁盘空间不足
```bash
# 清理未使用的镜像
$ docker image prune
# 清理所有未使用资源
$ docker system prune
```
---
## 本章小结
| 操作 | 命令 |
|------|------|
| 拉取镜像 | `docker pull 镜像名:标签` |
| 拉取所有标签 | `docker pull -a 镜像名` |
| 指定平台 | `docker pull --platform linux/amd64 镜像名` |
| 用摘要拉取 | `docker pull 镜像名@sha256:...` |
## 延伸阅读
- [列出镜像](list.md)查看本地镜像
- [删除镜像](rm.md)清理本地镜像
- [镜像加速器](../install/mirror.md)加速镜像下载
- [Docker Hub](../repository/dockerhub.md)官方镜像仓库

View File

@@ -1,87 +1,255 @@
# 删除本地镜像
如果要删除本地的镜像可以使用 `docker image rm` 命令其格式为
## 基本用法
使用 `docker image rm` 删除本地镜像
```bash
$ docker image rm [选项] <镜像1> [<镜像2> ...]
```
## ID镜像名摘要删除镜像
> 💡 `docker rmi` `docker image rm` 的简写两者等效
其中`<镜像>` 可以是 `镜像短 ID``镜像长 ID``镜像名` 或者 `镜像摘要`
---
比如我们有这么一些镜像
## 镜像标识方式
删除镜像时可以使用多种方式指定镜像
| 方式 | 说明 | 示例 |
|------|------|------|
| ** ID** | ID 的前几位通常 3-4 | `docker rmi 501` |
| ** ID** | 完整的镜像 ID | `docker rmi 501ad78535f0...` |
| **镜像名:标签** | 仓库名和标签 | `docker rmi redis:alpine` |
| **镜像摘要** | 精确的内容摘要 | `docker rmi nginx@sha256:...` |
### 使用短 ID 删除
```bash
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB
redis alpine 501ad78535f0 3 weeks ago 21.03 MB
docker latest cf693ec9b5c7 3 weeks ago 105.1 MB
nginx latest e43d811ce2f4 5 weeks ago 181.5 MB
REPOSITORY TAG IMAGE ID SIZE
redis alpine 501ad78535f0 30MB
nginx latest e43d811ce2f4 142MB
# 只需输入足够区分的前几位
$ docker rmi 501
Untagged: redis:alpine
Deleted: sha256:501ad78535f0...
```
我们可以用镜像的完整 ID也称为 `长 ID`来删除镜像使用脚本的时候可能会用长 ID但是人工输入就太累了所以更多的时候是用 `短 ID` 来删除镜像`docker image ls` 默认列出的就已经是短 ID 一般取前3个字符以上只要足够区分于别的镜像就可以了
比如这里如果我们要删除 `redis:alpine` 镜像可以执行
### 使用镜像名删除
```bash
$ docker image rm 501
$ docker rmi redis:alpine
Untagged: redis:alpine
Deleted: sha256:501ad78535f0...
```
### 使用摘要删除
摘要删除最精确适用于 CI/CD 场景
```bash
# 查看镜像摘要
$ docker images --digests
REPOSITORY TAG DIGEST IMAGE ID
nginx latest sha256:b4f0e0bdeb5... e43d811ce2f4
# 使用摘要删除
$ docker rmi nginx@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
```
---
## 理解输出信息
删除镜像时会看到两类信息**Untagged** **Deleted**
```bash
$ docker rmi redis:alpine
Untagged: redis:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7
```
我们也可以用`镜像名`也就是 `<仓库名>:<标签>`来删除镜像
### Untagged vs Deleted
| 操作 | 含义 |
|------|------|
| **Untagged** | 移除镜像的标签 |
| **Deleted** | 删除镜像的存储层 |
### 删除流程
```
docker rmi redis:alpine
┌───────────────────────────────────────────────────────────────┐
│ 1. Untag移除 redis:alpine 标签 │
│ ↓ │
│ 2. 检查是否还有其他标签指向这个镜像 │
│ ├── 有 → 只 Untag不删除 │
│ └── 无 → │
│ ↓ │
│ 3. 检查是否有容器依赖 │
│ ├── 有 → 报错,无法删除 │
│ └── 无 → │
│ ↓ │
│ 4. 从上到下逐层删除,检查每层是否被其他镜像使用 │
│ ├── 被使用 → 保留 │
│ └── 未使用 → Deleted │
└───────────────────────────────────────────────────────────────┘
```
---
## 批量删除
### 删除所有虚悬镜像
虚悬镜像dangling没有标签的镜像通常是旧版本被新版本覆盖后产生的
```bash
$ docker image rm centos
Untagged: centos:latest
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38
# 查看虚悬镜像
$ docker images -f dangling=true
# 删除虚悬镜像
$ docker image prune
# 不提示确认
$ docker image prune -f
```
当然更精确的是使用 `镜像摘要` 删除镜像
### 删除所有未使用的镜像
```bash
$ docker image ls --digests
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
node slim sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 6e0c4c8e3913 3 weeks ago 214 MB
# 删除所有没有被容器使用的镜像
$ docker image prune -a
$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
# 保留最近 24 小时的
$ docker image prune -a --filter "until=24h"
```
## Untagged Deleted
如果观察上面这几个命令的运行输出信息的话你会注意到删除行为分为两类一类是 `Untagged`另一类是 `Deleted`我们之前介绍过镜像的唯一标识是其 ID 和摘要而一个镜像可以有多个标签
因此当我们使用上面命令删除镜像的时候实际上是在要求删除某个标签的镜像所以首先需要做的是将满足我们要求的所有镜像标签都取消这就是我们看到的 `Untagged` 的信息因为一个镜像可以对应多个标签因此当我们删除了所指定的标签后可能还有别的标签指向了这个镜像如果是这种情况那么 `Delete` 行为就不会发生所以并非所有的 `docker image rm` 都会产生删除镜像的行为有可能仅仅是取消了某个标签而已
当该镜像所有的标签都被取消了该镜像很可能会失去了存在的意义因此会触发删除行为镜像是多层存储结构因此在删除的时候也是从上层向基础层方向依次进行判断删除镜像的多层结构让镜像复用变得非常容易因此很有可能某个其它镜像正依赖于当前镜像的某一层这种情况依旧不会触发删除该层的行为直到没有任何层依赖当前层时才会真实的删除当前层这就是为什么有时候会奇怪为什么明明没有别的标签指向这个镜像但是它还是存在的原因也是为什么有时候会发现所删除的层数和自己 `docker pull` 看到的层数不一样的原因
除了镜像依赖以外还需要注意的是容器对镜像的依赖如果有用这个镜像启动的容器存在即使容器没有运行那么同样不可以删除这个镜像之前讲过容器是以镜像为基础再加一层容器存储层组成这样的多层存储结构去运行的因此该镜像如果被这个容器所依赖的那么删除必然会导致故障如果这些容器是不需要的应该先将它们删除然后再来删除镜像
## docker image ls 命令来配合
像其它可以承接多个实体的命令一样可以使用 `docker image ls -q` 来配合使用 `docker image rm`这样可以成批的删除希望删除的镜像我们在镜像列表章节介绍过很多过滤镜像列表的方式都可以拿过来使用
比如我们需要删除所有仓库名为 `redis` 的镜像
### 按条件删除
```bash
$ docker image rm $(docker image ls -q redis)
# 删除所有 redis 镜像
$ docker rmi $(docker images -q redis)
# 删除 mongo:3.2 之前的所有镜像
$ docker rmi $(docker images -q -f before=mongo:3.2)
# 删除某个时间之前的镜像
$ docker image prune -a --filter "until=168h" # 7天前
```
或者删除所有在 `mongo:3.2` 之前的镜像
---
## 删除失败的常见原因
### 原因一有容器依赖
```bash
$ docker image rm $(docker image ls -q -f before=mongo:3.2)
$ docker rmi nginx
Error: conflict: unable to remove repository reference "nginx"
(must force) - container abc123 is using its referenced image
```
充分利用你的想象力和 Linux 命令行的强大你可以完成很多非常赞的功能
**解决方案**
```bash
# 方案1先删除依赖的容器
$ docker rm abc123
$ docker rmi nginx
# 方案2强制删除镜像容器仍可运行但无法再创建新容器
$ docker rmi -f nginx
```
### 原因二多个标签指向同一镜像
```bash
$ docker images
REPOSITORY TAG IMAGE ID
ubuntu 24.04 ca2b0f26964c
ubuntu latest ca2b0f26964c # 同一个镜像
$ docker rmi ubuntu:24.04
Untagged: ubuntu:24.04
# 只是移除标签,镜像仍存在(因为还有 ubuntu:latest 指向它)
```
### 原因三被其他镜像依赖中间层
```bash
$ docker rmi some_base_image
Error: image has dependent child images
```
中间层镜像被其他镜像依赖无法删除需要先删除依赖它的镜像
---
## 常用过滤条件
| 过滤条件 | 说明 | 示例 |
|---------|------|------|
| `dangling=true` | 虚悬镜像 | `-f dangling=true` |
| `before=镜像` | 在某镜像之前 | `-f before=mongo:3.2` |
| `since=镜像` | 在某镜像之后 | `-f since=mongo:3.2` |
| `label=key=value` | 按标签过滤 | `-f label=version=1.0` |
| `reference=pattern` | 按名称模式 | `-f reference='*:latest'` |
---
## 清理策略
### 开发环境
```bash
# 定期清理虚悬镜像
$ docker image prune -f
# 一键清理所有未使用资源
$ docker system prune -a
```
### CI/CD 环境
```bash
# 只保留最近使用的镜像
$ docker image prune -a --filter "until=72h" -f
```
### 查看空间占用
```bash
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 15 3 2.5GB 1.8GB (72%)
Containers 5 2 100MB 80MB (80%)
Local Volumes 8 2 500MB 400MB (80%)
Build Cache 0 0 0B 0B
```
---
## 本章小结
| 操作 | 命令 |
|------|------|
| 删除指定镜像 | `docker rmi 镜像名:标签` |
| 强制删除 | `docker rmi -f 镜像名` |
| 删除虚悬镜像 | `docker image prune` |
| 删除未使用镜像 | `docker image prune -a` |
| 批量删除 | `docker rmi $(docker images -q -f ...)` |
| 查看空间占用 | `docker system df` |
## 延伸阅读
- [列出镜像](list.md)查看和过滤镜像
- [删除容器](../container/rm.md)清理容器
- [数据卷](../data_management/volume.md)清理数据卷