Appearance
数据持久化
容器默认可写层跟着容器生命周期走——容器删了,里面的文件也没了。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' # 新容器看不到前一个容器写的文件三种挂载方式的本质区别
| 类型 | 数据位置 | 谁管理路径 | 适合场景 |
|---|---|---|---|
| volume | Docker 管理的目录(通常在 /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.26volume 适合"数据要跟着 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.26readonly 让容器内进程不能修改宿主机目录。静态配置、只读证书、只读网页目录都适合这样挂。
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 和宿主机挂载决定数据还在不在。