Skip to content

数据持久化

容器默认可写层跟着容器生命周期走——容器删了,里面的文件也没了。Docker 持久化主要靠 volume、bind mount 和 tmpfs。三者看起来都是挂载目录,但适用场景、生命周期和权限处理完全不同。

容器可写层的边界

可写层适合临时文件、缓存、中间产物。日志、上传文件、数据库数据如果只写在可写层,容器删除就全没了。验证一下就能感受到这个特性:

bash
docker run -dit --name tmpbox alpine sh
docker exec tmpbox sh -c 'echo hello > /data.txt'
docker rm -f tmpbox

docker run --rm alpine sh -c 'ls /data.txt'  # 新容器看不到前一个容器写的文件

三种挂载方式的本质区别

类型数据位置谁管理路径适合场景
volumeDocker 管理的目录(通常在 /var/lib/docker/volumes/Docker数据库数据、需要长期保留的应用数据
bind mount指定宿主机绝对路径运维人员配置文件、开发目录、需要明确路径的数据
tmpfs宿主机内存内核临时敏感文件、缓存、不需要落盘的数据

从容器内看,它们都是目录或文件。差异全在宿主机上:谁管理路径、生命周期怎么处理、权限怎么控制、备份怎么做。

volume

volume 由 Docker 管理,不直接暴露宿主机路径:

bash
docker volume create nginx-data
docker volume inspect nginx-data  # 查看在宿主机上的实际挂载点

docker run -d \
  --name web \
  --mount type=volume,source=nginx-data,target=/usr/share/nginx/html \
  -p 8080:80 \
  nginx:1.26

删除容器后数据还在,重新挂载同一个 volume 就能恢复:

bash
docker rm -f web
docker run -d --name web2 \
  --mount type=volume,source=nginx-data,target=/usr/share/nginx/html \
  -p 8080:80 \
  nginx:1.26

volume 适合"数据要跟着 Docker 走"的场景。宿主机上直接改 volume 目录不是好做法——绕过 Docker 的文件组织方式和权限管理,后续迁移时路径会很难对应。

bind mount

bind mount 把宿主机指定路径直接挂进容器:

bash
mkdir -p /opt/docker/nginx/html
echo "bind-data" > /opt/docker/nginx/html/index.html

docker run -d \
  --name bind-web \
  --mount type=bind,source=/opt/docker/nginx/html,target=/usr/share/nginx/html,readonly \
  -p 8081:80 \
  nginx:1.26

readonly 让容器内进程不能修改宿主机目录。静态配置、只读证书、只读网页目录都适合这样挂。

bind mount 的风险在于路径明确——也很危险。挂错路径可能把宿主机关键目录暴露给容器;容器内进程如果有写权限,就能直接改宿主机文件。生产里 bind mount 的目标路径要写在变更记录里,不靠记忆。

SELinux 和 bind mount 的权限陷阱

RHEL / Rocky / CentOS 上,bind mount 权限看起来正确(ls -l 没问题),容器里仍然读写失败时,除了 UID/GID,还要看 SELinux label:

bash
ls -Zd /opt/docker/nginx/html   # 查看 SELinux context
getenforce                      # Enforcing 表示 SELinux 正在强制执行

Docker 支持在挂载参数后加 :z:Z 处理 SELinux 标签:

bash
docker run -d \
  --name selinux-web \
  -v /opt/docker/nginx/html:/usr/share/nginx/html:ro,z \
  -p 8082:80 \
  nginx:1.26
参数说明
:z共享标签,多个容器共同访问同一路径时用
:Z私有标签,只给一个容器独占访问

:Z 会修改宿主机路径的 SELinux 标签。系统目录、共享目录、已经被其他服务使用的目录不能随手加。生产里更稳妥的做法是给容器准备专用数据目录,再按需设 :z:Z

Ubuntu 系的 AppArmor 对 bind mount 的影响方式和 SELinux 不同。遇到权限正常但访问被拒绝时,除了 Docker 参数,也要看系统安全模块日志(dmesg/var/log/audit/audit.log)。

tmpfs

tmpfs 把数据放在内存里,不写磁盘:

bash
docker run --rm \
  --mount type=tmpfs,target=/run/cache,tmpfs-size=64m \
  alpine sh -c 'df -h /run/cache && echo token > /run/cache/token'

适合临时敏感数据(如临时 token、会话缓存)或中间计算产物。容器停止后数据消失,宿主机重启后也不保留。需要恢复的数据不能放 tmpfs。

备份 volume

备份 volume 的常见做法是启动一个临时容器,把 volume 和宿主机备份目录同时挂进去,打包:

bash
mkdir -p /backup/docker

docker run --rm \
  --mount type=volume,source=nginx-data,target=/data,readonly \
  --mount type=bind,source=/backup/docker,target=/backup \
  alpine sh -c 'tar czf /backup/nginx-data.tar.gz -C /data .'

数据库类数据的备份更适合用数据库自己的备份工具(mysqldump、redis BGSAVE 等)。直接打包 volume 只有在确认数据一致性以后才合适——停库后备份,或使用支持快照一致性的存储。

UID/GID——名字不重要,数字才关键

容器内用户和宿主机文件权限通过 Linux UID/GID 数字对应。名字可能不同,数字才决定能不能读写。

bash
docker run --rm alpine id
docker run --rm -u 1000:1000 alpine id  # 容器内进程以 1000:1000 运行

bind mount 时经常遇到容器内没权限写文件,处理方式:

方式说明
调整宿主机目录属主让目录 UID/GID 和容器运行用户一致
指定 -u让容器按指定 UID/GID 运行
使用只读挂载配置类文件减少写权限
设计专用数据目录不把系统目录直接挂给容器

权限问题早一点整理,后面迁移和备份时能省很多时间。

删除语义的差异

操作结果
docker rm <容器>删除容器,命名 volume 保留
docker rm -v <容器>删除容器,同时删除挂在这个容器上的匿名 volume
docker volume rm <卷名>删除指定 volume
docker compose down删除容器和网络,默认保留命名 volume
docker compose down -v删除容器、网络和项目 volume

docker compose down -v 在实验环境很方便,在有数据的环境里要格外小心。数据库、上传目录这类数据如果在 compose volume 里,一个 -v 就会清掉。

删除前先确认挂载:

bash
docker inspect web --format '{{json .Mounts}}'
docker volume ls

迁移数据

迁移容器数据时要先确认数据在哪,然后迁移、验证、保留备份:

项目检查点
数据位置volume、bind mount、还是外部存储
一致性是否需要停服务、是否有事务未刷盘
权限UID/GID、SELinux/AppArmor、只读挂载
恢复验证新容器能否读写,应用能否正常启动
备份保留原始备份不要迁完立刻删除

容器迁移不是只拷镜像。镜像决定程序怎么跑,volume 和宿主机挂载决定数据还在不在。