本节摘要:容器是"用完即焚"的,容器内部写入的数据随容器删除而消失,持久化就是把数据放到容器生命周期之外的机制。Docker 提供三种挂载方式:命名卷由 Docker 托管、绑定挂载直连宿主机目录、tmpfs 只存内存。本节对比三者的生命周期与适用场景,解释数据库数据为什么必须进命名卷,并专门讨论 UID 与 GID 引发的权限问题,最后给出备份与恢复的命令配方。
容器镜像由只读层叠加而成,容器运行时写入的文件落在一个可写层里。这个可写层跟容器绑定:容器删除,可写层一并删除。docker-compose down 不会删卷,但 docker-compose down -v 或者手工 docker rm 之后再重建,容器内数据就是新的了。
所以规则只有一条:凡是删了容器就不能丢的数据,必须放到容器之外。卷(volume)就是 Docker 提供的"容器之外":它绕过容器的联合文件系统,直接落在宿主机文件系统或内存里。卷有三个天然属性:容器删除后数据仍在、多个容器可以共享同一个卷、卷可以独立备份和恢复。
| 对比项 | 命名卷 | 绑定挂载 | tmpfs |
|---|---|---|---|
| 数据落点 | Docker 管理目录 | 宿主机任意路径 | 宿主机内存 |
| 谁创建 | 首次挂载自动创建 | 手动准备目录 | 容器启动即创建 |
| 容器删除后 | 数据保留 | 宿主机文件保留 | 数据立即消失 |
| 是否跨容器共享 | 可以 | 可以 | 不可以 |
| 主要用途 | 数据库、上传目录 | 配置文件、源码热更新 | 会话、缓存、密钥临时存放 |
| 备份方式 | docker run 打包 | 宿主机工具直接拷贝 | 无需备份 |
选择逻辑一句话:数据库和长期数据用命名卷,需要直接读写宿主机文件的场景用绑定挂载,只活一瞬的临时数据用 tmpfs。三种挂载可以混用在同一份 compose 文件里:一个服务挂命名卷存数据、挂绑定挂载读配置、挂 tmpfs 放临时文件,互不冲突。
下面这张图把三种方式从创建到销毁的完整生命周期并排画出来,差异一目了然。

命名卷的用法在 2.2 节已经出现过:db_data:/var/lib/postgresql/data,文件底部用 volumes 段声明 db_data:。挂载时卷不存在会自动创建,Docker 把数据存在自己的管理目录下,典型位置是 /var/lib/docker/volumes 下的同名目录,目录内部以 _data 结尾,比如 <卷名>/_data。日常操作不需要碰这个路径,用 docker volume ls 查看卷列表、docker volume inspect 查看详情即可。
为什么数据库数据必须放命名卷?三个理由:生命周期独立——容器删了重建,挂回同一个卷名,数据原样回来;管理统一——备份、迁移、清理都有专门命令,不会像散落的目录那样失控;避免权限裸奔——绑定挂载会把宿主机目录的权限直接暴露给容器进程,而命名卷首次挂载时会按镜像声明的用户初始化所有权,省掉大量权限折腾。
Redis 的配方更短:
version: "3.9" services: redis: image: redis:7.2 volumes: - redis_data:/data volumes: redis_data:
redis_data:/data 把命名卷挂到 Redis 的持久化目录,AOF 与 RDB 快照都写进卷里。想要删除一个不再使用的卷,先确保没有容器在挂载它,然后 docker volume rm redis_data。
命名卷的数据量会一直增长,数据库卷尤其明显。建议给卷所在磁盘留足余量,定期用 df 检查;备份策略按数据重要程度分级:核心业务每日备份加异地存放,工具类数据每周一次即可。卷的备份文件本身也要加密,数据库明文备份文件泄露和数据库泄露是一个性质。
绑定挂载把宿主机路径直接挂进容器,语法是宿主机路径加容器路径:./html:/usr/share/nginx/html。它的价值在于"宿主机直接可见":开发时改源码即时生效,部署时把宿主机上现成的配置文件挂进容器,都不用进容器。
典型用法是把配置外置:
version: "3.9" services: web: image: nginx:1.21 ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro
尾部 :ro 是只读,容器内改不了配置文件,宿主机侧仍然随意编辑。配置文件、证书、初始化脚本都适合这种挂法。
代价是责任转移:目录要自己建、权限要自己管、Docker 不追踪它的生命周期,docker compose down -v 也删不掉它。所以我们的原则是:能上命名卷就不上绑定挂载,绑定挂载留给"必须与宿主机直接交互"的场景。
tmpfs 把数据放在宿主机内存里,不落盘。Compose 里用长语法声明:
version: "3.9" services: web: image: nginx:1.21 ports: - "80:80" volumes: - type: tmpfs target: /tmp
target 指定容器内挂载点,这里把 /tmp 变成内存盘。适合放会话票据、一次性缓存、中间计算产物这类"丢了也无所谓"的数据。两条铁律:一是别放重要数据,容器一停全没;二是大小受宿主机内存约束,别把大文件往里塞,拖垮整台机器。
绑定挂载的经典故障长这样:容器日志报 Permission denied,明明宿主机目录权限看起来没问题。原因在于容器内进程的用户和宿主机文件属主不是同一个。PostgreSQL 官方镜像默认用 uid 999 运行,宿主机目录属主如果是 root,容器内就写不进去;反过来,容器以 root 写入的文件,宿主机普通用户也删不动。
排查顺序:先用 docker exec 进容器看进程身份,再用 ls -n 看宿主机的 uid 与 gid,最后决定方案——改宿主机目录属主、给容器指定 user,或在镜像构建阶段把目录 chown 给对应 uid。命名卷能绕开大半这类问题,因为 Docker 在首次挂载时会按镜像声明的所有权初始化卷内容,这也是我们反复推荐命名卷的实操原因。
容器写卷的权限还取决于首次挂载的初始化时机:命名卷为空时,Docker 会把镜像里挂载点目录的所有者与权限复制到卷上,镜像声明了非 root 用户(比如 postgres 用户)就能直接写;绑定挂载则完全继承宿主机现状,容器用户与宿主机属主对不上就报 Permission denied。这个差异就是"数据库配命名卷少踩坑"的底层原因。
命名卷备份的标准姿势是用一个临时容器挂载卷并打包:
docker run --rm -v <volume_name>:/data -v $(pwd):/backup alpine tar cvf /backup/backup.tar /data
容器挂两个卷:目标数据卷挂到 /data,当前目录挂到 /backup,alpine 里一条 tar 把数据打成包落到宿主机当前目录。恢复是反向操作:
docker run --rm -v <volume_name>:/data -v $(pwd):/backup alpine tar xvf /backup/backup.tar -C /data
绑定挂载不用这么绕,宿主机备份工具直接拷贝目录即可。备份策略给三条底线:定期执行(按数据量定周期)、备份放不同存储位置防单点、定期演练恢复流程——备份恢复不了等于没备份。
⚠️ docker-compose down -v 会连同命名卷一起删除,数据无法找回。这条命令只在自己清楚要清场时用,日常 down 不要带 -v。
💡 卷名保持"项目加用途"的命名习惯,比如 blog_db_data。同名卷跨项目共享是合法的,但隐式耦合很难查,我们更倾向一个项目一套卷名。
卷的日常管理集中在几条命令:docker volume ls 列出全部卷,docker volume inspect <卷名> 查看挂载点与驱动信息,docker volume prune 清理未被容器引用的卷。prune 有杀伤力,执行前先 ls 确认,生产环境建议按项目标签过滤,只清自己打标的卷。
compose 里除了短语法 db_data:/var/lib/postgresql/data,还能用长语法显式控制挂载属性:
services: db: image: postgres:14 volumes: - type: volume source: db_data target: /var/lib/postgresql/data read_only: false
长语法在需要声明只读、指定卷驱动时更清晰,日常短语法足够。磁盘告警时想知道卷占了多少,进宿主机看 /var/lib/docker/volumes 下对应目录,du 一把就有数;先确认容器已停再统计,运行中的数据库目录会被占用导致数据不准。
给每份数据过四问:跨容器存活是不是硬需求——是则命名卷;宿主机需要直接编辑吗——是则绑定挂载;允许随时丢失吗——允许则 tmpfs;数据量会不会超过内存——超过就告别 tmpfs。四问走完,选型基本不会偏。再补一条纪律:同一个服务可以同时挂命名卷与 tmpfs,比如数据库数据进卷、临时排序文件进内存盘,别被"一个服务一种挂载"的想法困住。最后一条:卷的命名和目录规划一样值得投入,命名清晰的项目,三个月后回来做迁移,卷的归属一目了然。
数据有了归宿,下一个问题就是服务之间怎么找到对方、怎么隔离——2.4 节进入网络配置与服务间通信。