Docker Compose 卷:绑定挂载和命名卷怎么选
了解 Docker Compose 中 bind mount 与命名卷的区别:配置文件和应用数据分别怎么选,为什么会出现权限错误,以及如何检查、备份和迁移卷。
Bind mount 还是命名卷:简短结论
Docker Compose 卷分为两种,选择哪一种取决于文件由谁负责管理。对于您需要自行读写的文件,应使用 bind mount,例如配置文件、模板和静态网站文件。对于由应用负责管理的数据,应使用命名卷,例如数据库文件、搜索索引和上传的媒体文件。bind mount 指向主机上的路径,您可以直接在编辑器中打开该路径。命名卷是由 Docker 创建并跟踪的存储,您需要通过 Docker 访问它。
两者都位于服务中的同一个 volumes: 键下,因此容易混淆。区别在于冒号左侧的内容。左侧以 . 或 / 开头时,表示主机路径,因此这是 bind mount。其他内容表示名称,因此这是命名卷;该名称还必须在顶层 volumes: 块中声明。
Compose 文件中的两种语法
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: nginx:1.27
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site:/usr/share/nginx/html:ro
volumes:
pgdata:pgdata:/var/lib/postgresql/data 是命名卷。./nginx.conf:/etc/nginx/nginx.conf 是绑定挂载,:ro 会以只读方式挂载它。这是配置文件的正确默认设置,因为容器不应改写该文件。如果忘记顶层的 volumes: 条目,Compose 会因 service "db" refers to undefined volume pgdata 而停止。
启动服务,并列出 Docker 创建的对象:
docker compose up -d
docker volume ls该卷不叫 pgdata,而是叫 <project>_pgdata。项目名称默认取保存 Compose 文件的目录名。目录名为 myapp 时,卷名为 myapp_pgdata。这一点很重要,因为重命名目录会得到一个新的空卷,应用看起来就像丢失了数据。实际上,旧卷仍会由 docker volume ls 列出。若目录可能移动,请在 Compose 文件中使用 name: 固定卷名,或设置 COMPOSE_PROJECT_NAME。这类设置应与其他Compose 环境文件和密钥放在一起。
权限错误为什么只会影响绑定挂载
这是两者最重要的实际区别,原因只有一条:首次使用时,空的命名卷会从镜像复制初始内容,而绑定挂载不会。
当 Docker 将空的命名卷挂载到镜像中已有内容的目录上时,会把这些内容复制到卷中,并保留镜像设置的所有权和权限模式。官方 Postgres 镜像提供的 /var/lib/postgresql/data 由其自身的 postgres 用户拥有,因此卷会由相同的数字 ID 拥有,数据库也能正常启动。
绑定挂载的行为相反。主机上的内容就是容器看到的内容,包括所有权;镜像中该路径下的内容会被隐藏。如果主机目录不存在,Docker daemon 会创建该目录,而 daemon 以 root 身份运行,因此该目录会由 root:root 拥有。以非 root 用户运行的容器进程无法写入该目录:
PermissionError: [Errno 13] Permission denied: '/data/app.db'解决方法是让两侧的数字 ID 一致。绑定挂载中的所有权按数字用户 ID 比较,而不是按用户名比较,因为容器有自己的 /etc/passwd。容器内名为 app 的用户对主机没有意义。两侧的 uid 1000 都表示 uid 1000。
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id 会输出容器进程实际运行时使用的 uid。将主机目录的所有权改为该数字,或者在服务中使用 user: "1000:1000" 将容器固定为您的数字 ID。对于自行编写的应用,固定 user: 更简洁。对于不由您编写的镜像,修改主机目录所有权更安全,因为某些镜像会以 root 身份启动 entrypoint,然后降低权限,并要求底层目录具有特定所有权。
还需要注意两个问题。在启用 SELinux(security-enhanced Linux)的 Fedora、RHEL 及其他系统上,绑定挂载在重新标记前会被拒绝。因此,对于多个容器共享的路径,请添加 :z;对于仅供一个容器使用的路径,请添加 :Z,写法为 - ./data:/data:Z。此外,单个文件的绑定挂载,而不是目录的绑定挂载,会在编辑器替换文件而不是原地写入时失效,因为挂载跟随的是原始 inode。在您重启容器前,容器会继续看到旧内容。如果文件经常被编辑,请挂载其父目录。
性能:差异实际存在的地方
在 Linux 服务器上,两者都经过相同的内核路径,因此吞吐量差异很小,不应据此进行选择。使用默认 local 驱动的命名卷,与 Docker 的其他数据位于同一文件系统中,路径为 /var/lib/docker/volumes/;绑定挂载则位于您指定的路径。
在 macOS 和 Windows 上使用 Docker Desktop 时,差异会显现出来,因为容器运行在虚拟机中。此时,绑定挂载需要通过文件共享层,将主机文件系统中的数据传入虚拟机。对于包含大量小文件操作的工作负载,例如 Node.js 依赖树或 PHP 框架缓存,性能会明显下降。命名卷位于虚拟机内部,不会产生这项开销。因此,许多开发环境的 compose 文件会绑定挂载源代码目录,同时在 node_modules 上声明命名卷。
另一个实际差异是数据的存储位置。绑定挂载到 /mnt/backup 时,数据会写入该磁盘。命名卷则位于承载 /var/lib/docker 的文件系统中,而在 VPS 上,这通常是根磁盘。命名卷中的数据库不断增长时,会占满与系统日志相同的磁盘。应在问题演变为事故前检查:
docker system df -v
df -h /var/lib/dockerdocker system df -v 会列出所有卷及其大小,并标记不再被任何容器引用的卷。
检查命名卷
命名卷并不是黑盒。使用 Docker 查看其位置:
docker volume inspect myapp_pgdataMountpoint字段提供实际的主机路径,通常是/var/lib/docker/volumes/myapp_pgdata/_data。您可以使用sudo ls读取该路径,这适合快速检查。不要将其当作编辑文件的位置。以 root 身份在此处写入文件,会再次造成上文所述的所有权问题;而且该路径只是local驱动程序的具体实现细节,其他卷驱动程序并不提供相同路径。
安全的查看方式是使用挂载该卷的一次性容器:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /vol这种方式适用于任何驱动程序,看到的权限与实际容器看到的相同,并且由于--rm不会留下任何内容。
备份每种类型
绑定挂载本质上是普通目录,因此任何文件级备份工具都可以处理它。将备份目标指向主机路径即可。命名卷需要额外一步,因为工具必须进入卷内部。将该卷和主机目录同时挂载到同一个临时容器中,然后创建归档:
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .通过将归档恢复到新卷中完成还原:
docker volume create myapp_pgdata_restored
docker run --rm \
-v myapp_pgdata_restored:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /datatar在容器内以 root 身份运行时保留数字形式的所有权信息,因此还原后的卷仍可供应用使用。
两种类型都需要注意一点。数据库运行时复制其文件,相当于备份一个持续变化的目标,恢复后可能处于损坏状态。请先停止服务,或使用数据库自身的工具导出,如 docker compose exec -T db pg_dump -U postgres appdb > appdb.sql 所示。这样会生成一个普通文件,随后可以将其与 compose 文件一起纳入常规的 加密 restic 备份流程。
将 bind mount 迁移到命名卷
此迁移操作是复制,不是重命名,大约需要 1 分钟。
docker compose down
docker volume create myapp_pgdata
docker run --rm \
-v "$PWD/data":/from \
-v myapp_pgdata:/to \
alpine sh -c 'cp -a /from/. /to/'cp -a 会保留所有权、权限模式和时间戳,因此原目录的容器用户仍可读取新卷。然后将服务改为使用 pgdata:/var/lib/postgresql/data,在顶层 volumes: 块中添加 pgdata,运行 docker compose up -d,并在删除旧目录前查看应用日志。反向迁移时,使用相同的命令,但交换 /from 和 /to。
测试时请注意一点。docker compose down 不会处理命名卷,但 docker compose down -v 会删除项目声明的所有命名卷,而且无法撤销。bind mount 在这两种操作后都能保留,因为 Docker 从未拥有该目录。如果您还不熟悉这些生命周期命令,请参阅 适用于 VPS 的 Docker Compose 基础指南,其中介绍了这些命令。
按服务分别选择
先确认文件由谁写入。您在文本编辑器中修改并提交到 git 的配置,应放在绑定挂载中,并以 :ro 挂载,因为您需要直接查看这些文件并对其进行版本控制。您不会手动打开的应用状态数据,应放在命名卷中,因为 Docker 会正确设置权限,数据也不依赖主机路径。
媒体文件属于混合情况。照片库由应用写入,但也由您管理,而且通常足够大,需要存放在指定磁盘上。将其绑定挂载到该磁盘上的路径,并一次性明确设置所有权。大多数自托管服务栈最终都会采用这种模式:数据库和缓存使用命名卷,配置以及您重点管理的大型目录使用绑定挂载。支持工单系统 在 VPS 上运行的 Chatwoot 正是这种情况:Postgres 使用命名卷,上传的附件存放在可指定备份目标的路径中。
FAQ
bind mount 和 named volume 有什么区别?
bind mount 会将主机上的路径映射到容器中,因此两端看到的是同一个目录,您可以使用常规工具编辑它。named volume 是由 Docker 创建和管理的存储空间,通过名称引用,并在顶层 volumes: 块中声明。实际区别在于所有权:bind mount 适用于您维护的配置,named volume 适用于由应用维护的数据。
为什么 bind mount 会出现“permission denied”,而 named volume 不会?
空的 named volume 会从镜像中初始化,因此会继承镜像设置的所有权,容器用户可以写入其中。bind mount 会完全显示主机目录的现状;如果 Docker 必须创建该目录,则会将其所有者设为 root。运行 docker compose exec <service> id 查看容器使用的数字 ID,然后运行 sudo chown -R <uid>:<gid> 修改主机目录,或者在服务中设置 user: "1000:1000"。
Docker 在磁盘的什么位置存储 named volume?
使用默认的 local 驱动时,named volume 位于 /var/lib/docker/volumes/<volume>/_data 下,docker volume inspect <volume> 会输出准确的 Mountpoint。如果需要检查内容,可以读取该路径;但只能通过容器写入,因为在主机上以 root 身份编辑会以容器无法预期的方式改变所有权。
如何备份 named volume?
运行一个短生命周期容器,同时挂载该 volume 和一个主机目录,然后使用 docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data . 将数据从一侧归档到另一侧。对于数据库,应使用数据库自身的工具执行转储,而不是复制正在使用的文件,因为在写入进行期间创建的文件副本可能会在恢复时导致数据损坏。
docker compose down 会删除我的 volume 吗?
docker compose down 会删除容器和网络,但会保留 named volume。docker compose down -v 还会永久删除项目声明的所有 named volume。两条命令都不会删除 bind mount,因为该目录属于主机,而不属于 Docker。