Docker Note
本文最后更新于 2026年7月19日 凌晨
序
Docker——学过,看过,用过,都很零散,这次决定系统梳理一遍。
梳理一下我不太熟悉的东西:
- Docker Compose
- Docker 的网络相关
这次着重补一下这两块。
学习主线使用《Docker 从入门到实践》这本书,辅可能会有读到的文档和看的教学视频的内容加以补充。
Docker 简介
- 轻量级的虚拟化技术
- 将应用程序及其依赖环境可以被打包成一个标准化的单元,在满足架构、内核能力和外部依赖前提的环境中高度一致地运行。
和虚拟机的差别
传统虚拟机:
- 虚拟出一套完整的硬件,在其之上运行一个完整的 OS
Docker 容器:
- 容器运行与宿主的内核,容器没有自己的内核,没有对于硬件的虚拟
| 特性 | Docker 容器 | 传统虚拟机 |
|---|---|---|
| 启动速度 | 秒级 | 分钟级 |
| 资源占用 | MB 级别 | GB 级别 |
| 性能 | 接近原生 | 有明显损耗 |
| 隔离级别 | 进程级隔离 | 完全隔离 |
| 单机数量 | 可运行上千个 | 通常几十个 |
Docker 的优势
- 一次构建,到处运行
- 环境一致性
- 启动速度快
- 对 CI/CD,弹性扩容很好
- 资源效率相比虚拟机高
- 持续交付和部署
- 契合 DevOps 的工作流程:代码提交、
- 轻松迁移
- 微服务架构的基石
Docker 概念
镜像
-
只读的模板,包含运行应用所需要的一切。
镜像只读,不包含动态数据,构建之后内容不改变。
-
一个镜像可以创建多个容器,而镜像本身保持不变。
镜像与 OS 的关系
OS 分为内核和用户空间:
flowchart TD
subgraph UserSpace ["用户空间"]
direction TB
App["应用程序、工具、库、配置文件...<br/>(这部分被打包成 Docker 镜像)"]
end
subgraph KernelSpace ["Linux 内核"]
direction TB
Kernel["容器共享宿主机的内核"]
end
UserSpace --- KernelSpace
Docker 容器:本质是一个自己的 root 文件系统,挂载在 OS 的内核之下运行,不包含自己的内核。
分层存储
分成很多层,每一层都是基于上一层运行的。
体现在 Dockerfile 的不同的行中。
举一个陷阱式的例子来理解:
## 错误示范 ❌
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 的编译工具!## 正确做法 ✅
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/*
## 在同一层完成安装、使用、清理镜像的标识
多种标识方式
-
镜像名称和标签:
- 格式:
[仓库地址/]仓库名[:标签],但也有缩写形式,举例: - 完整格式
registry.example.com/myproject/myapp:v1.2.3 - 简写,默认仓库地址为官方的 Docker Hub:
nginx:1.28 - 省略标签,
nginx,等价于nginx:latest
- 格式:
-
镜像 ID
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE nginx latest a6bd71f48f68 2 weeks ago 187MB ubuntu 24.04 ca2b0f26964c 3 weeks ago 78.1MB此处的
IMAGE ID即为。 -
摘要
- digest
- 镜像内容的唯一标识
- 推荐使用这个代替标签
容器
- 容器是镜像的运行实例。
- 如果把镜像比作程序,那么容器就是进程。
- 用 OOP 术语来说:镜像是类 (Class),容器是对象 (Instance)。
容器的特征:
- 一个镜像可以创建多个容器
- 每个容器相互独立,互不影响
- 容器可以被创建、启动、停止、删除、暂停
容器的本质
容器的本质是一个特殊的进程。但是与一般的进程相比,有如下特质:
- 独立的进程空间:容器看不到宿主机上的其他进程。
- 独立网络环境:在默认网络模式下,容器通常拥有独立的网络命名空间,并可分配独立 IP;使用
host或container:等模式时则例外。 - 独立文件系统:容器拥有独立的 root 目录。
- 独立的用户空间:鉴于我不是很理解 Linux 的用户空间,这里暂且 TODO。
容器的存储层
镜像层+容器层:假设容器基于的镜像有 $N$ 层,那么容器就是构建一个第 $N+1$ 层,可写,作为容器存储层。
Copy-on-Write 写时复制:
当容器需要修改镜像层中的文件时:
- Docker 将该文件 复制 到容器存储层
- 在容器层中进行修改
- 原始镜像层保持不变
容器存储层的生命周期:和容器绑定,删除容器同时删除容器存储层。
数据持久化范式:Docker 的最佳实践认为容器存储层应该保持无状态性,如果需要保留,应当使用数据卷 / 绑定挂载。
容器的生命周期
存在的状态:
- Created
- Running
- Paused
- Stopped
- Deleted
容器和进程的关系
容器的生命周期 = 主进程 (PID 1) 的生命周期
运行:
docker run ubuntu容器会立刻退出:默认启动的程序没有持续工作,PID 1 很快结束了。使用:
docker run -it ubuntu bash则 bash 成为 PID 1;只要你不退出这个 shell,容器就会继续运行。
容器的隔离是如何实现的
使用 Linux 的 Namespace 机制。
Docker Registry
核心概念
Docker Registry 是存储和分发 Docker 镜像的服务,类似于代码的 GitHub 或包管理的 npm。
Docker Registry 中可以包含多个 Repository,每个 Repository 可以包含多个 Tag。
这也就是为什么,镜像的完整名称是 [registry 地址/][用户名/]仓库名[:标签]。
如果不指定 Registry 地址,默认使用 Docker Hub。如果不指定标签,默认使用 latest。
公共 Registry 服务
Docker Hub 是最大的公共 Registry,也是 Docker 的默认 Registry。
- 免费账户可以创建公开仓库
- 免费个人账户可创建 1 个私有仓库;更高套餐支持更多私有仓库
还有一些镜像源比如 Github Container Registry,Google 的,阿里巴巴的,腾讯的等。
可以配置镜像加速器来加速:
// /etc/docker/daemon.json
{
"registry-mirrors": [
"https://your-accelerator-url"
]
}私有 Registry
云厂商有提供服务,可以自己看,比如阿里云 ACR、腾讯云 TCR、AWS ECR 等。
有时需要使用 docker login 来登录到 Docker Registry。
镜像的推送和拉取
开发者机器 Registry 生产服务器
│ │ │
│ docker build │ │
│ 构建镜像 │ │
│ │ │
│ docker push ─────────────▶ │
│ 推送镜像 │ 存储镜像 │
│ │ │
│ │ ◀───────────── docker pull │
│ │ 拉取镜像 │
│ │ │
│ │ docker run │
│ │ 运行容器 │使用镜像
获取镜像
代码
## 完整格式
$ 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下载内容解析
$ docker pull ubuntu:24.04
24.04: Pulling from library/ubuntu
92dc2a97ff99: Pull complete
be13a9d27eb8: Pull complete
c8299583700a: Pull complete
Digest: sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04| 输出内容 | 说明 |
|---|---|
Pulling from library/ubuntu |
正在从官方 ubuntu 仓库拉取 |
92dc2a97ff99: Pull complete |
各层的下载状态 (显示层 ID 前 12 位) |
Digest: sha256:... |
镜像内容的唯一摘要 |
docker.io/library/ubuntu:24.04 |
镜像的完整名称 |
可以看到镜像是分层下载的。
对于 pull:
--quiet -q可以静默安装--platform可以指定平台架构
关于摘要
查看镜像摘要:
$ docker images --digests ubuntu
REPOSITORY TAG DIGEST IMAGE ID
ubuntu 24.04 sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26 ca2b0f26964c然后使用摘要拉取:
$ docker pull ubuntu@sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26生产环境使用摘要而非标签,因为标签可能被覆盖,摘要则是不可变的。
磁盘空间不足的解决方案
## 清理未使用的镜像
$ docker image prune
## 清理所有未使用资源
$ docker system prune查看镜像
基本用法
$ docker images
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关于 ubuntu:24.04 和 ubuntu:noble:
ubuntu:24.04 是具体版本号,ubuntu:noble 是发布代号。
拥有相同的 IMAGE ID,是同一个镜像的不同标签,只占用一份存储空间。
其实基本命令是 docker image,而 docker images 是 docker image ls 的 alias。
理解镜像的大小
本地大小和 Docker Hub 显示的大小:
- 前者是本地解压后的实际大小
- 后者是压缩后的网络传输大小
实际磁盘占用:
- 由于镜像分层存储,不同的镜像共享不同的层
- 故 $\sum{\text{size}} \gt \text{实际磁盘占用}$。
如何查看实际空间的占用:
$ 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 images ubuntu
$ docker images ubuntu:24.04使用过滤器:
--filter -f
有一系列的过滤条件,这个感觉记了也记不住,知道就好。
虚悬镜像 dangling images
仓库名和标签都显示为 <none> 的镜像。
产生原因:
- 镜像重新构建:新镜像使用了旧镜像的标签,旧镜像标签被移除
- docker pull 更新:拉取更新版本时,旧版本失去标签(
latest)
处理方式:
- 列出:
docker images -f dangling=true - 删除:
docker image prune
中间层镜像
docker images 只列出顶层镜像,为了查看中间镜像,使用 docker images -a。
永远不需要手动删除中间层镜像。
格式化输出
主要是命令之间的搭配使用。
只输出 ID:docker images -q
举例:删除所有 redis 镜像:docker rmi $(docker images -q redis)
不过其实我觉得个人开发而言,用 Docker Desktop GUI 管理更方便。
对于 Agent 而言,这些东西自然语言阐述即可。
显示摘要:docker images --digests
删除镜像
基本用法
$ docker image rm [选项] <镜像1> [<镜像2> ...]docker rmi 是 docker image rm 的简写,两者等效。
镜像标识方式
- 短 ID(可以标识的前几位)
- 完整 ID
- 镜像名:标签
- 镜像摘要
- 最精确,适合 CI/CD 场景
删除镜像的输出信息
$ docker rmi redis:alpine
Untagged: redis:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23- Untagged:移除镜像的标签
- Deleted:删除镜像的存储层
删除流程:
先检查指向该镜像的标签并逐个 untag,然后再逐层删除镜像,被使用则保留,不被使用则删除。
批量删除
这里大概自然语言描述一下能做到哪些吧:
- 删除所有虚悬镜像,
docker image prune - 删除一段时间内未使用的镜像:
docker image prune -a --filter "until=24h" - 按照条件删除,比如:
- 删除所有 redis 镜像
- 删除某个时间点之前的镜像
- 删除某个版本号之前的镜像
删除失败的原因
- 存在容器依赖这个镜像
- 这个镜像作为中间层被其他镜像依赖
- 多个标签指向同一个镜像,比如 ubuntu 的
24.04和latest同时指向同一个IMAGE ID
清理策略
开发环境:
- 定期清理 dangling images
- 清理未使用的资源
docker system prune -a
CI/CD:
- 只保留最近使用过的镜像(即一键删除一段时间内未使用过的镜像)
查看空间占用:
docker system df
使用 docker commit
把某个容器当前在文件系统上的改动,固化成一个新的 Docker 镜像。
命令格式:
$ docker commit [OPTIONS] 容器名或容器ID 新镜像名[:标签]举个例子,进入容器之后,修改了里面文件系统的一些内容,
执行
docker commit dev my-ubuntu:v1实际上做的:
原镜像的只读层 + dev 容器的可写层 -> 转化成新的只读镜像层 -> 新的镜像
原理是让新镜像继续复用原来的镜像层,再增加一层,用来保存这个容器相对于原镜像产生的文件系统变化。
缺点
使用 docker commit 定制的镜像存在如下问题:
- 执行命令会连着根式地修改一大堆无关的文件,可以通过
docker diff webserver看出; - 对镜像的操作都是黑箱操作;
- 层数会膨胀
使用 Dockerfile 定制镜像
解决上面所说的问题。
Dockerfile 是一个包含了一条条的指令的文本文件:
- 会修改文件系统的指令通常会创建新层;
- 而
LABEL、CMD这类只修改镜像元数据的指令,则不会新增文件系统层。
创建 Dockerfile
比较详细的命令后面单开一节介绍。
不过我觉得也没什么必要,知道基本概念最重要。
FROM 指定基础镜像:
所谓定制镜像,是以一个镜像为基础,在其上进行定制。所以需要 FROM 指定的基础镜像。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
FROM scratch
...如果以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言开发的应用很多会使用这种方式来制作镜像,这也是有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
RUN 执行命令行命令:
- shell 格式:
RUN <命令> - exec 格式:
RUN ["可执行文件", "参数1", "参数2"]
每一个 RUN 指令都会产生一个新的镜像层。为了减少镜像体积和层数,我们通常会将多个命令合并到一个 RUN 指令中执行。
构建镜像
docker build [选项] <上下文路径/URL/->执行之后的输出,含义还挺明显的,没什么必要单独扯出来记。
镜像构建上下文
关于什么是上下文路径:构建器可以访问到的文件集合。
比如 Dockerfile 里面会写 COPY 和 RUN 指令,假设有这样的:
COPY ./package.json /app/这里复制的就是上下文目录下的 package.json。
而假设写成:
COPY ../package.json /app这里的 .. 属于越界,构建器都无法读取上下文之外的宿主机文件。
由此可见,上下文,和 Dockerfile 所在的目录,没有关系。假设把上下文直接指定成 / ,理论是可行的,不过 BuildKit 的可见上下文会过大导致构建缓慢甚至失败。
而所谓的 .dockerignore 文件(通常置于项目根目录下,和 Dockerfile 平级,采用和 .gitignore 一样的 Glob 语法),就是为了忽略掉在构建时不希望传给 Docker 引擎的文件。
其余 docker build 的用法
- 从 Git Repo 中构建
- 用给定的压缩包构建
- 从标准输入中读取 Dockerfile 进行构建
cat Dockerfile | docker build -
操作容器
启动
启动容器有两种方式:
- 新建并启动:基于镜像创建新容器
docker run - 重新启动:将已终止的容器重新运行
docker start
由于 Docker 容器非常轻量,实际使用中常常是随时删除和新建容器,而不是反复重启同一个容器。
新建并启动
docker run [选项] 镜像 [命令] [参数...]交互式容器:
$ docker run -it ubuntu:24.04 /bin/bash
root@af8bae53bdd3:/#| 参数 | 作用 |
|---|---|
-i |
保持标准输入 (stdin) 打开,允许输入 |
-t |
分配伪终端 (pseudo-TTY),提供终端界面 |
-it |
两者组合使用,获得交互式终端 |
启动选项
| 选项 | 说明 | 示例 |
|---|---|---|
-d |
后台运行 (detach) | docker run -d nginx:latest |
-it |
交互式终端 | docker run -it ubuntu:24.04 bash |
--name |
指定容器名称 | docker run --name myapp nginx:latest |
--rm |
退出后自动删除容器 | docker run --rm ubuntu:24.04 echo hi |
端口映射:
## 将容器的 80 端口映射到宿主机的 8080 端口
$ docker run -d -p 8080:80 nginx:latest
## 只绑定到 localhost
$ docker run -d -p 127.0.0.1:8080:80 nginx:latest映射端口这一点很重要,比如像 nginx 等服务,不暴露出端口,外部根本无法访问。
网络相关的东西,等到网络专题再细究吧。
数据卷挂载
略,懒得记了
环境变量
## 设置单个环境变量
$ docker run -e MYSQL_ROOT_PASSWORD=secret mysql
## 从文件加载环境变量
$ docker run --env-file .env myapp这个倒是,容器有时候是需要诸如 .env 文件里面的环境变量的,假设 .dockerignore 了之后。
但是感觉把 .env 文件 docker ignore 掉的意义也不大,为什么要这样做呢?
Answer from ChatGPT,我归纳一下:
首先,肯定不能把 .env 放进镜像中啊(回忆一下本节的标题「新建并启动」),因为镜像是要分发的,分发出去,直接访问里面的 .env 文件不完蛋了,所以肯定要忽略掉。
其次,这样做的话,镜像和运行环境就解耦合了。
资源限制:
## 限制内存
$ docker run -m 512m nginx:latest
## 限制 CPU
$ docker run --cpus=1.5 nginx:latest重新启动容器
使用 docker start,后面跟上容器名。
获取容器名,使用 docker ps -a。
守护态运行
当在终端运行一个程序时,有两种模式:
- 前台运行:程序占用当前终端,输出直接显示,关闭终端程序就停止
- 后台运行:程序在后台执行,不占用终端,终端关闭也不影响程序
Docker 容器默认是 前台运行 的。使用 -d (detach) 参数可以让容器在后台运行。
理解为什么容器会立即退出
核心原理:容器的生命周期与主进程绑定。
举个例子,即使使用了 -d 参数运行:
$ docker run -d ubuntu:24.04再使用 docker ps 也看不见容器在运行,原因:
- 容器启动
- 没有指定命令,默认执行
/bin/bash - 但没有交互式终端 (没有
-it参数),bash 发现没有输入源 - bash 立即退出
- 主进程退出,容器停止
-d 参数是让容器 “在后台运行”,能运行多久取决于主进程
查看容器
$ docker container ls
$ docker ps
# 前者是后者的简写
# 查看日志
$ docker container logs 77b2dc01fe0f
# 实时查看日志
$ docker container logs -f 77b2dc01fe0f对于一次性任务,使用 --rm 参数让容器退出后自动删除。
终止
docker stop 优雅终止
docker kill 直接终止
进入容器
-d 启动之后,有时候需要进入容器进行操作。
## 进入容器并启动交互式 shell
$ docker exec -it 容器名 /bin/bash
## 或使用 sh(适用于 Alpine 等精简镜像)
$ docker exec -it 容器名 /bin/sh导入和导出
docker export docker import
删除
docker rm
docker rm 是 docker container rm 的简写,两者等效。
Dockerfile 指令详解
数据管理
数据卷
首先需要 $ docker volume create my-vol 创建一个数据卷,然后新建容器的时候,使用 --mount 或者 -v 挂载数据卷。
挂载主机目录
数据卷(volume):由 Docker 管理的存储。你只需要指定卷名,Docker 负责在主机上创建、保存和定位实际数据目录。
docker run -v mydata:/app/data nginx
# 名为 mydata 的数据卷,映射到容器的 /app/data 目录挂载主机目录(bind mount):把你指定的主机目录直接映射到容器中。
docker run -v /home/user/data:/app/data nginx核心区别:
- volume:你管卷名,Docker 管实际存放位置,适合数据库等持久化数据。
- bind mount:你自己管理主机路径,适合开发时挂载代码、配置文件。
- 可移植性:volume 不依赖具体主机路径;bind mount 依赖主机目录结构。