1. 为什么容器里存个文件都这么费劲——从“删了容器就丢数据”说起你有没有遇到过这种场景辛辛苦苦跑起一个数据库容器往里面灌了上万条测试数据结果一执行docker rm -f my-db再docker run起来发现表全空了或者写了个日志收集服务容器重启后昨天的 log 文件凭空消失又或者在本地调试一个 Web 应用改完配置文件一重启容器配置又变回默认值这些不是 bug而是 Docker 最基础、也最容易被新手踩坑的核心机制在起作用。Docker 容器本质上是进程隔离 文件系统分层的组合体。它把应用运行时需要的所有东西打包成镜像启动时基于镜像创建一个可写的“顶层”所有运行时产生的文件日志、上传的图片、数据库的 .db 文件、用户生成的配置都写在这个顶层里。这个设计非常精妙——它让容器启动快、销毁快、状态干净。但代价也很直接容器生命周期一结束这个可写层就跟着一起被销毁里面的数据连同它的 inode 和元数据彻底从文件系统里抹掉。这就像你把重要文档只存在电脑的临时内存里关机就没了。所以“持久化存储”根本不是 Docker 的一个“高级功能”而是你只要想让容器干点正经事存用户数据、记日志、跑数据库就必须直面的底层生存问题。它解决的不是“怎么存得更酷”而是“怎么别让数据凭空蒸发”。我带过的几十个刚接触容器的开发同学头三天问得最多的问题就是“我的文件怎么不见了”——这恰恰说明理解持久化是跨过 Docker 入门门槛的第一道硬坎。这篇文章不讲虚的不堆概念。我会用一个真实可跑的 Python Flask 小应用作为贯穿始终的“实验台”手把手带你把三种核心存储方案——Bind Mount、Volume、tmpfs——全部在本地实操一遍。你会亲眼看到当容器被rm掉再重建哪些文件还在哪些已经消失两个容器同时往同一个目录写文件会不会打架为什么docker volume ls看到的路径和你ls看到的完全不一样以及为什么有些场景下你宁可让数据“消失”也不让它落地硬盘。所有结论都来自终端里敲出来的命令和cat出来的文件内容。这不是理论课这是一份你明天就能照着做的运维笔记。2. 存储方案全景图不是选“哪个好”而是选“哪个对”在动手之前必须先理清一个关键认知Docker 提供的三种存储方式Bind Mount、Volume、tmpfs没有绝对的优劣之分只有场景的严丝合缝。很多教程一上来就说“Volume 是官方推荐应该优先用”这话没错但错在没告诉你“为什么”和“在什么前提下”。就像你不会因为“螺丝刀是标准工具”就拒绝用扳手拧螺母一样。我们得先画一张清晰的决策地图这张地图的坐标轴是三个最朴素的问题数据要活多久永久保存一次会话还是随容器生死数据要给谁看仅本容器多个容器共享宿主机上的其他程序也要读写数据放哪儿必须放在宿主机某个固定路径还是交给 Docker 自己管或者干脆别落地纯内存2.1 Bind Mount把宿主机的“抽屉”直接塞进容器里Bind Mount 是 Docker 最古老、最直白的方案。它的逻辑极其简单粗暴我宿主机上/home/user/myapp/data这个目录现在就是你的/app/data目录你往里写我就往里存你删我就删。它不创造新东西只是做了一次路径映射。它的优势是“所见即所得”。你在宿主机上ls /home/user/myapp/data看到的就是容器里ls /app/data的全部内容。开发调试时你想实时改配置、看日志、拖文件进去测试Bind Mount 是最顺手的。比如你用 VS Code 连着容器开发直接在宿主机编辑config.yaml容器里立刻生效不用docker cp或重启。但它的硬伤也源于这份“直白”。它把容器和宿主机的文件系统深度耦合了。这意味着如果你把/etc/passwd绑定进去容器里rm -rf /app/data可能就等于在宿主机上删掉了/home/user/myapp/data如果你换了一台新服务器路径/home/user/myapp/data不存在整个容器就起不来多个容器绑定同一个宿主目录时如果它们并发写同一个文件比如都往app.log里追加没有锁机制日志大概率会乱码或覆盖。所以Bind Mount 的黄金法则只有一条它只适合那些你完全掌控、路径稳定、且不介意容器与宿主机“共呼吸”的场景。比如本地开发环境、CI/CD 流水线中临时挂载源代码、或者将宿主机的证书目录/etc/ssl/certs挂进容器供 HTTPS 使用。2.2 VolumeDocker 自己建的“保险柜”专为生产而生如果说 Bind Mount 是“借宿主机的抽屉”那 Volume 就是 Docker 在宿主机上专门为你建的一个独立保险柜。你告诉 Docker“我要一个叫my_db_data的保险柜”Docker 就在/var/lib/docker/volumes/下给你划出一块地建好柜子配好钥匙权限然后把柜门挂载点装在容器的/var/lib/mysql上。至于这个柜子具体在宿主机硬盘的哪个扇区你不需要知道也不该关心。Volume 的设计哲学是“解耦”和“抽象”。它天生具备几个关键特性与容器生命周期分离docker rm容器Volume 里的数据纹丝不动。docker volume rm才是真正删除数据的命令。跨容器共享安全多个容器可以同时挂载同一个 VolumeDocker 内部做了文件系统级别的协调避免了 Bind Mount 那种裸奔式的并发风险。可移植性强docker volume create命令是跨平台的无论 Linux、Mac 还是 WindowsWSL2Volume 的管理接口一致。备份时你只需docker run --rm -v my_db_data:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /data .一条命令搞定。它的代价是“黑盒感”。你docker volume inspect my_db_data看到的Mountpoint路径通常是/var/lib/docker/volumes/my_db_data/_data对普通用户来说就是个神秘地址。你不能像 Bind Mount 那样随手cd进去编辑。但这恰恰是它的安全设计——它强制你通过容器或 Docker CLI 来操作数据而不是绕过管理层直接碰磁盘。因此Volume 是生产环境、数据库、任何需要可靠、可迁移、多副本共享数据的场景的唯一选择。PostgreSQL 官方镜像的文档里第一行就写着“Please use a volume for the database data.” 这不是建议是铁律。2.3 tmpfs数据的“一次性纸巾”用完即焚tmpfs 是三者中最特殊的一个。它压根不跟硬盘打交道所有数据都只存在于宿主机的 RAM内存里。你可以把它想象成一个超高速、但断电就忘的 U 盘插在容器的/run/secrets或/tmp上。它的核心价值就藏在名字里——“temporary”。数据有且仅有两个归宿要么在容器运行时被读写要么在容器停止/退出的瞬间被操作系统从内存中彻底清空。它甚至不会在宿主机的文件系统里留下任何痕迹df -h看不到占用ls找不到目录。这带来了两个不可替代的优势极致的安全性敏感信息如 API Token、数据库密码、TLS 私钥一旦放进 tmpfs就永远不可能被意外写入硬盘、被备份脚本扫走、或在服务器故障后被恢复出来。容器一停密钥自动销毁。极致的性能内存读写速度比 SSD 快 100 倍以上。对于需要高频读写的临时缓存、会话数据session store、或中间计算结果tmpfs 是最优解。当然它的限制也如影随形数据无法持久化无法跨容器共享且受宿主机内存总量限制。你不可能把一个 50GB 的数据库 dump 放进 tmpfs。它只服务于那些“活着才有意义死了就该消失”的数据。总结一下当你面对一个存储需求时按顺序问自己这三个问题数据是否绝对不能丢→ 是排除 tmpfs否考虑 tmpfs。数据是否需要被宿主机上的非 Docker 进程如 cron 脚本、另一个服务直接访问→ 是选 Bind Mount否优先 Volume。数据是否需要在多个容器间安全、可靠地共享→ 是必须 Volume否Bind Mount 或 tmpfs 视情况而定。这张地图是我过去五年在上百个微服务项目里用血泪教训和无数个docker volume prune画出来的。它不复杂但每一次选错都意味着几小时的排查和回滚。3. 实战拆解用一个 Flask 应用亲手验证每一种方案光说不练假把式。接下来我们用一个极简但功能完整的 Python Flask 应用作为我们的“小白鼠”逐个击破三种存储方案。这个应用只有一个接口/create_file接收 JSON 格式的{file_name: test.txt, content: Hello World}然后在指定目录下创建文件。它的代码简洁到可以直接贴在终端里运行但足以暴露所有存储方案的本质差异。3.1 应用准备三分钟搭好实验台首先创建项目目录和核心文件。打开你的终端Linux/macOS或 PowerShellWindows执行以下命令mkdir docker-persistence-demo cd docker-persistence-demo创建app.py这是应用的全部逻辑from flask import Flask, request import os app Flask(__name__) # 确保目标目录存在这是关键 PERSISTENT_DIR /app/docker_bind if not os.path.exists(PERSISTENT_DIR): os.makedirs(PERSISTENT_DIR) app.route(/create_file, methods[POST]) def create_file(): try: data request.get_json() file_name data.get(file_name, default.txt) content data.get(content, No content provided) # 构建完整文件路径 file_path os.path.join(PERSISTENT_DIR, file_name) # 写入文件使用 w 模式确保每次都是新内容 with open(file_path, w) as f: f.write(content) return {Status: Success, File: file_name, Path: file_path}, 200 except Exception as e: return {Error: str(e)}, 500 if __name__ __main__: app.run(debugFalse, host0.0.0.0, port5000)接着创建Dockerfile定义如何构建镜像FROM python:3.9-slim # 设置工作目录 WORKDIR /app # 复制应用代码 COPY app.py . # 安装 Flask RUN pip install --no-cache-dir flask # 暴露端口 EXPOSE 5000 # 启动命令 CMD [python, app.py]最后创建docker-compose.yml虽然我们主要用docker run但 compose 对后续多容器演示很有用version: 3.8 services: app: build: . ports: - 5000:5000 # 我们会在这里动态添加不同的挂载方式现在构建镜像docker build -t create-file-app .提示如果你在 Windows 上使用 PowerShell路径分隔符是\但在 Docker 命令中务必统一使用/。Docker 的--mount参数内部处理的是 Linux 路径混用\会导致挂载失败错误信息往往很晦涩比如invalid argument。这是我在 Windows 上踩过最多的坑之一没有之一。3.2 方案一Bind Mount —— “我的目录就是你的目录”现在我们来创建第一个容器使用 Bind Mount。假设你想把宿主机的~/docker-dataLinux/macOS或C:\docker-dataWindows作为数据存放点。Linux/macOS 用户# 创建宿主机目录 mkdir -p ~/docker-data # 运行容器将宿主机 ~/docker-data 挂载到容器的 /app/docker_bind docker run -d \ --name bind-app-1 \ -p 5001:5000 \ --mount typebind,source$HOME/docker-data,target/app/docker_bind \ create-file-appWindows 用户PowerShell# 创建宿主机目录 New-Item -ItemType Directory -Path C:\docker-data -Force # 运行容器注意 source 路径使用正斜杠并且是绝对路径 docker run -d --name bind-app-1 -p 5001:5000 --mount typebind,sourceC:/docker-data,target/app/docker_bind create-file-app容器启动后用curl或 Postman 发送请求curl -X POST http://localhost:5001/create_file \ -H Content-Type: application/json \ -d {file_name: bind_test_1.txt, content: This is from Bind Mount!}你应该收到{Status: Success, ...}的响应。现在验证数据是否真的落到了宿主机上# Linux/macOS ls -l ~/docker-data/ cat ~/docker-data/bind_test_1.txt # Windows (PowerShell) Get-ChildItem C:\docker-data\ Get-Content C:\docker-data\bind_test_1.txt你将看到文件清晰地躺在宿主机目录里。这就是 Bind Mount 的魔力——它让你对数据的掌控感和操作本地文件一模一样。注意os.makedirs(PERSISTENT_DIR)这行代码至关重要。如果挂载的目标目录在容器内不存在Docker 不会自动创建它应用启动就会因FileNotFoundError崩溃。而 Bind Mount 的source目录如果不存在Docker 会静默地为你创建一个空目录这有时会掩盖路径错误。所以在应用代码里主动创建挂载点是健壮性的第一道防线。3.3 方案二Volume —— “Docker 的保险柜我只管用”现在我们切换到 Volume 方案。这次我们不指定宿主机路径让 Docker 全权负责。第一步创建一个名为my_app_data的 Volumedocker volume create my_app_data第二步运行容器挂载这个 Volumedocker run -d \ --name volume-app-1 \ -p 5002:5000 \ --mount typevolume,sourcemy_app_data,target/app/docker_bind \ create-file-app第三步发送请求创建文件curl -X POST http://localhost:5002/create_file \ -H Content-Type: application/json \ -d {file_name: volume_test_1.txt, content: This is from Volume!}现在问题来了文件存在哪儿了我们不知道也不该直接去碰。但我们可以用 Docker 的官方命令来“窥探”# 查看所有 Volume docker volume ls # 查看这个 Volume 的详细信息重点关注 Mountpoint docker volume inspect my_app_data输出会类似这样路径因系统而异[ { CreatedAt: 2024-05-20T10:30:4508:00, Driver: local, Labels: {}, Mountpoint: /var/lib/docker/volumes/my_app_data/_data, Name: my_app_data, Options: {}, Scope: local } ]Mountpoint就是 Docker 为你建的“保险柜”的物理位置。你可以ls一下ls -l /var/lib/docker/volumes/my_app_data/_data/你会看到volume_test_1.txt。但请记住这不是一个你应该日常操作的路径。它的存在只是为了证明 Volume 确实在工作。日常管理你应该只用docker volume命令。实操心得docker volume inspect是你的“透视眼”。当你怀疑 Volume 没挂上或者数据没写进去时第一反应不是exec进容器瞎找而是inspect一下确认Mountpoint是否正确然后ls那个路径。这比在容器里find / -name *.txt快一百倍。3.4 方案三tmpfs —— “内存里的烟火绚烂但短暂”最后我们来体验一把“用完即焚”的 tmpfs。它不需要提前创建直接在docker run时声明即可。docker run -d \ --name tmpfs-app-1 \ -p 5003:5000 \ --mount typetmpfs,destination/app/docker_bind,tmpfs-size100m \ create-file-app这里tmpfs-size100m参数很重要。它限定了这个内存文件系统最多能用 100MB 内存。如果不设Docker 默认可能用到tmpfs的最大限制通常是宿主机内存的一半这可能导致 OOM内存溢出杀掉你的容器。为 tmpfs 设定合理的大小上限是生产环境的必备操作。发送请求curl -X POST http://localhost:5003/create_file \ -H Content-Type: application/json \ -d {file_name: tmpfs_test.txt, content: This lives in RAM only!}现在验证它的“短暂性”# 查看容器内文件应该存在 docker exec tmpfs-app-1 ls -l /app/docker_bind/ # 停止并删除容器 docker stop tmpfs-app-1 docker rm tmpfs-app-1 # 重新运行一个全新的容器用同样的 tmpfs 挂载 docker run -d \ --name tmpfs-app-2 \ -p 5004:5000 \ --mount typetmpfs,destination/app/docker_bind,tmpfs-size100m \ create-file-app # 查看新容器内的目录应该是空的 docker exec tmpfs-app-2 ls -l /app/docker_bind/你会发现tmpfs_test.txt彻底消失了。它从未踏上过硬盘只在上一个容器的内存里存在过。这就是 tmpfs 的全部意义——它不是为了存储而是为了隔离和瞬时性。4. 多容器协同共享、竞争与数据一致性实战单个容器的存储只是入门。真正的挑战在于多个容器如何安全、高效地共享同一份数据。这在微服务架构中是常态一个订单服务写数据一个报表服务读数据一个 Web 前端容器和一个静态资源缓存容器共享同一份 HTML/CSS 文件。我们用刚才的 Flask 应用来模拟这个经典场景。4.1 Bind Mount 多容器共享即裸奔小心并发冲突我们启动三个容器全部绑定到宿主机同一个目录~/docker-dataLinux/macOS或C:\docker-dataWindows。# 启动三个容器分别映射到不同端口 docker run -d --name bind-app-1 -p 5001:5000 --mount typebind,source$HOME/docker-data,target/app/docker_bind create-file-app docker run -d --name bind-app-2 -p 5002:5000 --mount typebind,source$HOME/docker-data,target/app/docker_bind create-file-app docker run -d --name bind-app-3 -p 5003:5000 --mount typebind,source$HOME/docker-data,target/app/docker_bind create-file-app现在让它们并发地向同一个目录写文件# 在三个终端里几乎同时执行 curl -X POST http://localhost:5001/create_file -H Content-Type: application/json -d {file_name: shared_bind_1.txt, content: From Container 1} curl -X POST http://localhost:5002/create_file -H Content-Type: application/json -d {file_name: shared_bind_2.txt, content: From Container 2} curl -X POST http://localhost:5003/create_file -H Content-Type: application/json -d {file_name: shared_bind_3.txt, content: From Container 3}稍等几秒检查宿主机目录ls -l ~/docker-data/你会看到三个文件都成功创建了。看起来很完美别急我们来制造一个经典的并发冲突。修改app.py让它不再创建新文件而是追加内容到一个共享的shared.log文件# 替换 app.py 中的 create_file 函数 app.route(/append_log, methods[POST]) def append_log(): try: data request.get_json() content data.get(content, No content) # 追加到共享日志 log_path os.path.join(PERSISTENT_DIR, shared.log) with open(log_path, a) as f: # 注意这里是 a不是 w f.write(f[{datetime.now().isoformat()}] {content}\n) return {Status: Appended, Log: log_path}, 200 except Exception as e: return {Error: str(e)}, 500重新构建镜像docker build -t create-file-app .然后启动三个新容器。现在让它们疯狂地并发追加# 在循环里快速发送请求 for i in {1..10}; do curl -X POST http://localhost:5001/append_log -H Content-Type: application/json -d {\content\: \Log from C1 - $i\}; done for i in {1..10}; do curl -X POST http://localhost:5002/append_log -H Content-Type: application/json -d {\content\: \Log from C2 - $i\}; done for i in {1..10}; do curl -X POST http://localhost:5003/append_log -H Content-Type: application/json -d {\content\: \Log from C3 - $i\}; done wait最后查看shared.logcat ~/docker-data/shared.log | head -20你极大概率会看到混乱的输出比如一行里夹杂着 C1、C2、C3 的内容或者某些行被截断。这是因为open(..., a)在 Linux 下并不是原子操作。多个进程容器同时打开同一个文件进行追加内核的文件指针file offset更新和写入操作之间存在竞态条件race condition。Bind Mount 把这个底层的并发风险原封不动地暴露给了上层应用。解决方案在 Bind Mount 场景下必须由应用自己实现并发控制。常见做法是使用文件锁flock、数据库事务、或者引入一个中心化的消息队列如 Redis Pub/Sub来协调写入。这大大增加了应用的复杂度。这也是为什么Bind Mount 不适合做生产级的多容器共享存储。4.2 Volume 多容器Docker 的“交通警察”天然支持共享现在我们用 Volume 来重做这个实验。Volume 的底层是 Docker 管理的文件系统通常是overlay2它对多进程并发访问做了更好的优化和协调。# 确保 Volume 存在 docker volume create shared_volume # 启动三个容器全部挂载同一个 Volume docker run -d --name volume-app-1 -p 5001:5000 --mount typevolume,sourceshared_volume,target/app/docker_bind create-file-app docker run -d --name volume-app-2 -p 5002:5000 --mount typevolume,sourceshared_volume,target/app/docker_bind create-file-app docker run -d --name volume-app-3 -p 5003:5000 --mount typevolume,sourceshared_volume,target/app/docker_bind create-file-app用同样的并发脚本向/append_log接口发送请求。# 清空之前的日志 docker exec volume-app-1 sh -c echo /app/docker_bind/shared.log # 并发追加 for i in {1..10}; do curl -X POST http://localhost:5001/append_log -H Content-Type: application/json -d {\content\: \Log from V1 - $i\}; done for i in {1..10}; do curl -X POST http://localhost:5002/append_log -H Content-Type: application/json -d {\content\: \Log from V2 - $i\}; done for i in {1..10}; do curl -X POST http://localhost:5003/append_log -H Content-Type: application/json -d {\content\: \Log from V3 - $i\}; done wait检查日志# 进入任意一个容器查看 docker exec volume-app-1 cat /app/docker_bind/shared.log | head -20你会发现输出虽然顺序是乱的这是并发的正常表现但每一行都是完整的、没有被截断的。这是因为 Volume 的文件系统层提供了比裸文件更可靠的并发语义。它不是魔法而是 Docker 工程师们在overlay2驱动上做了大量工作来保证多容器挂载时的基本一致性。注意Volume 保证的是一致性不是强事务性。它不能替代数据库的 ACID 特性。但对于日志、配置文件、静态资源等场景Volume 提供的共享能力已经足够健壮和易用。4.3 数据迁移与备份Volume 的终极价值Volume 的最大优势体现在它的可管理性和可移植性上。这才是它成为生产环境首选的真正原因。场景你需要把一个正在运行的 MySQL 容器的数据迁移到一台新服务器上。停止旧容器确保数据写入完成docker stop mysql-old备份 Volume使用一个临时容器# 创建一个临时容器挂载 Volume 和当前目录 docker run --rm \ -v shared_volume:/volume \ -v $(pwd):/backup \ alpine \ tar czf /backup/mysql-backup-$(date %Y%m%d).tar.gz -C /volume .这条命令的意思是启动一个 Alpine Linux 容器把shared_volume挂载到/volume把当前目录$(pwd)挂载到/backup然后在容器里执行tar命令把/volume下的所有内容打包成mysql-backup-20240520.tar.gz并保存到宿主机的当前目录。将备份文件mysql-backup-20240520.tar.gz复制到新服务器用scp、rsync或任何你喜欢的方式。在新服务器上创建 Volume 并恢复数据# 创建新的 Volume docker volume create mysql-new-data # 启动一个临时容器挂载新 Volume 和备份文件 docker run --rm \ -v mysql-new-data:/volume \ -v $(pwd):/backup \ alpine \ sh -c tar xzf /backup/mysql-backup-20240520.tar.gz -C /volume启动新的 MySQL 容器挂载这个恢复好的 Volumedocker run -d \ --name mysql-new \ -v mysql-new-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORDmy-secret-pw \ -p 3306:3306 \ mysql:8.0整个过程你没有碰过一行 MySQL 的 SQL没有手动导出导入没有担心字符集或版本兼容性。你操作的只是一个.tar.gz文件和几个docker volume命令。这就是 Volume 抽象带来的力量——它把复杂的数据迁移降维成了简单的文件拷贝。提示alpine镜像是一个极小的 Linux 发行版 5MB里面包含了tar、gzip、sh等基本工具是做这种“数据搬运工”任务的完美选择。它启动快、资源占用低用完即走是 DevOps 工具箱里的常备利器。5. 常见问题与避坑指南那些文档里不会写的细节在真实的项目中存储问题往往不是“能不能用”而是“为什么用着用着就出问题了”。以下是我在一线踩过、修过、被深夜报警电话叫醒过的典型问题以及最直接的解决方案。5.1 问题容器启动失败报错invalid argument或permission denied现象docker run命令执行后容器立即退出docker logs container显示Permission denied或invalid argument。排查思路首要怀疑SELinux仅 Linux。这是企业级 LinuxRHEL, CentOS, Fedora上最常见的元凶。SELinux 的安全策略默认禁止容器进程写入宿主机的任意目录。即使你chmod 777了它依然会拦住。验证在宿主机上执行sestatus。如果输出enabled则 SELinux 正在运行。临时解决仅用于测试在docker run命令末尾加上--security-opt labeldisable。但这只是关闭了防护不推荐生产环境使用。永久解决推荐给你的挂载目录打上正确的 SELinux 标签。例如如果你的 Bind Mount 目录是/home/user/myapp/data执行sudo semanage fcontext -a -t container_file_t /home/user/myapp/data(/.*)? sudo restorecon -Rv /home/user/myapp/data这条命令告诉 SELinux“这个目录及其所有子目录都是容器可以安全访问的”。次要怀疑Windows 路径格式。在 Windows 的 PowerShell 或 CMD 中--mount sourceC:\data是错误的。Docker 的 daemon 运行在 WSL2 的 Linux 内核里它不认识 Windows 的\。必须写成sourceC:/data。这是 Windows 用户的头号陷阱。5.2 问题Volume 里的数据“看不见”docker volume inspect显示的路径ls不到现象docker volume inspect my_vol显示Mountpoint: /var/lib/docker/volumes/my_vol/_data但ls /var/lib/docker/volumes/my_vol/_data提示No such file or directory。真相你很可能在 macOS 或 Windows 上。Docker Desktop 在这些系统上是通过一个轻量级的 Linux VMHyperKit 或 WSL2来运行 Docker daemon 的。/var/lib/docker/...这个路径是那个虚拟机内部的路径不是你宿主机的路径。解决方案macOS打开 Docker Desktop 的设置Settings - Resources - File Sharing确保你的项目目录如/Users/yourname/project已经被添加到共享列表中。然后你只能通过docker run -v /Users/yourname/project:/mnt alpine ls /mnt这样的方式从容器里访问宿主机文件。Windows同样在 Docker Desktop 设置Settings - Resources - File Sharing中确保C:\或你的项目盘符已被共享。然后在容器里/c/Users/yourname/project就是你宿主机的C:\Users\yourname\project。关键认知在 macOS/Windows 上docker volume的Mountpoint是一个“虚拟机内部路径”你无法也不应该在宿主机上直接访问它。它的存在只是为了 Docker daemon 内部管理。你要操作数据要么通过docker exec进容器要么用docker run -v挂载宿主机目录。5.3 问题tmpfs 容器内存爆满被系统 OOM Killer 杀掉现象容器运行一段时间后突然退出docker ps -a看到状态是Exited (137)。137是 Unix 信号SIGKILL的编号通常意味着被 OOM Killer 杀死。原因你创建 tmpfs 时没有指定tmpfs-size。Docker 默认会让 tmpfs 占用尽可能多的内存直到触发系统的 OOM Killer。解决方案永远为 tmpfs 指定大小。例如# 错误没有大小限制 --mount typetmpfs,destination/run/secrets # 正确明确限制为 16MB --mount typetmp