容器的文件系统是临时的——容器一删除,里面的数据就全没了。这在开发环境下可能无所谓,但数据库文件、用户上传、配置文件这些数据必须持久化。Docker 提供了三种挂载方式来解决这个问题,但该选哪种?本指南以”场景决策”为核心,帮你快速选对方案。
本篇是 Docker 系列(共 7 篇)的第 6 篇。上一篇:Docker 网络完全指南。下一篇:Docker Compose 完全指南。
Docker 容器的文件系统基于镜像层(只读)+ 容器层(可写)构成。容器运行时的所有写操作都发生在最上面的容器层:
┌─────────────────────────┐
│ 容器层(可写,临时) │ ← 容器运行时的写入
├─────────────────────────┤
│ 镜像层 3(只读) │
├─────────────────────────┤
│ 镜像层 2(只读) │
├─────────────────────────┤
│ 镜像层 1(只读) │
└─────────────────────────┘
容器层使用 Copy-on-Write(CoW)机制:修改镜像中的文件时,先复制到容器层再修改。这意味着频繁写入会带来性能开销。
用一个实验说明问题:
# 1. 启动容器并写入数据
docker run -d --name db -e POSTGRES_PASSWORD=secret postgres:16
docker exec db sh -c 'echo "important data" > /tmp/mydata.txt'
docker exec db cat /tmp/mydata.txt
# 输出:important data
# 2. 停止并删除容器
docker stop db && docker rm db
# 3. 用同一镜像重新创建容器
docker run -d --name db -e POSTGRES_PASSWORD=secret postgres:16
docker exec db cat /tmp/mydata.txt
# 输出:cat: /tmp/mydata.txt: No such file or directory
数据丢失了。因为新容器有全新的容器层,之前的写入随旧容器一起被删除。
数据丢失的常见场景:
| 操作 | 数据是否保留 |
|---|---|
docker stop |
✅ 保留 |
docker restart |
✅ 保留 |
docker rm |
❌ 丢失 |
docker rm -f |
❌ 丢失 |
重新 docker run |
❌ 全新容器 |
docker system prune |
❌ 清理停止的容器 |
Docker 提供三种挂载方式,将数据存储在容器之外:
┌─────────────────────────────────────────────────────┐
│ 容器 │
│ │
│ /app/data /app/src /app/tmp │
│ │ │ │ │
└──────┼────────────┼────────────┼────────────────────┘
│ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│ Volume │ │ Bind │ │ tmpfs │
│ │ │ Mount │ │ │
└───┬───┘ └───┬───┘ └───────┘
│ │ 内存中
▼ ▼
Docker 管理 宿主机任意
/var/lib/ 目录
docker/volumes
| 方式 | 存储位置 | 管理方 | 典型场景 |
|---|---|---|---|
| Volume | Docker 管理区域 | Docker | 数据库、持久化数据 |
| Bind Mount | 宿主机任意路径 | 用户 | 开发热重载、配置文件 |
| tmpfs | 宿主机内存 | 系统 | 临时数据、敏感信息 |
Docker 提供两种语法来挂载存储,功能基本相同,但语法风格差异明显:
-v / --volume 语法——紧凑,用冒号分隔:
# 格式:-v 源:目标[:选项]
docker run -v mydata:/app/data nginx # Volume
docker run -v ./src:/app/src nginx # Bind Mount
docker run -v mydata:/app/data:ro nginx # 只读
--mount 语法——显式,用键值对:
# 格式:--mount type=类型,src=源,dst=目标[,选项]
docker run --mount type=volume,src=mydata,dst=/app/data nginx
docker run --mount type=bind,src=./src,dst=/app/src nginx
docker run --mount type=volume,src=mydata,dst=/app/data,readonly nginx
核心差异:
| 对比项 | -v |
--mount |
|---|---|---|
| 语法风格 | 紧凑,冒号分隔 | 显式,键值对 |
| 可读性 | 简短但需记住参数顺序 | 冗长但含义清晰 |
| 不存在的源 | 自动创建目录 | 报错(更安全) |
| 高级选项 | 部分选项不支持 | 支持所有选项 |
| Compose 文件 | 短语法用 -v 风格 |
长语法用 --mount 风格 |
简单场景用 -v,写起来快:
# 开发环境挂载源码
docker run -v ./src:/app/src my-app
# 数据库持久化
docker run -v pgdata:/var/lib/postgresql/data postgres:16
复杂场景或生产环境用 --mount,不容易出错:
# 只读挂载配置文件
docker run --mount type=bind,src=./nginx.conf,dst=/etc/nginx/conf.d/default.conf,readonly nginx
# 使用 Volume 子路径
docker run --mount type=volume,src=mydata,dst=/app/logs,volume-subpath=app-logs nginx
提示:
-v在源路径不存在时会自动创建空目录,这可能导致应用启动失败却不报错。--mount会直接报错,帮你提前发现问题。在生产环境中推荐使用--mount。
Volume 由 Docker 引擎创建和管理,数据存放在 Docker 管理的宿主机目录中(Linux 上是 /var/lib/docker/volumes/)。
# 创建并使用 Volume
docker volume create pgdata
docker run -d --name db \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16
特点:
Bind Mount 将宿主机上的指定目录或文件直接挂载到容器中。容器和宿主机看到的是同一份文件。
# 挂载当前目录的 src 到容器
docker run -d --name web \
-v ./src:/app/src \
-p 3000:3000 \
node:22-slim
特点:
tmpfs 将数据存储在宿主机内存中,不会像 Volume 或 Bind Mount 那样持久化到磁盘。容器停止后数据消失。
注意:如果宿主机启用了 swap,tmpfs 中的数据仍可能被系统换出到磁盘。对安全敏感场景(如密钥),应确认宿主机的 swap 配置。
# 使用 tmpfs 存储临时数据
docker run -d --name app \
--mount type=tmpfs,dst=/app/tmp,tmpfs-size=100m \
my-app
特点:
| 特性 | Volume | Bind Mount | tmpfs |
|---|---|---|---|
| 存储位置 | Docker 管理区域 | 宿主机任意路径 | 宿主机内存 |
| 持久化 | ✅ 容器删除后保留 | ✅ 宿主机上保留 | ❌ 停止即消失 |
| 可移植性 | ✅ 不依赖宿主机 | ❌ 依赖目录结构 | ❌ 仅 Linux |
| 性能 | 接近原生 | 原生 | 最快(内存) |
| 多容器共享 | ✅ 支持 | ✅ 支持 | ❌ 不支持 |
| 适用环境 | 开发 + 生产 | 主要开发环境 | 特殊场景 |
| Docker 管理 | ✅ 完整生命周期 | ❌ 用户自行管理 | ❌ 自动清理 |
| 典型场景 | 数据库、持久数据 | 源码挂载、配置 | 临时缓存 |
# 创建 Volume
docker volume create pgdata
# 列出所有 Volume
docker volume ls
# 输出:
# DRIVER VOLUME NAME
# local pgdata
# local redis-data
# local a1b2c3d4... (匿名 Volume)
# 查看 Volume 详情
docker volume inspect pgdata
# 输出:
# [
# {
# "CreatedAt": "2024-01-15T10:30:00Z",
# "Driver": "local",
# "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
# "Name": "pgdata",
# "Options": {},
# "Scope": "local"
# }
# ]
# 删除 Volume
docker volume rm pgdata
# 清理所有未使用的 Volume
docker volume prune
命名 Volume——指定名称,明确管理:
# 创建命名 Volume
docker run -d --name db \
-v pgdata:/var/lib/postgresql/data \
postgres:16
# pgdata 会持久存在,可复用
docker volume ls | grep pgdata
# local pgdata
匿名 Volume——不指定名称,Docker 自动生成哈希名:
# 不指定 Volume 名称,Docker 自动创建匿名 Volume
docker run -d --name db \
-v /var/lib/postgresql/data \
postgres:16
# 匿名 Volume 名称是随机哈希
docker volume ls
# DRIVER VOLUME NAME
# local a1b2c3d4e5f6...
对比:
| 特性 | 命名 Volume | 匿名 Volume |
|---|---|---|
| 名称 | 自定义名称 | 随机哈希 |
| 可复用 | ✅ 通过名称引用 | ❌ 难以找到和引用 |
| 管理 | 容易识别和管理 | 容易积累成垃圾 |
| 适用场景 | 数据库、需要持久化的数据 | 临时覆盖(如 node_modules) |
提示:需要持久化的数据始终使用命名 Volume。匿名 Volume 主要用于特殊技巧(如后面会讲到的 node_modules 处理)。
# 方法 1:用临时容器查看 Volume 内容
docker run --rm -v pgdata:/data alpine ls -la /data
# 方法 2:在 Linux 上直接查看 Volume 目录(需要 root 权限)
sudo ls /var/lib/docker/volumes/pgdata/_data
# 方法 3:查看容器的挂载信息
docker inspect db --format='' | python3 -m json.tool
注意:macOS 和 Windows 上 Docker Desktop 运行在虚拟机中,无法直接访问
/var/lib/docker/volumes/路径。使用临时容器方式查看。
# 删除指定 Volume(Volume 正在被使用时会报错)
docker volume rm pgdata
# 查看哪些 Volume 未被使用
docker volume ls -f dangling=true
# 清理所有未使用的 Volume(谨慎操作!)
docker volume prune
# WARNING! This will remove all local volumes not used by at least one container.
# 清理所有未使用资源(包括 Volume)
docker system prune --volumes
注意:
docker volume prune会删除所有未被容器引用的 Volume,包括你可能还需要的数据。生产环境中务必确认后再执行。
性能提示:macOS 和 Windows 上,Docker Desktop 通过虚拟机文件共享(VirtioFS / gRPC FUSE)同步 Bind Mount 文件,性能低于 Linux 原生挂载。对于
node_modules等包含大量小文件的目录,建议使用 Volume 代替 Bind Mount(参见 §8.2 node_modules 处理)。
# 挂载目录
docker run -d --name web \
-v ./my-app/src:/app/src \
-p 3000:3000 \
node:22-slim
# 挂载单个文件(常用于配置文件)
docker run -d --name web \
-v ./nginx.conf:/etc/nginx/conf.d/default.conf:ro \
-p 3000:80 \
nginx
# 只读挂载(容器无法修改)
docker run -d --name web \
-v ./config:/app/config:ro \
my-app
注意:使用
-v时,如果宿主机路径不存在,Docker 会自动创建一个空目录。这常常不是你想要的行为。使用--mount则会直接报错。
Bind Mount 的权限问题是最常见的坑,而且 macOS 和 Linux 表现不同。
macOS(Docker Desktop):
macOS 上 Docker Desktop 通过虚拟机中的文件共享机制(VirtioFS / gRPC FUSE)同步文件,权限问题较少。容器内进程通常可以正常读写挂载的文件。
# macOS 上一般不会遇到权限问题
docker run --rm -v ./my-app:/app node:22-slim sh -c "touch /app/test.txt && echo OK"
# OK
Linux:
Linux 上 Bind Mount 是真正的目录挂载,容器内的用户 UID/GID 必须与宿主机文件的权限匹配。
# Linux 上可能遇到权限拒绝
docker run --rm -v ./my-app:/app node:22-slim sh -c "touch /app/test.txt"
# touch: cannot touch '/app/test.txt': Permission denied
# 原因:容器内以 node 用户(UID 1000)运行,但宿主机文件属于其他用户
Linux 权限问题的解决方法:
# ✅ 方法 1:指定容器用户与宿主机用户一致(推荐,无需改镜像)
docker run --rm -u $(id -u):$(id -g) -v ./my-app:/app node:22-slim \
sh -c "touch /app/test.txt"
# ✅ 方法 2:在 Dockerfile 中创建匹配 UID 的用户(推荐,可固化到镜像)
# Dockerfile 中:
# RUN groupadd -g 1000 appuser && useradd -u 1000 -g appuser appuser
# USER appuser
⚠️
chmod -R 777不建议作为长期方案:网上常见chmod -R 777 ./my-app的写法,虽然能快速解决权限报错,但它让所有用户都能读写执行该目录,存在安全隐患。仅在临时排障时使用,排查完毕后应恢复为上述方法 1 或方法 2。
| 平台 | 权限表现 | 常见问题 |
|---|---|---|
| macOS | 透明处理,很少有权限问题 | 文件同步性能稍慢 |
| Linux | 严格匹配 UID/GID | 容器用户与宿主机用户不一致 |
Bind Mount 最大的价值是开发时的热重载——修改宿主机代码,容器内的应用自动重新加载:
# 前端开发:Vite 热重载
docker run -d --name web \
-v ./my-app:/app \
-p 5173:5173 \
node:22-slim \
sh -c "cd /app && npm install && npm run dev -- --host 0.0.0.0"
# 后端开发:nodemon 热重载
docker run -d --name api \
-v ./my-api:/app \
-p 3000:3000 \
node:22-slim \
sh -c "cd /app && npm install && npx nodemon server.js"
修改宿主机上 my-app/src/App.tsx,Vite 会自动检测变更并热更新浏览器页面。
| 需求 | 选择 | 原因 |
|---|---|---|
| 源码热重载 | Bind Mount | 宿主机修改立即同步到容器 |
| 数据库数据保留 | Volume | 容器重建后数据不丢失 |
| 配置文件注入 | Bind Mount | 方便在宿主机编辑 |
| node_modules 隔离 | 匿名 Volume | 避免宿主机与容器依赖冲突 |
| 临时文件/缓存 | tmpfs | 快速读写,不占磁盘 |
| 需求 | 选择 | 原因 |
|---|---|---|
| 数据库持久化 | 命名 Volume | Docker 管理,易备份迁移 |
| 日志持久化 | Volume | 不依赖宿主机目录结构 |
| 配置文件注入(只读) | Bind Mount | 加 :ro 确保容器不修改 |
| 密钥/证书(临时) | tmpfs | 不落盘,安全 |
| 静态资源 | Volume | 多容器共享,可移植 |
flowchart TD
A[需要持久化数据吗?] -->|否| B[tmpfs]
A -->|是| C[需要宿主机直接编辑吗?]
C -->|是| D[Bind Mount]
C -->|否| E[Volume]
D --> F{什么场景?}
F -->|源码开发| G["Bind Mount<br/>-v ./src:/app/src"]
F -->|配置文件| H["Bind Mount + :ro<br/>-v ./config:/app/config:ro"]
E --> I{什么场景?}
I -->|数据库| J["命名 Volume<br/>-v pgdata:/var/lib/postgresql/data"]
I -->|日志/缓存| K["命名 Volume<br/>-v app-logs:/app/logs"]
I -->|覆盖容器目录| L["匿名 Volume<br/>-v /app/node_modules"]
速记口诀:
Volume 不能直接在宿主机上访问(尤其是 macOS/Windows),但可以用临时容器来备份:
# 备份 pgdata Volume 到当前目录的 tar 文件
docker run --rm \
-v pgdata:/source:ro \
-v ./backup:/backup \
alpine tar czf /backup/pgdata-backup.tar.gz -C /source .
工作原理:
┌──── 临时容器 ────┐
│ │
│ /source ← pgdata Volume(只读)
│ /backup ← 宿主机 ./backup 目录
│ │
│ tar czf /backup/pgdata-backup.tar.gz -C /source .
│ │
└──────────────────┘
注意:上述 tar 备份是文件级别的直接复制,适用于静态文件和已停止的服务。对于正在运行的数据库,直接复制数据目录可能得到不一致的快照。应优先使用数据库自带的备份工具(如
pg_dump、mysqldump),确保数据一致性:# PostgreSQL 推荐备份方式 docker exec db pg_dump -U postgres mydb > backup.sql
# 创建新 Volume 并恢复数据
docker volume create pgdata-restored
docker run --rm \
-v pgdata-restored:/target \
-v ./backup:/backup:ro \
alpine tar xzf /backup/pgdata-backup.tar.gz -C /target
将数据从一个 Volume 复制到另一个:
# Volume 间直接复制
docker run --rm \
-v old-volume:/source:ro \
-v new-volume:/target \
alpine sh -c "cp -a /source/. /target/"
将 Bind Mount 数据迁移到 Volume:
# 从宿主机目录迁移到 Volume
docker run --rm \
-v ./local-data:/source:ro \
-v app-data:/target \
alpine sh -c "cp -a /source/. /target/"
使用 Bind Mount 挂载源码进行热重载时,需要注意 HMR(Hot Module Replacement)的配置:
# 启动前端开发容器
docker run -d --name web \
-v ./my-app:/app \
-p 5173:5173 \
node:22-slim \
sh -c "cd /app && npm run dev -- --host 0.0.0.0"
Vite 配置注意事项:
// vite.config.js
export default defineConfig({
server: {
host: "0.0.0.0", // 必须,否则容器外无法访问
watch: {
usePolling: true, // macOS/Windows Docker Desktop 需要轮询监听
},
hmr: {
port: 5173, // 确保 HMR WebSocket 端口一致
},
},
});
提示:macOS 和 Windows 上 Docker Desktop 的文件系统事件通知可能不可靠。如果修改文件后 HMR 不触发,添加
usePolling: true可以解决,但会增加 CPU 开销。
经典问题:把整个项目目录挂载到容器,宿主机的 node_modules 会覆盖容器内通过 npm install 安装的依赖。两者的平台可能不同(macOS vs Linux),导致原生模块崩溃。
# ❌ 错误做法:宿主机的 node_modules 覆盖了容器的
docker run -d \
-v ./my-app:/app \
node:22-slim sh -c "cd /app && npm install && npm start"
# 如果宿主机 macOS 的 node_modules 存在,会覆盖容器中 Linux 的依赖
解决方案:匿名 Volume 技巧
# ✅ 正确做法:用匿名 Volume 隔离 node_modules
docker run -d --name web \
-v ./my-app:/app \
-v /app/node_modules \
-p 3000:3000 \
node:22-slim sh -c "cd /app && npm install && npm start"
原理:
宿主机 ./my-app/ 容器 /app/
├── src/ ←→ ├── src/ (Bind Mount,双向同步)
├── package.json ←→ ├── package.json (Bind Mount,双向同步)
├── node_modules/ ✗ ├── node_modules/ (匿名 Volume,隔离)
└── ... └── ...
-v /app/node_modules 创建一个匿名 Volume 挂载到容器的 /app/node_modules,优先级高于 Bind Mount。这样容器内的 npm install 安装到匿名 Volume 中,不受宿主机 node_modules 影响。
注意事项:
docker rm 删除容器后,匿名 Volume 中的 node_modules 也会丢失(除非用 docker run --rm 以外的方式管理)npm installnode_modules,可以用命名 Volume 替代匿名 Volume:# 命名 Volume 替代匿名 Volume,容器重建后 node_modules 仍在
docker run -d --name web \
-v ./my-app:/app \
-v web-node-modules:/app/node_modules \
-p 3000:3000 \
node:22-slim sh -c "cd /app && npm install && npm start"
前端项目构建后需要将产物持久化或与其他容器共享:
# 方法 1:构建产物输出到宿主机(Bind Mount)
docker run --rm \
-v ./my-app:/app \
node:22-slim sh -c "cd /app && npm run build"
# 构建产物在宿主机 ./my-app/dist/ 中
# 方法 2:构建产物存入 Volume,供 Nginx 容器使用
docker volume create app-dist
# 构建并输出到 Volume
docker run --rm \
-v ./my-app:/app:ro \
-v app-dist:/output \
node:22-slim sh -c "cd /app && npm run build && cp -r dist/. /output/"
# Nginx 从 Volume 读取静态文件
docker run -d --name web \
-v app-dist:/usr/share/nginx/html:ro \
-p 3000:80 \
nginx
--mount 更安全:源路径不存在时报错而非静默创建空目录docker run --rm 挂载 Volume 和宿主机目录来备份| 命令/操作 | 说明 |
|---|---|
docker volume create <名称> |
创建命名 Volume |
docker volume ls |
列出所有 Volume |
docker volume inspect <名称> |
查看 Volume 详情 |
docker volume rm <名称> |
删除 Volume |
docker volume prune |
清理未使用的 Volume |
-v mydata:/app/data |
命名 Volume 挂载 |
-v /app/node_modules |
匿名 Volume(隔离用) |
-v ./src:/app/src |
Bind Mount 挂载 |
-v ./config:/app/config:ro |
只读 Bind Mount |
--mount type=volume,src=X,dst=Y |
显式 Volume 挂载 |
--mount type=bind,src=X,dst=Y,readonly |
显式只读 Bind Mount |
--mount type=tmpfs,dst=/tmp,tmpfs-size=100m |
tmpfs 挂载 |
docker system prune --volumes |
清理所有未用资源含 Volume |
到这里,镜像、容器、网络、存储都已经学完。但每次手敲一长串 docker run 命令既繁琐又不可重复。下一篇用 Docker Compose 把所有配置收拢到一个 YAML 文件中——一条命令启动整个项目。见Docker Compose 完全指南。