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 管理卷的存储位置,但容器仍通过普通文件系统操作读写卷,因此所有权和权限模式的强制方式与绑定挂载相同。如果命名卷挂载到镜像中不存在的路径,或者最初由其他镜像或用户填充,则不会从镜像获得所有权,非 root 进程同样可能无法在那里执行操作。下面的 id 检查适用于这两种类型。
绑定挂载的行为正好相反。主机上的内容就是容器看到的内容,包括所有权,镜像在该路径中的内容则会被隐藏。如果主机目录不存在,Docker daemon 会创建它,而 daemon 以 root 身份运行,因此你会得到一个归 root:root 所有的目录。以非 root 用户运行的容器进程随后无法写入该目录:
PermissionError: [Errno 13] Permission denied: '/data/app.db'解决方法是让两边的数字一致。绑定挂载的所有权按数字用户 id 比较,而不是按名称比较,因为容器有自己的 /etc/passwd。容器内名为 app 的用户对主机没有意义。Uid 1000 在两边都表示 uid 1000,除非 daemon 使用 user-namespace remapping(userns-remap,在 /etc/docker/daemon.json 中设置,或通过 dockerd 命令行选项设置)。Remapping 会将每个容器 id 映射到主机上的 subordinate 范围。在 Docker 示例中,范围从 231072 开始,因此容器 uid 0 对应主机 uid 231072,容器 uid 1000 对应主机 uid 232072。
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id 会打印容器进程实际以哪个 uid 运行。未启用 remapping 时,将主机目录的所有权改为该数字,或者在服务中使用 user: "1000:1000" 将容器固定为你的数字。启用 remapping 后,user: 无法将容器固定为你的主机数字,因为你设置的 id 也会被偏移:在该示例范围内,user: "1000:1000" 仍会对应主机 uid 232072。应将主机目录改为映射后的主机 id 所有(你自己的范围位于 /etc/subuid 和 /etc/subgid 中)。Docker 文档仅将 --userns=host 与 docker container create、run 或 exec 一起使用,作为为单个容器关闭 remapping 的方式。对于自行编写的应用,固定 user: 更简洁。对于并非自行编写的镜像,修改主机目录所有权更安全,因为某些镜像会以 root 身份启动 entrypoint,随后降权,并要求下层目录具有特定所有权。
还需要注意两个问题。在 Fedora、RHEL 以及其他启用 SELinux(安全增强型 Linux)强制模式的系统上,绑定挂载在重新标记之前会被拒绝,因此,对于多个容器共享的路径,请添加 :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 的配置,应使用 bind mount,并挂载到 :ro,因为您需要让它可见且纳入版本控制。您不会手动打开的应用状态,应使用命名卷,因为 Docker 会根据镜像设置权限,数据也不依赖主机路径。
媒体文件属于混合情况。照片库由应用写入,但也由您管理,而且通常足够大,需要放在指定磁盘上。将其绑定挂载到该磁盘上的路径,并一次性明确设置所有权。大多数自托管堆栈最终都会采用这种模式:数据库和缓存使用命名卷,配置以及您重点管理的大型目录使用 bind mount。支持工单系统 运行在 VPS 上的 Chatwoot 正是这种情况:Postgres 使用命名卷,上传的附件存放在可供备份工具指定的路径中。
FAQ
绑定挂载和命名卷有什么区别?
绑定挂载会将主机上的路径映射到容器中,因此两边看到的是同一个目录,你可以使用普通工具编辑它。命名卷是 Docker 创建并管理的存储,通过名称引用,并在顶层的 volumes: 块中声明。实际区别在于所有权:绑定挂载用于你维护的配置,命名卷用于应用维护的数据。
为什么绑定挂载会出现 "permission denied",而命名卷不会?
空的命名卷会从镜像中填充,因此通常会继承镜像设置的所有权,容器用户可以写入。它仍会强制执行权限,所以如果镜像在该路径没有内容,也可能以相同方式失败。绑定挂载会完全显示主机目录的实际状态;如果 Docker 必须创建该目录,则该目录归 root 所有。运行 docker compose exec <service> id 查看容器使用的数字 id,然后对主机目录执行 sudo chown -R <uid>:<gid>;或者,如果 daemon 未进行 user namespace remapping,则在服务中设置 user: "1000:1000"。如果 daemon 使用 user-namespace remapping,则应改为将所有权改为映射后的主机 id,因为容器的 id 会在主机上发生偏移,而 user: 不会改变这一点。
Docker 将命名卷存储在磁盘的什么位置?
使用默认的 local driver 时,命名卷位于 /var/lib/docker/volumes/<volume>/_data 下,docker volume inspect <volume> 会打印准确的 Mountpoint。如果需要检查内容,可以读取该路径,但只能通过容器写入,因为在主机上以 root 身份编辑会改变所有权,容器可能无法按预期使用。
如何备份命名卷?
运行一个短生命周期容器,同时挂载该卷和主机目录,然后使用 docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data . 将数据从一处归档到另一处。对于数据库,应使用数据库自身的工具执行转储,而不是复制实时文件,因为在写入进行期间执行的文件复制可能会恢复为损坏状态。
docker compose down 会删除我的卷吗?
docker compose down 会删除容器和网络,并保留命名卷。docker compose down -v 还会永久删除项目声明的每个命名卷。两个命令都不会删除绑定挂载,因为该目录属于主机而不属于 Docker。