Docker 基础体系 · 第 69/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 在 Linux、macOS 与 Windows 的差异:VM、路径、网络和性能
Docker 的命令行体验在 Linux、macOS 和 Windows 上很相似,但容器实际运行的位置可能完全不同。理解这种差异的关键,不是记住某个平台要写哪条命令,而是先区分三个角色:
- Docker CLI:负责发送 API 请求,例如
docker run、docker build。 - Docker daemon:负责创建容器、管理镜像、挂载文件系统、配置网络和执行构建。
- 容器运行时:通常由 containerd、runc 等组件完成容器进程的创建。
命令最终由 daemon 执行,而不是由当前打开终端的 CLI 进程执行。因此,路径、端口、卷和构建上下文的实际含义,首先取决于 daemon 位于哪里。
在原生 Linux 上,CLI 和 daemon 通常都运行在同一个 Linux 主机;在 macOS 和 Windows 上,Docker Desktop 通常通过一个 Linux 虚拟机运行 Linux 容器。于是,同样的 docker run 命令,在不同平台上可能经过不同的数据路径。
一、先确定 Docker daemon 到底运行在哪里
1. Linux 原生 Engine:容器直接使用 Linux 内核
Linux 容器不是一个完整的虚拟机。容器进程通常由宿主机 Linux 内核直接创建,并通过以下内核机制隔离:
- namespace:隔离进程、网络、挂载点、用户等视图;
- cgroup:限制和统计 CPU、内存、进程数等资源;
- capabilities、seccomp、LSM:限制进程能够执行的特权操作;
- overlayfs 等存储驱动:组织镜像层和容器可写层。
例如:
docker run --rm alpine uname -a
在 Linux 主机上,容器中的 uname 通常看到的是宿主机 Linux 内核版本:
Linux 8f3c... 6.x.x ... x86_64 Linux
这里的 Linux 不是一个被虚拟出来的独立内核。容器拥有独立的进程和文件系统视图,但不拥有独立内核。
这也决定了 Linux 容器的边界:
Linux 容器需要 Linux 内核提供容器所依赖的内核接口;Docker 本身不会把 Linux 内核翻译成 macOS 或 Windows 内核。
因此,在 macOS 和 Windows 上运行 Linux 容器时,Docker Desktop 必须额外启动一个 Linux VM。
2. macOS:Docker Desktop 中的 Linux VM
macOS 的 Darwin/XNU 内核不是 Linux 内核,不能直接运行普通 Linux 容器。Docker Desktop 会创建一个 Linux 虚拟机,并在其中运行 Docker daemon 和 Linux 容器。
典型路径是:
docker CLI
│
│ Docker API
▼
Docker Desktop 管理组件
│
▼
Linux VM 中的 dockerd/containerd
│
▼
Linux 容器
macOS 上 VM 的具体虚拟化实现、文件共享组件和网络实现会随 Docker Desktop 版本、硬件架构以及设置变化。可以确定的是:Linux 容器实际使用的是 VM 中的 Linux 内核,而不是 macOS 内核。
因此下面的命令:
docker run --rm alpine uname -a
在 macOS 上看到的是 Linux 内核信息,而不是 Darwin:
Linux ...
macOS 主机上的终端只是 Docker CLI 的运行位置。容器进程并不直接作为 macOS 进程运行。
3. Windows:Linux 容器与 Windows 容器是两条不同边界
Windows 上需要区分两种容器:
Linux 容器
Docker Desktop 通常通过以下方式之一提供 Linux 内核:
- WSL 2 后端;
- Hyper-V 虚拟机后端;
- 具体版本支持的其他 Linux VM 集成方式。
在 WSL 2 后端中,Linux 环境由 WSL 2 虚拟化提供;在 Hyper-V 后端中,则由独立的 Linux VM 提供。实现细节和可选项与 Docker Desktop 版本有关,但共同点仍然是:
Windows CLI
│
▼
Docker Desktop / WSL2 或 Hyper-V
│
▼
Linux daemon
│
▼
Linux 容器
Windows 容器
Windows 容器使用 Windows 容器镜像和 Windows 内核能力。它不是“Linux 容器换了一种启动参数”,而是另一套容器平台。
例如,Linux 镜像:
docker run --rm alpine sh
只能在 Linux 容器环境中运行。切换到 Windows containers 后,通常需要使用 Windows 基础镜像,并且命令、文件系统语义、进程模型和镜像兼容性都会变化。
Linux 容器镜像和 Windows 容器镜像不能因为 CPU 架构相同就互相运行。容器镜像至少需要匹配以下条件:
- 容器用户态所需的操作系统 ABI;
- 容器运行时可用的内核能力;
- CPU 架构,例如
amd64或arm64。
二、使用 Docker context 判断命令实际发往哪里
Docker CLI 可以连接不同的 daemon。不要仅凭“我在 Linux/macOS/Windows 终端中执行命令”判断容器在哪里运行。
查看当前 context:
docker context ls
可能看到类似结果:
NAME DESCRIPTION DOCKER ENDPOINT
default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock
desktop-linux Docker Desktop unix:///...
查看 daemon 详情:
docker info
重点观察:
Operating SystemKernel VersionOSTypeArchitectureDocker Root Dir- 存储驱动
- cgroup 版本
- 当前 daemon 的资源限制
查看客户端和服务器端版本:
docker version
其中 Client 表示 CLI,Server 表示 daemon。两者可以不在同一个操作系统上,甚至不在同一台机器上。
例如,远程 context 的路径语义是:
CLI 所在机器上的命令
│
▼
远程 daemon 所在机器解析路径并创建挂载
因此:
docker -H ssh://server.example.com run \
--mount type=bind,src="$PWD",dst=/src \
alpine ls /src
这里的 src 必须在远程 daemon 能访问的位置存在。它不是自动把本地电脑的当前目录上传到远程服务器。
三、路径差异:路径由 daemon 解释,而不是由容器解释
1. Bind mount 的形式化模型
Bind mount 是把 daemon 所在主机上的一个已有路径挂载到容器路径。可以把它表示为:
H: 宿主机文件系统
D: Docker daemon 所在环境
C: 容器文件系统命名空间
S: 宿主机源路径
T: 容器目标路径
绑定挂载成立的条件是:
S ∈ Filesystem(D)
而不是:
S ∈ Filesystem(CLI)
挂载完成后,容器中的路径关系近似为:
C/T → D/S
如果容器目标目录原来有内容,这些内容会被挂载遮蔽:
镜像中的 /app
│
├── main.py
└── config.yaml
挂载主机目录到 /app 后:
容器看到的 /app
│
└── 主机目录内容
原有镜像文件没有被删除,但在挂载解除前不可见。
2. Linux 原生路径
Linux 常见路径是:
docker run --rm \
--mount type=bind,src="$PWD",dst=/workspace \
alpine ls -la /workspace
前置条件是当前目录存在,并且 daemon 具有访问权限。Linux shell 中,$PWD 由 shell 展开为绝对路径,然后 Docker CLI 将路径交给 daemon。
更明确的写法是:
docker run --rm \
--mount type=bind,src="$(pwd)",dst=/workspace,readonly \
alpine find /workspace -maxdepth 1 -type f
readonly 只限制容器通过该挂载写入,不能阻止宿主机上的其他进程修改文件。
--mount 与 -v 有一个容易造成故障的差异:
docker run --rm \
--mount type=bind,src=/does/not/exist,dst=/data \
alpine true
通常会因为源路径不存在而报错。
而短语法:
docker run --rm \
-v /does/not/exist:/data \
alpine true
在许多 Docker Engine 场景下会自动创建源路径,并且往往创建成目录。这样拼写错误就可能被静默转换为一个空目录,导致容器启动成功但读不到预期文件。
3. macOS 路径:本地路径需要跨 VM 文件共享
macOS 中:
docker run --rm \
--mount type=bind,src="$PWD",dst=/workspace \
alpine ls -la /workspace
表面上和 Linux 相同,但实际过程包含额外的一层:
macOS ~/project
│
│ Docker Desktop 文件共享机制
▼
Linux VM 中可访问的共享路径
│
▼
容器 /workspace
这会带来三个重要后果。
文件共享权限是独立边界
如果 Docker Desktop 没有获得访问某个目录的权限,容器可能启动失败,或者挂载结果不符合预期。macOS 的隐私控制也可能限制终端或 Docker Desktop 访问“桌面”“文稿”等目录。
文件系统事件不是完全等价的
Linux 工具通常依赖 inotify 监听文件变化。macOS 主机文件变更需要通过 Docker Desktop 的共享机制传入 Linux VM,再转发给容器。某些开发服务器可能出现:
- 文件修改后不能自动重载;
- 事件延迟;
- 事件数量较多时丢失或降级为轮询;
- 大量小文件扫描明显变慢。
这不是容器进程逻辑错误,而是跨文件系统边界转发事件的结果。
UID/GID 语义不同
Linux 容器通常以数字 UID/GID 访问文件。例如容器中的用户 1000:1000,在 Linux bind mount 上会直接与宿主机的数字所有权产生关系。
macOS 主机文件系统并不以 Linux 容器所理解的方式提供完整的 Unix 所有权语义。Docker Desktop 会在共享层处理权限映射,因此容器内的 chown、权限检查和宿主机 Finder 中显示的所有权之间不一定是一一对应关系。
4. Windows 路径:驱动器、反斜杠和 WSL 路径不是一回事
Windows 主机路径常见形式有:
C:\Users\alice\project
PowerShell 中当前目录可以通过:
${PWD}
获取。使用 Docker CLI 时,推荐先验证解析结果:
docker run --rm `
--mount type=bind,src="${PWD}",dst=/workspace `
alpine ls -la /workspace
PowerShell 的反引号是续行符;如果复制到 cmd.exe 或 Bash 中,语法需要改变。
在 Windows 上常见的路径错误包括:
- 把
C:\project直接写进 Bash 风格脚本; - 把
/mnt/c/project当成所有 Docker 后端都能直接理解的路径; - 把 WSL 内部路径、Windows 路径和 Docker Desktop 共享路径混用;
- 在 Compose YAML 中使用未转义的反斜杠;
- 主机路径包含冒号时被错误解析为
源:目标分隔符。
Compose 的长语法可以减少短语法歧义:
services:
app:
image: alpine
command: ["sh", "-c", "ls -la /workspace && sleep 3600"]
volumes:
- type: bind
source: ./project
target: /workspace
read_only: true
相对路径通常按 Compose 项目文件所在目录解析,而不是按容器工作目录解析。执行前可以查看 Compose 展开的结果:
docker compose config
这一步能发现环境变量、相对路径和合并文件展开后的实际配置。
5. 大小写敏感性是重要的可移植性边界
Linux 文件系统通常区分:
src/App.js
src/app.js
它们是两个不同路径。Windows 默认 NTFS 配置通常大小写不敏感,macOS 默认文件系统也常见大小写不敏感配置,但两者不等于“永远不区分大小写”。
因此,以下代码可能在 macOS 或 Windows 上正常,在 Linux CI 或生产环境失败:
import App from "./app.js";
实际文件却是:
App.js
当源码通过 bind mount 进入 Linux 容器时,容器侧的 Linux 工具链可能仍然受主机共享层行为影响;构建、Git 检出和运行时解析也可能产生差异。可移植项目应避免仅靠大小写差异区分文件名,并在 Linux CI 上验证。
四、Bind mount 的深层边界:传播、递归、只读和符号链接
1. 只读不是“不可见”,也不是宿主机整体只读
docker run --rm \
--mount type=bind,src="$PWD",dst=/src,readonly \
alpine sh -c 'echo test >/src/x'
容器中的写入应失败,例如:
sh: can't create /src/x: Read-only file system
但这不表示:
- 宿主机上的其他进程不能写入
$PWD; - 挂载源路径下的所有相关挂载都自动变成只读;
- 容器不能修改其他未设置只读的挂载;
- 文件系统本身一定具备所有预期的只读语义。
2. 挂载传播主要是 Linux 原生能力
挂载传播决定一个挂载命名空间中的子挂载变化,是否传播到另一个命名空间。常见模式包括:
privaterprivatesharedrsharedslaverslave
其中 r 表示递归地作用于子挂载。
例如,一个容器内启动 systemd、挂载 loop device 或创建嵌套挂载时,是否能让挂载变化传播回宿主机,取决于源挂载的传播属性、内核支持和容器权限。
Linux Engine 上可以显式指定:
docker run --rm \
--mount type=bind,src=/var/lib/mydata,dst=/data,bind-propagation=rshared \
alpine mount
这不是普通的“目录共享”开关,而是 Linux mount namespace 的属性。
在 Docker Desktop 上,容器运行在 Linux VM 中,macOS 或 Windows 主机目录先经过文件共享机制进入 VM。宿主机原生挂载命名空间并不直接等同于 VM 内的 Linux mount namespace,因此不能把 Linux 原生的 mount propagation 预期套用到 Docker Desktop 文件共享上。需要传播挂载时,应优先使用 Linux 主机上的 Docker Engine,并在目标内核上验证。
3. 递归只读依赖实现与内核能力
普通:
--mount type=bind,src=/data,dst=/data,readonly
主要限制目标挂载本身。源目录下面如果存在独立子挂载,是否也被递归设置为只读,取决于 Docker Engine、Linux 内核和挂载实现对递归只读的支持。不能把普通 readonly 自动理解成所有嵌套挂载都只读。
4. 符号链接可能越出预期目录
假设宿主机目录中存在:
project/
└── secret-link -> /home/alice/.ssh
将 project bind mount 到容器后,容器访问:
cat /workspace/secret-link/id_rsa
可能解析到挂载源之外的路径。符号链接不是目录边界。是否能成功访问还取决于挂载、权限和路径在 daemon 环境中的实际存在,但安全审查不能只检查目录名,还必须检查符号链接和实际解析路径。
在 Docker Desktop 中,符号链接还会受到 Windows/macOS 主机文件系统、共享层和 VM 内路径映射的共同影响。一个在 Linux 主机上成立的符号链接,不一定在 Desktop VM 中指向同样的位置。
五、数据卷的位置:named volume 不等于当前目录中的文件夹
Named volume 由 Docker 管理:
docker volume create app-data
docker run --rm \
--mount type=volume,src=app-data,dst=/var/lib/app \
alpine sh -c 'echo hello >/var/lib/app/message'
查看它的元数据:
docker volume inspect app-data
Linux 原生 Engine 可能显示类似:
[
{
"Name": "app-data",
"Mountpoint": "/var/lib/docker/volumes/app-data/_data"
}
]
这个路径位于 Linux daemon 所在主机。
在 Docker Desktop 中,named volume 通常位于 Linux VM 的文件系统中。docker volume inspect 显示的路径是 daemon 视角的路径,不一定能直接在 macOS Finder 或 Windows Explorer 中打开。
这解释了 named volume 与 bind mount 的主要差异:
bind mount:
宿主机明确路径 ──挂载──> 容器路径
named volume:
Docker 管理的数据对象 ──挂载──> 容器路径
数据库、包管理器缓存等大量小文件通常更适合放在 named volume 或 VM 内部文件系统中,而不是直接放在 macOS/Windows 主机目录的 bind mount 中。原因不是 volume “天然更快”,而是它避免了频繁穿越主机文件共享边界。
六、网络差异:Linux bridge 与 Desktop 双层网络
1. Linux 原生网络路径
默认 bridge 网络中,容器通常拥有独立的 network namespace 和虚拟网卡。典型组件关系是:
容器 eth0
│
▼
veth pair
│
▼
docker0 bridge
│
▼
Linux 主机网络栈
│
├── NAT/iptables/nftables
└── 物理网卡
容器访问外网时,通常经过 bridge、主机路由和地址转换。容器访问同一 user-defined network 中的其他容器时,则通过 Docker 内置 DNS 解析服务名。
例如:
docker network create app-net
docker run -d --name web --network app-net nginx
docker run --rm --network app-net alpine \
wget -qO- http://web
这里 web 是 Docker 网络中的服务名。容器不应依赖另一个容器的动态 IP;服务名由 Docker 网络的 DNS 机制解析。
2. macOS/Windows Linux 容器的额外一层
Docker Desktop 中,容器网络至少包含 Linux VM 这一层:
容器
│
▼
VM 内 bridge 和 Linux 网络栈
│
▼
Docker Desktop 虚拟网络
│
▼
macOS/Windows 主机网络栈
│
▼
物理网络
因此,Linux 原生下看到的容器网桥、地址和路由,不一定能在主机系统中直接看到。你在 macOS 上运行:
ifconfig
或在 Windows 上运行:
ipconfig
通常不会看到一个等价于 Linux 原生 docker0 的主机网桥。这个网桥位于 Desktop 的 Linux 环境中。
3. 发布端口的真实含义
下面的命令:
docker run -d --name web -p 8080:80 nginx
表示把 daemon 所在环境的 TCP 8080 转发到容器的 TCP 80:
客户端访问 daemon-host:8080
│
▼
Docker 端口转发
│
▼
容器 web:80
在 Linux 原生 Engine 上,daemon-host 通常就是 Linux 主机。
在 macOS/Windows Desktop 上,端口还需要由 Docker Desktop 从主机转发到 Linux VM,再由 VM 转发到容器。因此客户端可以访问:
http://localhost:8080
但这里的 localhost 是客户端所在的 macOS/Windows 主机,而不是容器。
端口发布只对发布端口生效:
docker run -d --name web nginx
如果没有 -p,容器内部虽然监听 80,但主机的 localhost:80 不会因此自动可访问。容器之间应使用 Docker 网络和容器端口,例如:
http://web:80
而不是使用主机映射端口。
4. localhost 的三种含义
在以下位置执行请求时,localhost 的含义不同:
| 执行位置 | localhost 指向 |
|---|---|
| 容器内 | 当前容器自身 |
| Linux 主机上 | Linux 主机自身 |
| macOS/Windows 主机上 | macOS/Windows 主机自身 |
例如,容器中的应用监听:
127.0.0.1:8080
只能接受当前容器内的连接。即使发布了端口,Docker 也无法把外部请求转发给一个只绑定容器 loopback 的服务。要接受容器外部连接,应用通常需要监听:
0.0.0.0:8080
但 0.0.0.0 是监听地址,不是客户端访问地址。客户端仍应通过已发布的主机端口访问。
5. 从容器访问宿主机
host.docker.internal 是 Docker Desktop 常用的特殊主机名:
docker run --rm alpine ping -c 1 host.docker.internal
它用于让 Desktop 中的容器访问 macOS 或 Windows 主机上的服务。这个名称不是跨所有 Docker Engine 都由规范保证的通用 DNS 名称。
Linux 原生 Engine 上,如果希望使用相同名称,可以在支持 host-gateway 的现代 Engine 中显式添加:
docker run --rm \
--add-host host.docker.internal:host-gateway \
alpine getent hosts host.docker.internal
也可以使用 Docker bridge 的网关地址,但网关地址、主机防火墙和 daemon 配置需要具体验证。生产环境不能假设“容器一定能访问宿主机所有端口”;主机监听地址、防火墙、VPN 和安全策略都可能阻断连接。
6. host network 不是跨平台等价能力
Linux 上:
docker run --rm --network host alpine ip addr
会让容器使用主机网络命名空间,而不是使用普通独立 network namespace。此时 -p 通常没有普通 bridge 模式下的意义。
Docker Desktop 中,Linux 容器位于 Linux VM 内。即使某些较新版本提供了经过配置或需要显式启用的 host networking 能力,它也不应被直接理解为“容器与 macOS/Windows 主机共享同一个网络命名空间”。具体行为必须以当前 Docker Desktop 版本和设置为准。
七、性能差异:先区分计算、存储、网络和构建
性能不能简单概括为“Linux 快、Desktop 慢”。更准确的模型是:
总耗时 =
CPU 执行时间
+ 内存与调度开销
+ 文件系统访问时间
+ 文件事件同步时间
+ 网络路径开销
+ 镜像构建与缓存开销
不同工作负载中,各项占比不同。
1. CPU 和内存
Linux 原生容器直接使用宿主机 Linux 内核调度,通常没有额外的完整 VM 边界。Docker Desktop 的 Linux 容器需要经过 VM,因此存在:
- VM 的 CPU 调度层;
- VM 内存上限或动态回收;
- 主机与 VM 之间的资源竞争;
- Docker Desktop 后台组件本身的资源占用。
这不意味着所有 CPU 密集型任务都会明显变慢。对于主要在容器内部执行的 CPU 计算,虚拟化开销可能小于文件系统和构建上下文开销;但当程序频繁访问共享目录、进行大量小文件操作或触发大量文件事件时,差异通常更明显。
需要区分资源限制来源:
docker inspect <container>
docker stats
docker info
docker stats 显示容器视角的运行时统计;Docker Desktop 的 VM 资源设置又决定了容器所在 Linux 环境可用的总体资源。一个容器没有设置 CPU 限额,不代表它可以无限使用 Windows/macOS 主机的全部资源。
2. 文件 I/O:开发工作负载的主要差异来源
假设一次操作访问 个小文件,每个文件需要执行元数据查询、打开、读取和关闭。可以粗略表示为:
其中:
- :查询目录项和属性的时间;
- :打开文件的时间;
- :文件变化事件跨边界传递的时间;
- :真正传输文件内容的时间。
在 Linux 原生 Engine 中,bind mount 通常直接访问 Linux 文件系统。macOS/Windows Desktop 的 bind mount 可能多出:
主机文件系统
→ 文件共享协议或集成层
→ Linux VM 文件系统
→ 容器挂载点
当 很大而单个文件很小时,固定的元数据和事件开销会被重复放大。典型受影响场景包括:
node_modules;- PHP Composer 依赖目录;
- Python 虚拟环境;
- JavaScript 文件监听器;
- 大型单体仓库的全量扫描;
- Git 状态检查和 IDE 索引。
这就是为什么一种常见的折中方式是:
源码:bind mount,便于主机编辑
依赖和构建缓存:named volume 或 VM 内部路径
示例 Compose 配置:
services:
app:
image: node:22
working_dir: /workspace
command: ["sh", "-c", "npm install && npm run dev"]
volumes:
- type: bind
source: .
target: /workspace
- type: volume
source: node_modules
target: /workspace/node_modules
volumes:
node_modules:
这里源码仍然从主机同步到容器,但 /workspace/node_modules 被 named volume 覆盖,因此依赖文件不必频繁穿越主机文件共享层。
这个配置也有一个边界:如果 package.json、锁文件或 Node 版本变化,必须重新执行安装或清理 volume。named volume 不会自动理解项目依赖是否已经过期。
3. 镜像构建:BuildKit 仍然受构建上下文位置影响
现代 Docker 构建通常使用 BuildKit。执行:
docker build -t demo .
. 是构建上下文。Docker CLI 或相关客户端需要把上下文提供给 daemon;BuildKit 再根据 Dockerfile 使用其中的文件。
上下文过大时,构建开始前就可能产生明显延迟。使用 .dockerignore 降低上下文规模:
.git
node_modules
dist
coverage
*.log
构建过程可以理解为:
构建目录
│
├── 读取 .dockerignore
▼
构建上下文
│
▼
BuildKit daemon / builder
│
├── 读取 Dockerfile
├── 查询缓存
├── 执行 RUN
└── 生成镜像层
在 Linux 原生 Engine 上,BuildKit 通常直接操作 Linux 文件系统。在 Docker Desktop 上,BuildKit builder 往往位于 Linux VM 中;如果上下文来自 macOS/Windows bind-mounted 目录,读取大量文件的成本会受到共享机制影响。
可以用以下命令确认构建器和 daemon 状态:
docker buildx ls
docker buildx inspect --bootstrap
输出中的 builder 驱动、平台和节点信息比单看 docker build 命令更能说明实际构建位置。
4. 多架构构建与虚拟化
现代 BuildKit 支持多平台构建,例如:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/demo:latest \
--push .
这条命令中的两个平台表示镜像目标平台,不表示当前主机一定原生执行了两种架构的构建步骤。
如果目标架构与 builder CPU 架构不同,构建中的 RUN 可能依赖 QEMU 用户态模拟。模拟会影响 CPU 密集型构建步骤,尤其是编译大型项目。需要区分:
linux/amd64、linux/arm64:镜像目标平台;- builder 节点架构:实际执行 BuildKit 步骤的环境;
- QEMU 模拟:在不同架构上执行二进制的兼容机制。
Windows/macOS 上还要叠加 Linux VM;因此多架构构建失败时,应先查看:
docker buildx inspect --bootstrap
docker version
docker info
不要仅根据宿主机系统名称判断构建能力。
八、端到端示例:用 Compose 同时验证路径、网络和卷
下面的配置可在 Linux、macOS 和 Windows 的 Linux containers 模式下使用:
services:
api:
image: python:3.12-alpine
working_dir: /app
command:
- sh
- -c
- |
python -m http.server 8000 --bind 0.0.0.0
ports:
- "127.0.0.1:8000:8000"
volumes:
- type: bind
source: ./public
target: /app
read_only: true
- type: volume
source: api-cache
target: /cache
volumes:
api-cache:
准备目录和文件:
mkdir -p public
printf 'hello from container\n' > public/index.html
docker compose up -d
Windows PowerShell 中可以使用:
New-Item -ItemType Directory -Force public
"hello from container" | Set-Content public/index.html
docker compose up -d
验证:
curl http://127.0.0.1:8000
预期输出:
hello from container
每一步成立的原因如下:
source: ./public按 Compose 项目路径解析为主机目录;- 该目录以只读方式挂载到容器
/app; - Python 监听
0.0.0.0:8000,因此能接受容器外请求; 127.0.0.1:8000:8000只把主机 loopback 上的端口发布出来;- named volume
api-cache由 Docker 管理,不依赖主机路径; - macOS/Windows 上,主机目录还要通过 Docker Desktop 文件共享进入 Linux VM。
查看最终 Compose 配置:
docker compose config
查看挂载:
docker inspect "$(docker compose ps -q api)" \
--format '{{json .Mounts}}'
查看端口:
docker compose port api 8000
可能输出:
127.0.0.1:8000
清理容器和网络:
docker compose down
如果同时删除 named volume:
docker compose down -v
-v 会删除 Compose 管理的 named volume。若该卷中存放数据库数据,删除前必须确认已有备份;bind mount 中的主机文件不会因为 down -v 被删除。
九、常见失败表现与诊断路径
1. 容器中目录为空
先检查挂载是否真的存在:
docker inspect <container> \
--format '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}'
如果 Source 不是预期目录,重点检查:
- 当前 Docker context;
- Compose 相对路径;
- Windows shell 的变量展开;
-v是否因源路径拼写错误自动创建了空目录;- Docker Desktop 是否允许访问该主机目录。
2. 文件修改后容器不重载
分层诊断:
docker exec <container> sh -c 'ls -l /workspace'
确认容器确实看到新文件,然后检查应用使用的文件监听机制。若目录来自 macOS/Windows bind mount:
- 尝试应用的 polling 模式;
- 将依赖和缓存移到 named volume;
- 减少被监听的文件数量;
- 在 Linux CI 或 Linux 主机上复现。
不能只通过重启容器判断问题是否解决,因为重启可能暂时重建了应用内部缓存,但没有改变文件事件路径。
3. 容器无法访问宿主机服务
先验证名称解析:
docker exec <container> getent hosts host.docker.internal
再验证端口:
docker exec <container> \
wget -S -O- http://host.docker.internal:9000
如果解析成功但连接失败,检查:
- 宿主机服务是否只监听
127.0.0.1; - 宿主机防火墙;
- Docker Desktop 或 Linux daemon 的网络模式;
- VPN 是否改变了路由;
- Linux 原生环境是否配置了
host-gateway。
4. 主机访问不到已发布端口
检查端口发布:
docker ps
docker port <container>
然后检查容器内监听地址:
docker exec <container> sh -c 'cat /proc/net/tcp'
更直接的方式是进入应用容器或使用诊断镜像确认服务是否监听 0.0.0.0,而不是只监听 127.0.0.1。
还要区分:
-p 127.0.0.1:8080:80
和:
-p 0.0.0.0:8080:80
前者只允许本机访问,后者可能允许其他网络主机访问,实际还受主机防火墙控制。
5. 权限错误
Linux 原生 bind mount 上,容器进程的 UID/GID 直接影响主机文件权限:
docker run --rm \
--user "$(id -u):$(id -g)" \
--mount type=bind,src="$PWD",dst=/workspace \
alpine sh -c 'touch /workspace/from-container'
这种写法可以减少容器以 root 创建主机文件的问题,但要求镜像内程序能以该 UID 正常运行。
macOS/Windows Desktop 上,权限由主机文件系统、Docker Desktop 共享层和 Linux VM 内映射共同决定。此时简单地在容器中执行:
chown -R app:app /workspace
不一定能得到与 Linux 原生主机相同的结果,而且可能造成大量文件操作和额外同步成本。
十、平台取舍与运行边界
Linux 原生 Engine 适合直接使用 Linux 内核能力
当任务依赖以下能力时,Linux 原生 Engine 通常边界最清晰:
- mount propagation;
- 精确的 Linux UID/GID 和文件权限;
- 宿主机网络命名空间;
- 内核模块、设备和 cgroup 行为;
- 大量文件 I/O;
- 直接访问宿主机 Linux 文件系统;
- 需要观察和控制 Linux daemon 的底层网络、存储和进程状态。
但 Linux Engine 的 daemon 通常拥有较高权限,尤其是以 root 运行时。bind mount /、/var/run/docker.sock 等路径可能扩大容器对宿主机的控制范围。容器隔离不是自动形成的强多租户安全边界。
macOS/Windows Desktop 适合本地开发,但边界包含 VM
Docker Desktop 的 VM 带来明确的隔离层,也带来额外的:
- 文件共享边界;
- VM 资源配置;
- 主机到 VM 的网络转发;
- Desktop 特权组件和企业策略;
- named volume 的 VM 内存储位置。
向容器开放主机目录,本质上是把主机文件访问能力交给容器。共享范围、访问权限、企业设备管理和 Docker socket 的使用都应纳入安全边界审查。
Windows 容器不能作为 Linux 容器的透明替代品
如果生产环境使用 Linux 容器,Windows 主机上的开发验证应明确处于 Linux containers 模式。切换到 Windows containers 后,以下内容都可能改变:
- 基础镜像;
- shell 和路径;
- 文件权限;
- 进程和服务管理;
- 网络与存储实现;
- Dockerfile 中的命令。
“Docker 命令相同”只说明 CLI API 相似,不说明容器运行时、内核和文件系统语义相同。
十一、一个可靠的判断顺序
遇到平台差异问题时,可以按以下因果顺序排查:
1. 当前 CLI 连接哪个 daemon?
│
▼
2. daemon 是 Linux 主机、Linux VM 还是 Windows daemon?
│
▼
3. 源路径由谁解析?是否存在文件共享层?
│
▼
4. 容器使用哪种网络模式?端口绑定在哪台主机?
│
▼
5. 数据位于 bind mount、named volume 还是镜像层?
│
▼
6. 性能瓶颈来自 CPU、文件 I/O、事件同步还是构建上下文?
对应的最小诊断命令是:
docker context show
docker info
docker version
docker inspect <container>
docker volume inspect <volume>
docker network inspect <network>
docker buildx ls
其中最容易被忽略的是第一步:如果 daemon 实际位于远程主机或 Docker Desktop 的 Linux VM 中,那么后续关于路径、网络和性能的判断都必须从 daemon 的视角出发。
Docker 在三种桌面操作系统上的命令可以保持一致,但容器并不因此拥有相同的内核、路径、网络和 I/O 环境。Linux 原生 Engine 的容器直接受 Linux 内核控制;macOS 和 Windows 的 Linux 容器则经过 Linux VM;Windows containers 又是另一套 Windows 内核边界。只要把 daemon 位置、文件共享路径和网络转发链路明确画出来,绝大多数“同一条 Docker 命令在不同系统上表现不同”的问题,都可以还原为具体的组件和状态差异。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker OOM 与性能诊断:cgroups、PSI、CPU Throttle、I/O 和内存
- 下一篇:Docker 安全事件响应:隔离容器、保全证据、镜像追溯和恢复
- 延伸:Docker Desktop 架构:Linux VM、文件共享、网络、资源和企业边界
- 延伸:Docker Bind Mount 深入:传播、递归、只读、符号链接和平台差异
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论