1. 为什么容器里的数据会“丢”:理解容器文件系统的本质
开始聊数据卷之前,我先把一个很多人踩过的坑摆出来:你敢不敢在没做任何持久化处理的情况下,直接跑一个 MySQL 容器,然后往里写业务数据?我当年第一次干这事,容器运行得好好的,数据也查得到,结果某天手一抖执行了docker rm -f mysql,再重新启动一个新容器,所有表、数据、配置全部灰飞烟灭,那一刻真是汗流浃背。
这个问题的根源,得从容器文件系统的镜像分层机制说起。Docker 镜像本质上是一堆只读层的堆叠,容器运行时在只读层之上加一层可写层。你对容器文件系统做的所有修改,包括写入文件、安装软件、修改配置,都会被记录在这个可写层里。听着好像没问题,但一旦容器被删除,这一层可写层也会跟着销毁,数据自然就没了。这还不算完,哪怕容器还在,只是被重启,有些写入如果不落盘,也会因为各种原因丢失。
所以数据卷要解决的核心问题,说白了就三个:
- 持久化:容器删了、重建了,数据还得在。数据库、配置文件、日志这类东西,必须脱离容器的生命周期存在。
- 共享:多个容器需要读写同一份数据,比如 Nginx 和 PHP-FPM 两个容器要协同处理同一个项目目录里的文件。
- 性能:容器可写层在部分存储驱动下性能一般,而数据卷是直接挂载宿主机目录或专用存储卷,读写效率更稳。
换句话说,数据卷是容器世界里“数据不随容器陪葬”的根本保障。谁要是觉得“容器里的文件只要还在容器里就算安全”,那迟早会被生产事故教育。
让我用一个生活类比帮你建立直觉:容器就像一间临时租来的房子,镜像就是精装修的样板间。你住进去之后添置的家具、写的文件、改的装修,全在房子内部。某一天房东(Docker)说房子到期拆了重建,你屋里所有私人物品全部没了。数据卷呢,就相当于你在外面租了一个仓库,把贵重东西统统放进去,房子的墙、地板和仓库是连通的,房子拆了重建一百遍,仓库里的东西纹丝不动。
2. 三种持久化方案怎么选:Volume、Bind Mount、tmpfs 的区别与实战
Docker 官方提供三种数据挂载方式,很多人一上来就懵:到底用-v还是-mount?到底该挂目录还是该建卷?我先把三者的本质区别摊开讲清楚,再给你一套可以“抄作业”的选型逻辑。
2.1 数据卷(Volume):Docker 替你管理的那块地
数据卷是 Docker 官方最推荐的方式。它的特点是:由 Docker 守护进程在宿主机上创建一个专门目录(默认在/var/lib/docker/volumes/下),然后把容器里的路径挂到这个目录上。你不需要关心这个目录具体在哪,Docker 自己管。
用-v创建具名卷并挂载的写法:
docker volume create mydata docker run -d --name nginx-volume -p 8080:80 -v mydata:/usr/share/nginx/html nginx:latest也可以不提前建卷,直接写卷名,Docker 会自动创建:
docker run -d -v mydata:/usr/share/nginx/html nginx:latestVolume 的核心优势有三个:
- 跨平台迁移方便,用
docker volume系列命令可以统一管理。 - 不用担心宿主机目录权限问题,Docker 会配合容器内用户处理。
- 配合
docker volume backup之类的工具或第三方插件可以做更精细的备份策略。
缺点是:如果你需要直接到宿主机上改文件,还得去翻 Docker 的存储目录,路径比较深,操作起来不够直观。
2.2 绑定挂载(Bind Mount):直接指路宿主机目录
绑定挂载是把宿主机上的任意目录直接映射到容器里。这个方式最直观,也最适合开发环境,比如你本地代码在/home/user/project,想直接在容器里跑服务并实时看到代码修改,那就 bind mount:
docker run -d --name web-dev -p 3000:3000 -v /home/user/project:/app node:18注意:-v后面只要带绝对路径,Docker 就认为是 bind mount;不带路径只有名字,就认为是 volume。这个区分很容易绕晕,其实记住一句口诀就行——“带斜杠的是宿主机路径,不带斜杠的是卷名”。
Bind mount 的灵活性高,但也有个大坑:宿主机的目录权限会直接覆盖容器内目录权限。比如宿主机目录是 root 所有、权限 755,容器内进程以普通用户运行,那容器里就是没权限写入。
2.3 tmpfs:不落盘的内存挂载
tmpfs 直接挂在内存里,不写宿主机磁盘,适合放密码、临时文件、session 这类重启就无所谓的数据。性能极好,但数据没了就是真没了。用法:
docker run -d --name temp-test --tmpfs /tmp nginx:latest这个方式在生产里用得不多,主要在开发调试或跑短任务时用。
2.4 选型实战:Nginx 静态站点挂载
我拿一个最常见的需求来演示:把宿主机上的静态网站目录挂到 Nginx 容器里。假设目录是/data/www,里面有index.html。两个等价命令:
方式一:-v
docker run -d --name website -p 8080:80 \ -v /data/www:/usr/share/nginx/html:ro nginx:latest方式二:--mount
docker run -d --name website -p 8080:80 \ --mount type=bind,src=/data/www,dst=/usr/share/nginx/html,readonly nginx:latest--mount语法比-v啰嗦,但语义更清晰,适合写进脚本或 compose 文件里让人一眼看懂。ro只读挂载是个好习惯,静态文件本来就不该被容器改写,加个只读能防止容器出问题的时候把宿主机文件弄乱。
我给一张三种方式的对照表,方便你存档:
| 特性 | Volume | Bind Mount | tmpfs |
|---|---|---|---|
| 宿主机位置 | Docker 管理目录 | 自定义任意路径 | 内存 |
| 适合场景 | 数据库数据、容器间共享、生产持久化 | 代码开发、配置注入 | 临时文件、密钥 |
| 持久化 | 是 | 是 | 否 |
| 性能 | 好 | 取决于宿主机磁盘 | 极快 |
| 管理难度 | 低 | 中 | 低 |
| 跨主机迁移 | 容易 | 较麻烦 | 不适用 |
3. 从零规划持久化存储:MySQL 容器完整实战
聊了这么多理论,该上手干点真东西了。我用 MySQL 容器做持久化实战,因为它是数据持久化需求最典型、最不能出岔子的场景。你在热词列表里应该也看到了大量“docker安装mysql失败”“docker安装mysql8.0并使用”“访问docker容器内的mysql”这类搜索,说明这块确实是新手重灾区。
3.1 创建数据卷并挂载 MySQL
第一步先建立专用的数据卷。有人喜欢让 Docker 自动建,但我的习惯是显式创建并命名,因为后续做备份、迁移、查看元数据时,具名卷比匿名卷好找得多。
docker volume create mysql-data docker volume create mysql-config然后启动 MySQL 8.0 容器:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=MyStrongPass123 \ -e MYSQL_DATABASE=appdb \ -e MYSQL_USER=appuser \ -e MYSQL_PASSWORD=AppUserPass123 \ -v mysql-data:/var/lib/mysql \ -v mysql-config:/etc/mysql/conf.d \ mysql:8.0这里有两个细节需要展开讲。
第一个细节是挂载路径的选择。/var/lib/mysql是 MySQL 的数据目录,这个必须挂;/etc/mysql/conf.d是配置文件目录,挂它主要是为了日后调整配置不用进容器。很多教程只告诉你挂数据目录,这是对的,但生产环境你往往还需要改字符集、改max_connections,这些配置散落在容器里,如果不挂出来,每次改配置都得docker exec进容器用 vi 改,效率极低,而且容器一重建配置又没了。
第二个细节是权限。网上大量“docker安装mysql失败”的案例,其实就是卡在权限上。MySQL 容器内的 mysql 用户 UID 是 999,它要求数据目录的属主必须是这个 UID。如果你在宿主机上预先创建了目录并手动改了权限成 root,容器起来就直接报Permission denied。所以用 Docker 管理的具名卷反而是最省事的——Docker 初始化卷的时候会自动处理好属主和 SELinux 上下文。
跑起来之后,先测试一下数据是否真的写入了:
docker exec -it mysql8 mysql -uroot -pMyStrongPass123 -e "CREATE DATABASE testdb; USE testdb; CREATE TABLE t1(id INT); INSERT INTO t1 VALUES (1); SELECT * FROM t1;"看到查询结果后,我们模拟最惊险的场景:删库跑路式操作——直接删除容器。
docker stop mysql8 docker rm mysql8然后重新用同样的卷创建新容器:
docker run -d \ --name mysql8-new \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=MyStrongPass123 \ -v mysql-data:/var/lib/mysql \ -v mysql-config:/etc/mysql/conf.d \ mysql:8.0再查一次刚才那张表:
docker exec -it mysql8-new mysql -uroot -pMyStrongPass123 -e "SELECT * FROM testdb.t1;"你会发现数据安安静静地躺在那里。这就是数据卷最核心的价值:容器可以随时推倒重来,数据永远锚定在卷里。
3.2 数据卷的备份、恢复与迁移
持久化的另一面是备份和迁移。如果你只做了持久化但不会备份,那跟裸奔没区别。手动备份数据卷的标准姿势是用一个临时容器把卷打包:
docker run --rm \ -v mysql-data:/source \ -v /data/backup:/backup \ ubuntu:22.04 \ tar czf /backup/mysql-data-$(date +%F).tar.gz -C /source .解释一下这条命令的逻辑:--rm表示容器运行完立刻删除,不会残留垃圾;“-v mysql-data:/source” 把数据卷挂到容器里的 /source;“-v /data/backup:/backup” 把宿主机备份目录挂进去;容器里执行的命令是把 /source 内容打包到 /backup。整个操作完全不碰 MySQL 本身,纯粹是文件级备份,非常干净。
恢复备份的思路反过来:
docker run --rm \ -v mysql-data:/target \ -v /data/backup:/backup \ ubuntu:22.04 \ tar xzf /backup/mysql-data-2025-01-01.tar.gz -C /target注意:恢复前最好先把目标卷里现有的数据清掉,不然可能出现新旧文件混杂。清空卷可以用docker volume rm或临时容器执行find /target -type f -delete。
迁移到另一台机器也不难,思路就是“打包卷文件 → 传到新机器 → 恢复到同名卷”。生产环境里我一般结合rsync做增量同步,隔一段时间把卷目录直接同步过去,配合主从复制,基本能做到分钟级 RPO。
3.3 绑定挂载方式实战:配置文件管理
有些情况下你不想用 Docker 管理的卷,而是想直接维护宿主机上的配置文件。比如 Redis,它的配置经常需要调整,用 bind mount 最直观:
mkdir -p /data/redis cp /path/to/redis.conf /data/redis/redis.conf docker run -d \ --name redis-server \ -p 6379:6379 \ -v /data/redis/redis.conf:/etc/redis/redis.conf \ -v /data/redis/data:/data \ redis:7.0 \ redis-server /etc/redis/redis.conf这里有一个血泪教训:如果你 bind mount 了一个宿主机上不存在的文件,Docker 会自动帮你创建一个空目录,而不是空文件。也就是说,如果/data/redis/redis.conf不小心被我写成了/data/redis/redis.conf/(多了个斜杠),容器里看到的就不是配置文件而是一个目录,Redis 直接启动失败。这个坑在搭建 Redis 主从时经常遇到,因为每台机器的配置文件名稍有差异就容易被路径问题绊倒。
绑定挂载还有个隐患:宿主机上的配置文件权限如果不对,容器内的程序可能读不到。我踩过一次:把宿主机上 root 所有的 redis.conf 挂进容器,Redis 进程以 redis 用户运行,结果启动时报Can't open the log file: Permission denied。解决方案很简单,把配置文件的属主改成容器内用户,或者给文件开 644 权限。
4. 多容器数据共享:你要知道的几种姿势与坑
持久化只是数据卷的一半功能,另一半是共享。多个容器要访问同一份数据,这个需求在微服务、前后端分离、定时任务等架构里太常见了。
4.1 同卷共享:最简单的多容器读写同一份数据
假设你有两个容器,一个写日志,一个读日志做分析。两个容器共享同一个卷:
docker volume create shared-logs docker run -d --name producer -v shared-logs:/logs busybox \ sh -c "while true; do echo \"$(date)\" >> /logs/app.log; sleep 2; done" docker run -d --name consumer -v shared-logs:/logs ubuntu:22.04 \ tail -f /logs/app.log这个做法的优点是简单直接,缺点是并发写的时候可能出现锁竞争,尤其是数据库类应用共享卷时的并发一致性。所以生产上共享卷更多用于只读场景或追加写场景,比如配置文件分发、静态资源供给、日志聚合。
4.2--volumes-from:数据卷继承
--volumes-from是 Docker 里非常有年代感但依然好用的功能。它的作用是:新容器直接继承另一个容器的所有挂载点。比如你启动了一个配置初始化容器,里面有各种配置文件卷,后续容器可以直接这样继承:
docker run -d --name config-generator -v /data/configs:/configs alpine \ sh -c "echo 'server: {port: 8080}' > /configs/app.yml && sleep 3600" docker run -d --name web-app --volumes-from config-generator -p 8080:8080 myapp:latestweb-app 容器里直接就能在/configs下看到生成的配置。这个功能在你需要“一个容器负责初始化数据,另一个容器负责跑业务”的架构里特别顺手,不需要关心数据卷的名字到底是什么,只要知道哪个容器挂了卷就行。
但是--volumes-from有个容易被忽视的坑:它继承的是“挂载关系”,而不是快照。如果源容器被删了,挂载关系依然存在(底层卷还在),但如果你源容器用的是匿名卷,源容器删了之后匿名卷不会被自动删除,却会变成“孤儿卷”,时间长了占用磁盘。所以用这个功能时,建议源容器要么用具名卷,要么一直保持运行状态。
4.3 容器与宿主机之间的文件交换
很多新手操作容器文件都用docker cp:
docker cp ./local-file.txt container-name:/app/ docker cp container-name:/app/output.txt ./local-output.txtdocker cp适合临时传小文件,正式场景还是要用挂载来做。开发环境里,最常见的是把当前目录直接挂进去:
docker run -d --name frontend-dev -v $(pwd):/app -p 3000:3000 node:18 npm run dev这样宿主机上改代码,容器里的服务自动热更新。这种工作流能成立的核心原因是 bind mount 是实时同步的,不是启动时复制一次。
这里要特别提醒一个坑:挂载目录会遮蔽容器内原有内容。比如镜像里/app目录有预置代码,你 bind mount 宿主机空目录到/app,那容器里原本的代码就被“遮住”了。这不是数据被清空,而是挂载点指向了宿主机目录,容器镜像里的原文件还在,但你访问不到。想要保留镜像里的内容,要么启动时做一次拷贝,要么把挂载点改成子目录。
4.4 容器间共享时的权限与安全策略
数据共享最让人头疼的问题就是权限。最好从一开始就规划好 UID/GID 策略,不然等到容器都跑起来才发现互相读写不了,再逐个docker exec chown会把人逼疯。
我的建议是:在创建容器的阶段就固定 UID。例如运行 Nginx 和 PHP-FPM 两个容器共享/app目录,两个镜像内部的运行用户 UID 很可能不一样(Nginx 是 nginx UID 101,PHP-FPM 是 www-data UID 33),处理不好就是 PHP 写入的文件 Nginx 读不了,Nginx 创建的缓存 PHP 删不掉。
方案一是改容器镜像里的用户 ID,让它们一致;方案二是在宿主机上把共享目录设成 ACL,允许多个 UID 读写。方案一更彻底,我用的是 Dockerfile 里显式指定:
FROM php:8.2-fpm RUN usermod -u 1000 www-data && groupmod -g 1000 www-data然后用这个镜像跑容器,宿主机目录属主就设成 UID 1000,两个容器加宿主机三方都能读写。这套思路可以延伸到任意需要跨容器共享的组合里。
5. 数据卷避坑指南:权限、孤儿卷、挂载遮蔽与性能
说实话,数据卷本身不难,真正让人头秃的都是边缘情况。我把这几年实战中遇到的高频问题和对应的排查思路整理成速查表,遇到问题直接对照着查。
5.1 高频问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
容器启动报Permission denied | 卷或宿主目录属主与容器内用户不匹配 | 用具名卷;或 chown 目录属主为容器内 UID |
| 容器里看到的是空目录,原本镜像里的文件不见了 | 挂载点遮蔽了镜像内目录 | 不要挂整个目录,改挂子目录;或挂载后拷贝 |
| 删容器之后发现磁盘没释放 | 匿名卷变成孤儿卷 | docker volume ls -f dangling=true找出孤儿卷,确认无用后删除 |
| 挂载的配置文件没生效 | 挂载路径写错,或者 Docker 把文件当目录建了 | 检查路径,确保宿主机文件存在,不要带尾斜杠 |
| 容器重启后数据丢失 | 挂载的是容器层路径,没绑卷 | 检查docker inspect中的 Mounts 列表 |
| 多个共享卷的容器同时写文件时数据错乱 | 并发写无锁机制 | 改架构,只让一个容器写,其他容器只读;或改用分布式存储 |
5.2 孤儿卷和匿名卷的处理
长期使用 Docker 的人,机器上或多或少会积累一些无名无姓的卷。它们是容器删除时没带-v参数导致残留的匿名卷,不占内存,但占磁盘。查看方式:
docker volume ls -f dangling=true清理前一定确认卷里没有需要的数据,我见过有人直接docker volume prune把生产环境的备份卷全干掉的惨案。安全做法是:先docker volume inspect看卷挂载点,再到对应目录里确认内容,最后再删。
另外有一个提法是 Docker 新版本在docker rm的时候有个行为差异:直接docker rm container不会删除容器使用的匿名卷,而docker rm -v container会连带删除匿名卷。强烈建议不要养成带-v删容器的习惯,万一某个看起来像匿名卷的卷其实有其他容器还在用,或者里面存了你想保留的数据,那删掉就真没了。
5.3 挂载遮蔽问题的正确解法
前面提过挂载遮蔽,这里给一个完整的例子加深印象。假设镜像里/var/www/html已经放了应用代码,你想通过 bind mount 把宿主机新代码放进去运行,但又不想丢镜像里的默认页面。这时候不能直接挂目录,而是先启动容器,再手动引入新代码,比如:
docker run -d --name myapp -p 8080:80 myapp-image:latest docker cp ./new-code/. myapp:/var/www/html/或者直接在镜像构建时就规划好:镜像里把代码放在/opt/app,启动容器时把/opt/app挂到宿主机目录,不要挂/var/www/html这种镜像默认根目录。
如果确实需要启动时从镜像初始化数据,可以加一个 entrypoint 脚本,在容器启动时判断挂载目录是否为空,为空就把镜像内目录内容拷贝过去,再启动主进程。这是很多官方镜像(比如某些 MySQL 变种)的做法,也是最稳妥的“镜像自带种子数据 + 挂载持久化”方案。
5.4 性能优化:别把数据卷当普通磁盘用
数据卷的读写性能理论上接近宿主机磁盘,但实际使用中很多细节会影响最终表现。如果你的应用对 IOPS 敏感,比如数据库、消息队列,需要注意几点:
- 避免在 macOS/Windows 上把整个项目目录 bind mount 进容器。这类系统的文件共享机制(gRPC-FUSE 或 virtiofs)本身有性能损耗,尤其是大量小文件读写时,和 Linux 原生环境差距明显。开发场景图方便用 bind mount 没毛病,生产环境请直接用 volume 或直接在 Linux 服务器上跑。
- 数据库数据卷最好放在 SSD 盘上。这似乎是一句废话,但很多人迁移 Docker 时图省事把
/var/lib/docker放在了机械盘分区,MySQL 跑起来慢得令人发指,排查半天才发现是磁盘问题。 - 谨慎使用
:delegated挂载选项(macOS 下)。这个选项可以显著提升 bind mount 的写入性能,代价是容器内的写入不会立刻同步到宿主机。如果用在数据库上,断电场景下数据丢失风险会变大。写代码的热更新项目可以用,数据库类容器千万别碰。 - 文件描述符数量。容器内跑高并发应用时,挂载目录里的文件句柄占用受宿主机
fs.file-max和容器 ulimit 限制。超过限制会报Too many open files,这时候不是数据卷的问题,是系统参数要调。
6. 用 Docker Compose 管理数据卷:生产级配置解析
到这一步,单纯的docker run已经不够看了。实际项目大多用 Docker Compose 来编排多容器应用,数据卷的定义和管理方式跟命令行有差异,这里单独拎出来讲。
6.1 Compose 文件中定义数据卷
一个典型的带数据卷的 Compose 文件结构:
version: "3.8" services: mysql: image: mysql:8.0 container_name: compose-mysql ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: MyStrongPass123 MYSQL_DATABASE: appdb volumes: - mysql_data:/var/lib/mysql - ./mysql/conf:/etc/mysql/conf.d:ro networks: - backend app: image: myapp:latest ports: - "8080:8080" volumes: - ./app/code:/app:ro - shared_uploads:/app/uploads depends_on: - mysql networks: - backend volumes: mysql_data: name: app-mysql-data shared_uploads: driver: local networks: backend: driver: bridge注意这里的层级关系:volumes:顶层定义的是具名卷,services.<name>.volumes配置的是挂载关系。我习惯给顶层卷显式指定name,这样docker volume ls看到的是app-mysql-data而不是默认的compose_mysql_data,迁移和备份时更容易匹配环境。
6.2 通过 Compose 实现数据初始化
Compose 里还有个常用技巧:利用挂载 + entrypoint 做数据初始化。比如你有一个初始化 SQL 脚本,希望 MySQL 首次启动时自动导入:
mkdir -p ./mysql/init cat > ./mysql/init/init.sql <<EOF CREATE DATABASE IF NOT EXISTS bizdb; USE bizdb; CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name VARCHAR(64)); INSERT INTO users VALUES (1, 'admin'); EOF然后 Compose 里把./mysql/init挂载到/docker-entrypoint-initdb.d:
services: mysql: image: mysql:8.0 volumes: - mysql_data:/var/lib/mysql - ./mysql/init:/docker-entrypoint-initdb.d:ro这个目录很特殊:MySQL 官方镜像在首次初始化数据目录时,会自动按文件名顺序执行里面的.sql、.sh等文件。注意是只有在数据目录为空时才执行,如果mysql_data卷里已经有数据,重新挂载 init 脚本是不会再跑的。这个机制非常适合做“一键拉起新环境”。
6.3 Compose 数据卷备份脚本化
有了 Compose 之后,备份也可以脚本化。我一般在项目仓库里放一个backup.sh,内容类似:
#!/bin/bash set -euo pipefail BACKUP_DIR=/data/backups STAMP=$(date +%Y%m%d-%H%M%S) VOLUMES="app-mysql-data app-shared-uploads" for vol in $VOLUMES; do docker run --rm \ -v ${vol}:/source \ -v ${BACKUP_DIR}:/backup \ ubuntu:22.04 \ tar czf /backup/${vol}-${STAMP}.tar.gz -C /source . done echo "Backup completed: ${BACKUP_DIR}"用cron每天凌晨跑一次,配合find $BACKUP_DIR -mtime +7 -delete做保留周期管理,一套最简单的备份机制就落地了。别看它简陋,很多小团队的生产环境就是这么撑过来的,关键在“有”而不在“全”。
7. 几个我踩过的坑,写出来给你避雷
最后分享一些常规文档里找不到的经验,都是我交了“学费”才明白的。
第一个坑:宿主机目录权限被容器改掉。如果你 bind mount 某个目录进容器,容器内进程以 root 跑,写入的文件属主就是 root。就算你宿主机上平时用普通用户开发,一旦容器写入过这个目录,之后你在宿主机上想删改这些文件就经常碰到权限拒绝。血的教训是:能用具名卷就别用 bind mount,必须用 bind mount 时尽量在容器里指定非 root 用户。
第二个坑:数据库容器千万别用 bind mount 挂宿主机普通目录。数据库对文件属主、权限、目录结构非常敏感。宿主机目录的 SELinux 上下文、属主、挂载选项稍微不对,MySQL 启动时很可能直接初始化失败或者启动一半崩溃。用 Docker 管理的具名卷,Docker 会在创建时把目录属主和权限一次性设置成容器需要的状态,省掉大半麻烦。我在热词列表里看到大量“docker安装mysql失败”的搜索,估计八成都是栽在这类权限问题上。
第三个坑:docker run -v自动创建目录的规则很迷惑。如果宿主机路径不存在,Docker 会以 root 身份自动创建一个目录。这个目录权限默认 755,如果容器内用户是非 root,写入照样失败。更骚的是,如果宿主机路径写错成“文件路径带了个斜杠”,Docker 会创建一个目录而不是报错,让你排查半天。
第四个坑:卷的备份要停服务吗?很多人备份 MySQL 数据卷之前纠结要不要先停容器。如果你用的是 tar 直接打包,没有数据库层的在线备份机制,不停服务打包出来的数据可能处于不一致状态。我的建议是:MySQL 这类数据库,要么用官方备份工具(mysqldump、备份工具)做逻辑备份,要么干脆停容器做冷备。前者对数据一致性没风险,后者简单粗暴但需要停机窗口。直接 tar 正在运行中的 MySQL 数据目录,我吃过亏,不推荐。
第五个坑:不要轻易执行docker system prune -a --volumes。这条命令会清掉所有未使用的卷。听起来很干净,但它不区分“没用”和“我还有用但没挂载到运行中容器”。有一次我在清理磁盘时顺手执行了,结果把一个离线备份用的卷删了,那份数据彻底找不回来。此后我的默认操作是docker system prune不带--volumes,卷手动逐个检查,绝不批量删除。
在我自己的日常操作习惯里,现在几乎已经形成了一套固定模板:凡是数据库或有状态服务,一律使用具名卷;凡是开发环境热更新代码,一律 bind mount;凡是临时容器,能加--rm就加;凡是删容器,默认不带-v。这套模板本质上就是在反复踩坑之后沉淀下来的条件反射。
数据卷管理这东西,一开始觉得是 Docker 的一个小功能,用到后来你会发现,容器的越界能力全靠它兜底。哪怕你对 Docker 的其他部分还不太熟,只要把这个模块玩明白,已经能应付绝大多数业务场景了。