Docker Desktop on Windows 挂载问题根因分析

Docker Desktop on Windows 挂载问题根因分析

本文档记录在 Windows + Docker Desktop 环境下使用 bind-mount 时,运行 Symfony/Pimcore 应用遇到的 rmdir(...): Directory not empty 错误的根因分析与解决方案。

问题现象

运行 Symfony 的原子文件操作(如缓存重建、assets:install)时,反复报:

复制代码
Failed to remove directory "/var/www/html/var/cache": rmdir(/var/www/html/var/cache): Directory not empty

或:

复制代码
Failed to remove directory "/var/www/html/public/bundles/openadmin": rmdir(/var/www/html/public/bundles/.!!oU+): Directory not empty

chmod -R 777 无效,rm -rf 也无效,即使目录里看起来已经没有内容。

根因分析

1. 文件系统桥接机制

Windows 主机是 NTFS,Docker 容器是 Linux(ext4)。bind-mount 把 Windows 路径(如 d:\php_code\open-skeleton)挂载到容器内(/var/www/html),中间需要桥接:

复制代码
Windows NTFS  ──9p / virtiofs 协议──  WSL2 VM ext4  ──mount──  容器
  • 9p:9P2000.L 网络文件系统协议
  • virtiofs:基于 virtio 的 FUSE 实现

两者都是 虚拟化/网络协议,不是内核原生文件系统。所有文件操作(unlink、mkdir、rmdir、rename)都要在 Windows 主机和 WSL2 VM 之间通过 RPC 传递。

2. Symfony Filesystem 的原子 rename 模式

Symfony 的 Filesystem::rename() 在目标目录已存在时,走 Linux 经典的原子替换流程:

复制代码
1. 创建临时目录     var/.!!XXXX      (随机后缀防冲突)
2. 写入新内容       var/.!!XXXX/...
3. 备份当前目录     var/cache  →  var/cach~
4. 重命名就位       var/.!!XXXX  →  var/cache     (Linux 原生支持 rename-over)
5. 清理备份         递归 unlink var/cach~ 下所有文件
6. 删除空目录       rmdir(var/cach~)

在 Linux 原生 ext4 上,这个流程是可靠的------步骤 5 的 unlink 立即生效,步骤 6 的 rmdir 看到的就是空目录。

3. 9p 协议下的状态同步延迟

在 9p/virtiofs 上,Symfony 的递归 remove() 实现大致是:

php 复制代码
foreach (scandir($dir) as $file) {
    unlink($file);   // ← 步骤 5:删除子文件
}
rmdir($dir);         // ← 步骤 6:立即删除空目录

问题出在步骤 5 和 6 之间:

  • unlink() 通过 9p 把删除请求发给 Windows 主机
  • Windows 主机删除文件,回复 "OK"
  • 但 9p 协议 不保证 Windows 主机侧的状态变更立即对容器可见
  • 容器调用 rmdir() 时,9p 可能还报告"目录非空"
  • 返回 ENOTEMPTY,PHP 翻译为 rmdir(...): Directory not empty

这不是 race condition (并发竞争),是 协议同步延迟------即使在单线程顺序执行的情况下也会发生,因为 9p 的 readdir 缓存可能比实际状态滞后几毫秒到几百毫秒。

4. 为什么 chmod -R 777 完全无效

bash 复制代码
chmod -R 777 html/   # 改的是 mode bits (rwxrwxrwx)

报错是 ENOTEMPTY,不是 EACCES:

错误码 含义 触发场景 chmod 能否解决
EACCES Permission denied 没有读写权限 ✅ 能
ENOTEMPTY Directory not empty 目录里有内容(POSIX 硬性规定) ❌ 不能

rmdir() 是 POSIX 系统调用,只能删除空目录 ,这是设计如此,与权限位无关。即使目录的 mode 是 777,只要里面还有文件(或 9p 报告有文件),rmdir 就会失败。

5. 为什么 rm -rf 也可能无效

rm -rf 内部同样是 unlink() + rmdir() 的递归组合,只是加了 -f 忽略错误继续执行。但最终在 rmdir 父目录时,还是会踩同样的 9p 同步延迟问题:

复制代码
rm: cannot remove 'var/cache': Directory not empty

6. 命名卷为什么能解决

Docker 命名卷(open-var、open-bundles)的存储路径在 WSL2 VM 内部:

复制代码
\\wsl$\docker-desktop-data\version-pack-data\community.docker\volume\...
        ↓ 挂载到容器
/var/www/html/var  (真实 ext4,不走 9p)

命名卷的数据在 WSL2 的 ext4 文件系统上,容器的 rmdir 直接走 Linux 内核 VFS,没有协议中间层,目录状态实时同步,unlink 后立即 rmdir 就能成功。

解决方案

方案 A:使用 Docker 命名卷(推荐)

给需要原子写入的路径用命名卷,绕开 9p 协议。在 <docker-compose.yaml> 中:

yaml 复制代码
php:
    volumes:
        - .:/var/www/html                              # 项目代码 bind-mount
        - open-var:/var/www/html/var                 # 命名卷,跑在 ext4 上
        - open-bundles:/var/www/html/public/bundles  # 命名卷

supervisord:
    volumes:
        - .:/var/www/html
        - open-var:/var/www/html/var                 # 必须和 php 共享同一卷
        - open-bundles:/var/www/html/public/bundles

nginx:
    volumes:
        - .:/var/www/html:ro
        - open-bundles:/var/www/html/public/bundles:ro   # 只读,与 php 共享视图

volumes:
    open-var:
    open-bundles:

需要命名卷的典型路径:

路径 用途 触发原子操作的场景
var/ Symfony 缓存、日志、installer 临时目录 缓存重建、installer 临时目录替换
public/bundles/ Symfony bundle 静态资源 assets:install 替换 bundle 目录
public/var/ Pimcore 上传资源(可选) 大文件上传、缩略图生成

不需要命名卷的路径:

路径 原因
src/、config/、templates/ 只读,不触发原子 rename
public/(根) 静态文件,通常不原子替换
vendor/ composer 安装时整体创建,不原子替换

方案 B:改名挪走(临时 workaround)

如果没加命名卷,遇到 rmdir 失败时,把残留目录改名挪走:

bash 复制代码
# 把失败的目录改名(避开 rmdir)
docker exec open-skeleton-php-1 mv /var/www/html/var/cache /var/www/html/var/cache.broken

# 重新跑触发原子操作的命令
docker exec open-skeleton-php-1 php /var/www/html/bin/console cache:clear

mv 在同文件系统内是 rename(),即使目标存在也能成功(覆盖)。挪走后,新操作可以重新创建目录。

方案 C:循环重试(不推荐)

bash 复制代码
# 循环 rm 直到成功
until docker exec open-skeleton-php-1 rm -rf /var/www/html/var/cache 2>/dev/null; do
    sleep 0.5
done

靠 9p 最终同步,可能要等几秒到几十秒。不推荐生产用,只适合调试。

本次安装中触发的具体场景

路径 触发操作 Symfony 走的流程 报错
var/cache/dev/ 缓存重建 临时目录 → 备份 → rename → 清理备份 rmdir(var/.!!06i): Directory not empty
public/bundles/openadmin/ assets:install 重新安装 rmdir 旧 bundle 目录 → 复制新内容 rmdir(public/bundles/.!!oU+): Directory not empty

两者都依赖 rmdir 删除"刚被清空"的目录,在 9p 上都失败。改用命名卷后,这些操作在 ext4 上原生执行,问题消失。

一句话总结

Windows bind-mount 的 9p/virtiofs 协议在 unlink 和 rmdir 之间有状态同步延迟,Symfony 的原子 rename 流程踩中这个窗口。这不是权限问题,是文件系统协议问题,只能通过改用 ext4 命名卷绕开。

参考

相关推荐
Surpass-HC2 小时前
WSL2 windows-ubuntu
linux·windows·ubuntu
遇印记2 小时前
网络安全,数据链路层,物理层
运维·服务器·网络·windows·学习
Brilliantee4042 小时前
docker迁移SOP
docker·容器·eureka
kuroomi2 小时前
K8s集群升级与etcd备份恢复实战
容器·kubernetes·etcd
mx_jun3 小时前
Wireshark 蓝牙 HCI 日志分析常用操作速查
windows·microsoft·hci·bt
艾iYYY3 小时前
思考: C++ 万能引用与完美转发
c++·windows·microsoft
大盛供应链·卢生3 小时前
半导体行业发展的主要驱动力,到底是什么?
容器·新浪微博
川石课堂软件测试3 小时前
自动化测试|持续集成Git使用详解
git·python·jmeter·mysql·ci/cd·容器·kubernetes
探索云原生4 小时前
KServe 工作流程:从 InferenceService 到 vLLM 服务
docker·ai·云原生·kubernetes·go