Docker 基础体系 · 第 22/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker CLI 与对象模型:Container、Image、Network、Volume 和 Filter

Docker CLI 不是直接操作“容器进程”的一组快捷命令,而是通过 Docker Engine API 请求 Docker daemon 管理一组具有明确生命周期和引用关系的对象。本文以现代 Docker Engine、BuildKit 和 Compose 规范为基础,讨论 Linux 容器场景下最重要的五类对象:

  • Image:用于创建容器的不可变内容模板;
  • Container:某个 Image 的一次可运行实例,包含配置、状态和可写层;
  • Network:连接容器与外部网络的虚拟网络对象;
  • Volume:由 Docker 管理的持久化数据对象;
  • Filter:由 CLI 传递给对象查询命令的筛选条件。

理解这些对象之间的关系,比记住几十条命令更重要。很多看似相似的操作,例如“删除镜像”“删除容器”“删除数据”,实际作用于不同对象,失败原因也不同。


一、CLI 操作的真实对象:客户端、Daemon 与上下文

在 Linux 主机上执行:

docker ps

通常经历如下路径:

docker CLI
   │
   │ Docker Engine API
   ▼
docker daemon
   │
   ├── containerd
   ├── runc
   ├── 镜像存储与快照
   ├── 网络驱动
   └── Volume 驱动

CLI 负责解析命令行参数、选择 Docker context、发起 API 请求并格式化输出。真正创建容器、挂载文件系统、配置网络和维护对象元数据的是 daemon。

1. Docker context 决定操作哪一个 Engine

docker context ls
docker context show

输出可能类似:

NAME        DESCRIPTION                               DOCKER ENDPOINT
default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock

default 通常通过 Unix socket 连接本机 daemon。也可以切换到远程 Engine:

docker context use production
docker ps

此时 docker ps 展示的是远程主机上的容器,而不是当前开发机上的容器。

因此,以下两个命令的语义可能完全不同:

docker image ls
docker --context production image ls

前者查询当前 context 指向的 Engine,后者查询 production context 指向的 Engine。排查“为什么我刚刚创建的容器不见了”时,首先应确认:

docker context show
docker info

docker info 还可以确认 Server 端的存储驱动、容器数量、镜像数量、架构和根目录等信息。

2. CLI 参数与 daemon 配置不是同一层

例如:

docker run --name web nginx:alpine

--name web 是本次创建请求中的容器配置。

而以下配置通常属于 daemon 或 Engine 环境:

  • 镜像存储目录;
  • 默认日志驱动;
  • 默认网络;
  • cgroup 与内存限制能力;
  • rootless 模式;
  • 远程 API 监听地址。

CLI 可以请求资源,是否能成功仍取决于 daemon 的权限、内核能力、存储驱动和网络环境。Linux 容器的隔离最终依赖 Linux namespaces、cgroups、挂载和网络机制;Docker CLI 本身不提供隔离。


二、对象关系:Image、Container、Network 和 Volume 如何组合

一个典型的 Web 容器可以抽象为:

graph LR
    I[Image nginx:alpine] --> C[Container web]
    C --> W[可写容器层]
    C --> E[Network Endpoint]
    E --> N[Network app-net]
    C --> M[Mount]
    M --> V[Volume web-data]
    N --> P[宿主机端口 127.0.0.1:8080]

这张图中有几个容易混淆的关系:

  1. Image 不是 Container。Image 是模板,Container 是实例。
  2. Network 不是 Container 内部的网络命名空间本身,而是管理连接关系的对象;容器加入网络时会创建网络 endpoint。
  3. Volume 不是 Image 的一层,也不是容器的可写层。
  4. 端口发布不是“把容器加入网络”的同义词,而是配置宿主机到容器网络的转发规则。
  5. Container 删除通常不会自动删除 Image;Volume 是否删除则取决于删除命令和 Volume 类型。

可以使用 inspect 查看对象之间的实际引用:

docker inspect web
docker network inspect app-net
docker volume inspect web-data
docker image inspect nginx:alpine

普通 ls 命令适合发现对象,inspect 才适合确认对象的详细配置和引用关系。


三、Image:可复用的内容模板

3.1 Image 的定义

Docker Image 是由元数据和文件系统层组成的内容集合。它通常包含:

  • 基础文件系统;
  • 环境变量默认值;
  • 默认工作目录;
  • 默认用户;
  • ENTRYPOINT
  • CMD
  • 暴露端口声明;
  • Volume 声明;
  • 构建历史和标签信息。

Image 本身不是一个正在运行的进程,也没有“启动”这一状态。

例如:

docker pull nginx:alpine
docker image ls nginx

可能看到:

REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
nginx        alpine    3f8a00f137a0   ...           ...

这里的 nginx:alpine 是一个标签引用,不是镜像内容本身。标签可以被重新指向新的镜像摘要,因此它不是不可变标识。

更稳定的标识是 digest:

docker image inspect nginx:alpine \
  --format '{{json .RepoDigests}}'

可能输出:

["nginx@sha256:..."]

使用 digest 运行:

docker run --rm nginx@sha256:<digest>

表示请求该内容摘要对应的镜像,而不是当前标签可能指向的版本。

标签与摘要的区别

可以把它们抽象为:

标签:nginx:alpine ─────► 当前某个镜像摘要
摘要:sha256:abc... ────► 固定内容

标签是可变的名称映射,digest 是内容寻址结果。执行:

docker pull nginx:alpine

可能使本地标签更新到新的镜像内容,但已有容器不会因此自动替换根文件系统。容器创建时使用的是当时解析到的 Image 内容,除非显式重新创建容器。


3.2 镜像层、可写层和容器文件系统

通过 Dockerfile 构建镜像:

FROM alpine:3.20
RUN apk add --no-cache curl
COPY app.sh /usr/local/bin/app.sh
ENTRYPOINT ["/usr/local/bin/app.sh"]

使用 BuildKit 构建:

DOCKER_BUILDKIT=1 docker build -t demo:1 .

每个能够产生文件系统变化的构建步骤通常会形成可复用的层。BuildKit 还维护构建缓存,并可以并行执行独立步骤,但最终镜像仍表现为由若干只读层组成的文件系统视图。

启动容器时,Docker 通常在 Image 只读层之上增加一个容器专属的可写层:

Image layer 1  只读
Image layer 2  只读
Image layer 3  只读
-------------------
Container writable layer

容器内执行:

echo changed > /tmp/example

写入的是容器可写层,而不是 Image。执行:

docker rm container-name

删除容器时,这个可写层通常也会消失。

因此,以下命令不会把容器运行期间写入的普通文件保存为新镜像:

docker stop web
docker start web

停止和重新启动同一个容器会保留其可写层;删除后再用同一个 Image 创建新容器,则不会保留这些修改。

如果确实需要将当前容器文件系统状态制作成新镜像:

docker commit web web:snapshot

docker commit 更适合调试或临时捕获状态,不适合作为可审计的常规构建流程。Dockerfile 和 BuildKit 构建过程能明确记录输入、步骤和产物,commit 则容易隐藏修改来源。

可写层不是持久化数据库

即使容器停止后可写层仍然存在,也不应把它当作数据库持久化方案:

  • 容器删除后通常丢失;
  • 存储位置受 Engine 存储驱动管理;
  • 备份和迁移不如 Volume 明确;
  • 写时复制可能使频繁修改大文件产生额外开销。

需要跨容器或跨生命周期保存的数据,应使用 Volume 或明确的 bind mount。


3.3 Image 的查询与删除

查看本地镜像:

docker image ls
docker image ls --all

docker image ls 展示的是本地标签视图,多个标签可能指向同一个 Image ID。--all 会显示中间层或没有常规标签的镜像记录,但它仍不是底层所有存储层的逐层磁盘报告。

查看镜像详细信息:

docker image inspect nginx:alpine

常见字段包括:

  • Id:镜像内容标识;
  • RepoTags:本地标签;
  • RepoDigests:仓库摘要;
  • Config:默认配置;
  • RootFS:根文件系统层信息;
  • ArchitectureOs:平台信息。

删除标签或镜像:

docker image rm nginx:alpine

如果镜像仍被容器引用,删除可能失败:

conflict: unable to delete ... image is being used by running container

即使容器已停止,只要容器对象仍引用该 Image,也可能需要先删除容器。docker image rm -f 可以强制删除标签或镜像引用,但它不会把正在运行的容器“改成没有镜像”;运行中的容器仍需要其已有 rootfs 资源。


四、Container:Image 的一次实例化结果

4.1 创建、启动与运行不是同一个动作

以下命令可以拆成两个阶段:

docker create --name web nginx:alpine
docker start web

docker create 创建 Container 对象,但不启动其主进程。docker start 才启动已经存在的容器。

而:

docker run --name web nginx:alpine

大致等价于:

解析 Image
  ↓
创建 Container 配置、可写层、挂载和网络连接
  ↓
启动容器主进程
  ↓
等待或后台返回

Container 的生命周期可以简化为:

stateDiagram-v2
    [*] --> Created: docker create
    Created --> Running: docker start/run
    Running --> Exited: 主进程退出或 stop
    Exited --> Running: docker start
    Running --> Paused: docker pause
    Paused --> Running: docker unpause
    Created --> Removed: docker rm
    Exited --> Removed: docker rm
    Running --> Removed: docker rm -f
    Removed --> [*]

容器状态不是 Image 的状态,也不是宿主机进程状态的简单别名。Docker 记录容器的生命周期、退出码、启动时间、停止时间、健康检查结果等元数据。

查询运行中容器:

docker container ls

查询所有状态的容器:

docker container ls -a

默认 docker ps 只显示运行中的容器,是常见误解的来源。一个“没有显示”的容器可能只是已经退出。


4.2 主进程决定容器何时退出

例如:

docker run --name once alpine:3.20 sh -c 'echo hello'

sh -c 'echo hello' 很快退出,因此容器状态会变成 Exited

查看:

docker ps -a --filter name=once
docker inspect once --format \
  'status={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}}'

可能得到:

status=exited exit=0 error=

退出码为 0 只表示该主进程正常退出,不表示业务一定完成了所有预期工作;非零退出码表示进程以错误状态退出,但具体原因仍需结合日志和容器内进程行为判断。

长时间运行的容器通常需要一个持续运行的前台进程:

docker run -d --name sleeper alpine:3.20 sleep 3600

-d 只让 CLI 在后台运行并返回容器 ID,不会改变容器中进程的前台/后台语义。容器的生命周期仍由其主进程决定。


4.3 CMDENTRYPOINT 与命令覆盖

假设镜像配置为:

ENTRYPOINT ["python"]
CMD ["app.py"]

那么:

docker run app-image

等效于执行:

python app.py

而:

docker run app-image worker.py

通常会把 CMD 替换为 worker.py,形成:

python worker.py

如果需要完全覆盖入口:

docker run --entrypoint sh app-image

这组规则属于 Image 配置与 docker run 参数的合并结果。排查“容器启动后立即退出”时,应检查:

docker image inspect app-image
docker container inspect app
docker logs app

不要只看 Dockerfile,因为运行命令、Compose 配置和 CLI 参数都可能覆盖默认值。


4.4 startrestartrmexec 的边界

docker start

启动已经存在的容器:

docker start web

它不会重新读取最新的 Image 标签,也不会重新创建挂载、端口和网络配置。

docker restart

docker restart web

通常是停止后再次启动同一个容器,仍然保留容器对象和可写层。它不是“用最新镜像滚动更新”。

docker rm

docker rm web

删除已停止容器。删除运行中容器需要:

docker rm -f web

强制删除会终止容器进程,可能中断正在进行的请求或写入。删除容器不会默认删除它使用的命名 Volume。

docker exec

docker exec -it web sh

exec 在已经运行的容器中启动额外进程。它不是重新进入原来的 shell,也不是启动新的容器。这个进程共享目标容器的 namespaces,因此通常可以看到同一个网络、挂载和进程视图。

如果容器不是运行状态,docker exec 会失败。调试时应先确认:

docker ps --filter name=web
docker inspect web --format '{{.State.Status}}'

极简 Image 可能没有 bashpsip 或调试工具,因此:

docker exec -it web bash

可能失败,而:

docker exec -it web sh

可能成功。调试工具不存在不等于容器网络或进程不存在。


五、Network:连接关系、Endpoint 与端口发布

5.1 Network 对象解决什么问题

Docker Network 是一组网络配置和连接关系。容器加入某个 Network 后,Docker 为该容器创建对应的 endpoint,并将其接入网络驱动管理的网络空间。

查看网络:

docker network ls

典型网络包括:

  • bridge:默认桥接网络;
  • host:Linux 上使用宿主机网络命名空间的特殊模式;
  • none:不配置常规网络连接;
  • 用户创建的 bridge、overlay 等网络。

创建用户定义桥接网络:

docker network create app-net

启动两个容器:

docker run -d --name redis --network app-net redis:7-alpine
docker run --rm --network app-net redis:7-alpine \
  redis-cli -h redis ping

第二个容器将 redis 作为目标主机名。用户定义的 bridge 网络通常提供容器名称和网络别名解析;这比依赖默认 bridge 网络中的旧式链接机制更清晰。

redis-cli 成功得到:

PONG

其因果路径是:

redis 容器加入 app-net
  ↓
网络驱动为容器配置网络接口和地址
  ↓
Docker 内置 DNS 将 redis 解析到该网络地址
  ↓
redis-cli 连接 redis:6379

查看具体连接:

docker network inspect app-net

其中的 Containers 字段显示加入网络的容器、IPv4/IPv6 地址和 endpoint 信息。


5.2 网络连接与端口发布不是一回事

启动服务:

docker run -d \
  --name web \
  --network app-net \
  -p 127.0.0.1:8080:80 \
  nginx:alpine

--network app-net 表示容器加入 app-net

-p 127.0.0.1:8080:80 表示配置宿主机地址和端口到容器端口的发布关系:

宿主机 127.0.0.1:8080
        ↓ 转发
容器 web 的 80/tcp

它们解决不同问题:

  • 同一 Docker 网络中的容器访问 web:80,不需要宿主机发布端口;
  • 宿主机访问 127.0.0.1:8080,依赖端口发布;
  • Image 中的 EXPOSE 80 只是元数据声明,不自动让宿主机端口可访问;
  • 如果没有 -p,容器仍可能被同一网络中的其他容器访问。

验证:

curl http://127.0.0.1:8080

如果宿主机端口已被占用,创建或启动容器可能失败:

Bind for 127.0.0.1:8080 failed: port is already allocated

恢复方式不是修改容器运行状态,而是选择空闲宿主机端口、停止占用者,或重新创建容器。端口发布属于容器创建配置,不能用 docker start 重新读取新的 -p 参数。


5.3 容器加入和离开网络

创建容器时加入网络:

docker run -d --name api --network app-net my-api:1

也可以对已有容器进行连接:

docker network connect app-net api
docker network disconnect app-net api

一个容器可以同时连接多个 Network:

docker network connect frontend api
docker network connect backend api

这使容器可能同时拥有多个网络接口和多个地址。网络隔离边界由“容器是否连接到同一个网络”以及网络驱动的配置共同决定,而不是由容器名称决定。

删除网络:

docker network rm app-net

如果仍有容器连接,通常会失败。正确顺序是:

docker network disconnect app-net api
docker network rm app-net

或者先删除相关容器。强制清理网络前应确认没有生产流量,因为网络删除会立即破坏依赖该网络的连接。


5.4 Linux 上的 hostnone

docker run --rm --network host nginx:alpine

Linux 容器使用 host 网络模式时,容器进程使用宿主机网络命名空间,容器端口与宿主机端口不再是两个独立网络栈中的映射关系。此模式削弱网络隔离,应明确评估监听地址、端口冲突和权限影响。

docker run --rm --network none alpine:3.20 ip addr

none 网络模式只保留回环等有限接口,不提供通常的外部网络连接。它适合验证程序是否意外依赖网络,但具体可见接口仍取决于运行时和镜像工具。

这些模式是 Linux 容器边界下的实现与平台行为,不能直接推导为所有 Docker Desktop 平台上的宿主机等价行为。Docker Desktop 中容器通常运行在 Linux VM 内,宿主机访问路径还包含虚拟机网络层。


六、Volume:独立于容器可写层的数据对象

6.1 Volume 的基本模型

命名 Volume 是 Docker 管理的数据对象:

docker volume create web-data
docker volume inspect web-data

其关键属性通常包括:

  • Name:Volume 名称;
  • Driver:Volume 驱动;
  • Mountpoint:daemon 主机上的实际挂载位置;
  • Scope:本地或其他作用域;
  • Labels:标签元数据;
  • Options:驱动选项。

在容器中挂载:

docker run -d \
  --name web \
  -v web-data:/usr/share/nginx/html \
  nginx:alpine

容器内的 /usr/share/nginx/html 不再只由 Image 内容提供,而是由 Volume 挂载覆盖。这里发生的是挂载遮蔽:

Image 中 /usr/share/nginx/html 的内容
             ↓ 被 Volume mount 覆盖
容器进程看到 Volume 中的目录

如果容器删除,命名 Volume 默认仍然存在:

docker rm -f web
docker volume ls

重新创建容器并挂载同一个 web-data,可以继续看到其中的数据。


6.2 Volume、bind mount 与 tmpfs

Docker 常见挂载形式包括:

命名 Volume

docker run --rm \
  --mount type=volume,src=web-data,dst=/data \
  alpine:3.20

数据由 Docker Volume 驱动管理,适合需要明确生命周期和 Docker 对象管理的数据。

Bind mount

docker run --rm \
  --mount type=bind,src="$PWD/config",dst=/etc/myapp,readonly \
  my-app:1

宿主机路径直接映射进容器。它适合开发时共享源代码或注入配置,但依赖宿主机目录结构、权限和 SELinux/AppArmor 等环境。

tmpfs

docker run --rm \
  --tmpfs /run:rw,noexec,nosuid,size=64m \
  alpine:3.20

数据保存在内存或内核管理的临时文件系统中,容器停止后不作为持久化数据保留。具体可用参数受 Linux 和 Engine 支持限制。

长格式 --mount 通常更明确:

--mount type=volume,src=web-data,dst=/data,readonly

短格式 -v 更简洁,但路径、冒号和读写选项混在一起,复杂场景更容易写错。


6.3 Volume 挂载会遮蔽 Image 内容

假设镜像中有:

/usr/share/nginx/html/index.html

把一个空 Volume 挂载到该目录后,容器内是否立即为空,取决于 Docker 对空 Volume 的初始化行为和挂载配置。对于常见 Docker Volume,Docker 通常会把挂载点原有内容复制到新建的空 Volume;使用 volume-nocopy 可以关闭这一行为:

docker run --rm \
  --mount type=volume,src=web-data,dst=/usr/share/nginx/html,volume-nocopy \
  nginx:alpine

但一旦完成挂载,容器进程看到的是 Volume 视图,而不是同时合并 Image 目录和 Volume 目录。Volume 中已有文件不会与 Image 中的文件进行普通目录合并。

这也是“文件明明在镜像里,挂载后却找不到”的常见原因。


6.4 Volume 删除与数据风险

删除容器:

docker rm web

默认不会删除命名 Volume。

删除 Volume:

docker volume rm web-data

如果仍有容器使用它,通常会失败:

volume is in use

必须先停止并删除引用该 Volume 的容器,或移除挂载关系。删除 Volume 通常意味着删除数据,不能把它当成清理元数据的无害操作。

docker rm -v 的行为也需要精确理解:

docker rm -v web

它会尝试删除容器关联的匿名 Volume;对于命名 Volume,通常不会因为这个参数而自动删除。命名 Volume 的删除应显式执行:

docker volume rm web-data

生产环境中删除前应先确认:

docker volume inspect web-data
docker ps -a --filter volume=web-data

备份 Volume 也不应假定可以直接复制 daemon 的内部目录。更可靠的方式是启动临时容器,将 Volume 挂载到 /source,将备份目标挂载到 /backup,使用归档工具读取文件。


七、Filter:对象查询的筛选语言

7.1 Filter 的定义

--filter 用于限制查询或选择命令的对象集合。例如:

docker ps --filter status=running
docker image ls --filter dangling=true
docker volume ls --filter dangling=true

Filter 不是 SQL,也不是一个对所有 Docker 子命令都完全统一的表达式语言。每个命令支持的键、值格式和匹配对象由该命令定义。

必须区分:

docker ps --filter name=web

和:

docker image ls --filter reference=nginx

前者筛选 Container,后者筛选 Image 引用。name=web 不能直接推导为“所有名字包含 web 的 Docker 对象”。

过滤与格式化不是一回事

docker ps --filter status=exited \
  --format '{{.ID}}\t{{.Names}}\t{{.Status}}'

这里:

  • --filter 决定返回哪些对象;
  • --format 决定如何显示返回结果。

如果要在脚本中进一步处理,建议使用稳定字段或 JSON:

docker inspect --format '{{json .}}' web

不要依赖人类可读的 STATUS 文本进行精确解析,因为输出格式可能包含时间、健康状态和版本相关文本。


7.2 Container 常用 Filter

查看所有退出状态的容器:

docker ps -a --filter status=exited

只看非零退出的容器:

docker ps -a \
  --filter status=exited \
  --filter "exited=1"

按名称筛选:

docker ps -a --filter name=web

按标签筛选:

docker ps -a --filter "label=app=demo"

按网络筛选:

docker ps -a --filter network=app-net

按 Volume 筛选:

docker ps -a --filter volume=web-data

按祖先镜像筛选:

docker ps -a --filter ancestor=nginx:alpine

这里的 ancestor 表示容器使用的镜像祖先关系,不能简单理解为容器当前文件内容与标签字符串做文本比较。

按创建时间相对筛选:

docker ps -a --filter before=web
docker ps -a --filter since=web

这些条件通常以某个容器的创建时间为边界,而不是以容器最近启动时间为边界。清理脚本若误把“很久没运行”理解为“很久没创建”,可能删除错误对象。

docker ps 中的常用状态包括:

  • created
  • running
  • paused
  • restarting
  • exited
  • dead

查询结果为空并不代表 daemon 没有对象,可能只是所有对象不满足当前筛选条件。


7.3 Image、Network 和 Volume 的 Filter

Image

docker image ls --filter dangling=true
docker image ls --filter "reference=nginx:*"
docker image ls --filter "label=stage=builder"

dangling=true 通常用于查找没有仓库标签直接引用的镜像记录:

docker image prune

但 dangling 镜像仍可能被构建缓存、调试流程或某些容器引用。生产清理前应结合:

docker ps -a
docker image inspect <image>
docker system df

Network

docker network ls --filter driver=bridge
docker network ls --filter scope=local
docker network ls --filter label=project=demo
docker network ls --filter dangling=true

dangling 网络通常指没有容器连接的网络,但实际筛选定义由 docker network ls 支持的条件决定。

Volume

docker volume ls --filter dangling=true
docker volume ls --filter driver=local
docker volume ls --filter name=web
docker volume ls --filter label=project=demo

Volume 查询结果为空,不代表主机没有业务数据;数据也可能位于 bind mount、远程驱动或容器可写层中。


7.4 Filter 的组合、引用和安全边界

多个过滤条件通常用于缩小结果集,但不能把所有命令都假定为同一种布尔语义。尤其是同一个键重复出现时,是否表示多个候选值、是否允许多个值、是否按 AND 或 OR 解释,应以目标子命令的文档和实际版本为准。

例如,不要未经验证地把下面命令理解为“状态是 exited 且 running”:

docker ps -a \
  --filter status=exited \
  --filter status=running

更可控的方式是使用一次查询和明确的脚本逻辑,或分别查询后合并结果。

Filter 也不会绕过权限和对象引用检查:

docker rm $(docker ps -aq --filter "label=app=demo")

这条命令存在几个风险:

  1. 没有匹配结果时,命令替换可能导致 docker rm 参数不足;
  2. 标签可能被错误地复用;
  3. 容器可能仍在运行;
  4. 删除容器可能导致服务中断;
  5. 容器删除后,命名 Volume 仍可能保留,造成“对象已清理但数据仍在”。

较安全的步骤是先预览:

docker ps -a \
  --filter "label=app=demo" \
  --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}'

确认后再执行删除:

docker ps -aq --filter "label=app=demo" |
  xargs -r docker rm -f

这仍然是破坏性操作,xargs 只是避免空输入,并不能替代人工确认和业务保护。


八、完整算例:创建、查询、调试和清理一组对象

下面建立一个最小但完整的对象关系:

  • Image:nginx:alpine
  • Container:demo-web
  • Network:demo-net
  • Volume:demo-data
  • Label:app=demo
  • 宿主机端口:127.0.0.1:8080

8.1 准备对象

docker network create --label app=demo demo-net
docker volume create --label app=demo demo-data

预期输出分别是网络名和 Volume 名:

demo-net
demo-data

然后创建容器:

docker run -d \
  --name demo-web \
  --label app=demo \
  --network demo-net \
  -p 127.0.0.1:8080:80 \
  --mount type=volume,src=demo-data,dst=/usr/share/nginx/html \
  nginx:alpine

这里的因果顺序是:

  1. 解析本地的 nginx:alpine,若不存在则尝试拉取;
  2. 创建 demo-web Container;
  3. 为 Container 创建可写层;
  4. demo-data 挂载到指定目录;
  5. 将 Container 连接到 demo-net
  6. 配置宿主机端口转发;
  7. 启动 Image 的默认入口进程;
  8. -d 让 CLI 返回后不等待进程退出。

验证容器:

docker ps \
  --filter name=demo-web \
  --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'

预期类似:

CONTAINER ID   NAMES       STATUS         PORTS
...            demo-web   Up ...         127.0.0.1:8080->80/tcp

验证 HTTP:

curl -I http://127.0.0.1:8080

预期看到类似:

HTTP/1.1 200 OK
Server: nginx/...

版本号和完整响应头可能不同,不应把它们当作固定输出。


8.2 查询引用关系

查询容器的核心状态:

docker inspect demo-web --format '
status={{.State.Status}}
image={{.Config.Image}}
network={{json .NetworkSettings.Networks}}
mounts={{json .Mounts}}
'

查询网络中的容器:

docker network inspect demo-net

查询 Volume:

docker volume inspect demo-data

查询带标签的全部对象:

docker ps -a --filter "label=app=demo"
docker network ls --filter "label=app=demo"
docker volume ls --filter "label=app=demo"

注意标签不是跨对象自动继承的。给 Container 加上:

--label app=demo

不会自动给它使用的 Network 或 Volume 加标签。因此上面的网络和 Volume 必须在创建时分别设置标签。


8.3 修改 Volume 中的数据

在临时容器中写入 Volume:

docker run --rm \
  --mount type=volume,src=demo-data,dst=/data \
  alpine:3.20 \
  sh -c 'echo "managed by volume" > /data/index.html'

然后访问 Web 容器:

curl http://127.0.0.1:8080/index.html

预期:

managed by volume

这里的文件不在 demo-web 的可写层,而在 demo-data 中。删除并重新创建 Web 容器,只要继续挂载相同 Volume,文件仍会存在:

docker rm -f demo-web

docker run -d \
  --name demo-web \
  --label app=demo \
  --network demo-net \
  -p 127.0.0.1:8080:80 \
  --mount type=volume,src=demo-data,dst=/usr/share/nginx/html \
  nginx:alpine

curl http://127.0.0.1:8080/index.html

如果执行:

docker volume rm demo-data

则会删除这个数据对象,后续重新创建同名 Volume 时不会自动恢复原文件。


8.4 故障路径:对象存在但服务不可用

情况一:容器存在但已退出

docker ps

没有结果不代表没有容器。执行:

docker ps -a --filter name=demo-web
docker logs demo-web
docker inspect demo-web --format \
  'status={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}}'

如果容器状态是 exited,先看主进程退出原因,而不是重复执行 docker start

情况二:网络不存在

docker run --rm --network missing-net alpine:3.20 true

会因网络对象不存在而失败。需要先创建或使用正确的 context:

docker network ls
docker context show

情况三:端口冲突

docker run -d -p 127.0.0.1:8080:80 nginx:alpine

若 8080 已被占用,创建可能失败。验证宿主机监听者:

ss -ltnp | grep ':8080'

然后选择其他端口,例如:

docker run -d -p 127.0.0.1:8081:80 nginx:alpine

情况四:Volume 删除失败

docker volume rm demo-data

如果 demo-web 仍挂载该 Volume,Engine 会拒绝删除。先检查引用:

docker ps -a --filter volume=demo-data
docker inspect demo-web --format '{{json .Mounts}}'

正确顺序通常是:

docker rm -f demo-web
docker volume rm demo-data
docker network rm demo-net

九、Docker Compose 与底层对象模型

Compose 文件描述的是一组服务及其依赖关系,但启动后仍然会创建 Docker Engine 的基础对象。

示例:

services:
  web:
    image: nginx:alpine
    ports:
      - "127.0.0.1:8080:80"
    networks:
      - app
    volumes:
      - web-data:/usr/share/nginx/html

networks:
  app:

volumes:
  web-data:

启动:

docker compose up -d

Compose 通常会创建:

  • 一个或多个 Container;
  • 一个项目级 Network;
  • 一个命名 Volume;
  • 对应的端口、挂载和标签元数据。

查询时可以同时使用 Compose 和原生 CLI:

docker compose ps
docker ps --filter label=com.docker.compose.project
docker network ls
docker volume ls

Compose 的服务名、项目名和容器实际名称不是同一个概念。应用连接时应优先使用 Compose 网络中的服务名,而不是手工推断生成的容器名。

web 服务名
  ↓ Compose DNS
同一网络中的 web 地址
  ↓
访问容器内部端口

docker compose down 默认会停止并删除由该项目创建的容器和网络,但通常保留命名 Volume。执行:

docker compose down -v

会额外删除该 Compose 项目管理的 Volume,因此可能导致数据库数据丢失。这个命令应被视为破坏性清理,而不是普通的“重启项目”。


十、对象模型中的几个关键边界

10.1 删除容器不等于删除镜像

docker rm demo-web

只删除 Container。Image nginx:alpine 仍然存在,可以继续创建新容器。

反过来,删除 Image 也不会自动删除所有使用它的 Container;如果存在引用,Engine 通常拒绝删除,避免破坏对象关系。

10.2 重启容器不等于更新镜像

docker restart demo-web

不会拉取新镜像,也不会重新创建容器。要使用新 Image,通常需要:

docker pull nginx:alpine
docker rm -f demo-web
docker run ...

或者使用 Compose:

docker compose pull
docker compose up -d

Compose 会根据配置决定是否重新创建受影响的容器,但 Volume 和网络的保留策略仍需单独理解。

10.3 EXPOSE 不等于 -p

Dockerfile:

EXPOSE 8080

只是声明镜像预期使用的容器端口。它不会创建宿主机监听端口。宿主机访问能力需要:

docker run -p 127.0.0.1:8080:8080 image-name

而同一 Docker 网络中的容器访问容器端口,不一定需要端口发布。

10.4 Container 可写层不等于 Volume

容器内执行:

echo x > /tmp/x

默认写入 Container 可写层。

执行:

echo y > /data/y

如果 /data 是 Volume 挂载点,则写入 Volume。

可以通过:

docker inspect container-name --format '{{json .Mounts}}'

确认一个路径是否位于挂载对象中,而不能只凭容器内路径名称猜测。

10.5 Filter 不等于安全策略

Filter 只筛选查询结果或命令目标。它不会提供:

  • 访问控制;
  • 审批;
  • 数据备份;
  • 事务回滚;
  • 运行中连接的保护。

因此:

docker system prune

或带有 -a--volumes 的清理命令必须先理解会影响哪些对象。过滤和清理是两个不同阶段:先枚举、再验证、最后执行破坏性操作。


十一、推荐的诊断顺序

当“容器无法访问”时,可以按对象边界逐层检查。

1. 确认目标 Engine

docker context show
docker info

避免在错误的远程主机上操作。

2. 确认 Container 状态

docker ps -a --filter name=demo-web
docker inspect demo-web --format \
  '{{.State.Status}} {{.State.ExitCode}} {{.State.Error}}'

如果不是 running,先检查:

docker logs demo-web

3. 确认端口发布

docker port demo-web
docker inspect demo-web --format '{{json .HostConfig.PortBindings}}'

确认宿主机地址、端口和容器端口是否符合预期。

4. 确认网络连接

docker network inspect demo-net
docker inspect demo-web --format '{{json .NetworkSettings.Networks}}'

确认容器确实连接到目标 Network,并检查容器 IP、别名和 endpoint。

5. 确认挂载

docker inspect demo-web --format '{{json .Mounts}}'
docker volume inspect demo-data

确认应用实际读取的目录没有被错误的 Volume 或 bind mount 遮蔽。

6. 最后进入容器或执行诊断进程

docker exec demo-web sh

进入后检查应用进程、监听地址和文件是否存在。若镜像没有诊断工具,可以使用同一 Network 的临时工具容器,而不是为了调试永久修改生产镜像。


Docker CLI 的命令表面上以动词组织:

run、start、stop、rm、inspect、exec、network、volume

但其真实语义由对象模型决定:

Image 提供内容与默认配置
Container 保存实例配置、状态和可写层
Network 管理连接关系与地址路径
Volume 管理独立数据生命周期
Filter 限制对象查询与选择范围

掌握这五个对象及其引用关系后,docker run 不再是一个需要死记参数的黑盒命令,而可以被还原为一组明确的状态变化:解析 Image、创建 Container、建立挂载、连接 Network、配置端口、启动主进程,并在后续通过 Filter、Inspect、Logs 和 Exec 观察每一层是否满足预期。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。