Docker Compose 绑定挂载和命名卷怎么选?
了解 Docker Compose 中绑定挂载与命名卷的区别:配置文件和应用数据分别该用什么,为什么会出现权限错误,以及如何检查、备份和迁移。
绑定挂载还是命名卷:简短答案
Docker Compose 卷分为两种,选择哪一种取决于文件由谁管理。对于您自行写入和读取的文件,请使用绑定挂载,例如配置文件、模板和静态网站。对于由应用管理的数据,请使用命名卷,例如数据库文件、搜索索引和上传的媒体文件。绑定挂载指向主机上的路径,您可以在编辑器中打开该路径。命名卷是 Docker 创建并跟踪的存储,您需要通过 Docker 访问它。
两者都位于服务中的同一个 volumes: 键下,因此容易混淆。区别在于冒号左侧的内容。以 . 或 / 开头的左侧内容是主机路径,因此属于绑定挂载。其他内容都是名称,因此属于命名卷,并且该名称还必须在顶级 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 身份运行,因此该目录的所有者会是 root:root。以非 root 用户运行的容器进程随后无法写入该目录:
PermissionError: [Errno 13] Permission denied: '/data/app.db'解决方法是让这些数字保持一致。绑定挂载中的所有权按数字用户 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" 将容器固定为您的 uid。对于您自己编写的应用程序,固定 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 备份流程。
将绑定挂载迁移到命名卷
这是复制操作,不是重命名操作,约需 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 会删除项目声明的所有命名卷,且无法撤销。绑定挂载在这两种操作后都会保留,因为 Docker 从未拥有该目录。如果您还不熟悉这些生命周期命令,请参阅 VPS 的 Docker Compose 基础指南。
逐个服务选择
确定文件由谁写入。您在文本编辑器中修改并提交到 git 的配置,应使用绑定挂载,并挂载到 :ro,因为您需要查看这些文件并对其进行版本控制。您不会手动打开的应用程序状态,应使用命名卷,因为 Docker 会正确初始化权限,而且数据不依赖主机路径。
媒体文件属于混合情况。照片库由应用程序写入,但也由您管理,而且通常足够大,需要使用指定磁盘。将其绑定挂载到该磁盘上的路径,并一次性明确设置所有权。大多数自行托管的服务栈最终都会采用这种模式:数据库和缓存使用命名卷,配置和您关注的大型目录使用绑定挂载。
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。