Docker容器部署完重启宿主机数据丢了怎么配持久化卷
Docker容器重启宿主机后数据丢了,问题通常不在“重启”本身
线上经常能遇到这种情况:Docker容器跑得好好的,应用也能写文件,宿主机一重启,容器重新拉起来之后发现数据没了。MySQL库空了,Redis持久化文件不见了,上传目录没了,甚至Nginx配置也回到了镜像里的默认状态。
这个问题表面看像是“Docker不可靠”,实际多数是容器数据没有做持久化。容器里的文件系统默认是跟容器生命周期绑定的,镜像提供初始文件,容器运行后产生的变更写在容器自己的可写层里。容器被删除、重新创建,或者部署脚本里用了 docker run 重新起一个新容器,原来的可写层就不再被新容器使用。
宿主机重启本身不会必然删除容器数据,但很多人的启动方式是这样的:开机脚本执行 docker run,或者面板、CI/CD、运维脚本检测容器不存在就新建,结果新容器没有挂载原来的数据目录,看起来就像“重启宿主机导致数据丢失”。
先分清容器里的哪些目录必须持久化
不是所有目录都要挂出来。实际使用中发现,很多故障是因为挂载目录选错了。比如MySQL只挂了 /backup,没挂 /var/lib/mysql;Nginx只挂了 /usr/share/nginx/html,没挂配置目录;应用只挂了代码目录,没挂上传文件目录。
常见服务的数据目录可以这样看:
MySQL / MariaDB:/var/lib/mysql
PostgreSQL:/var/lib/postgresql/data
Redis:/data,前提是配置了 appendonly yes 或 RDB 持久化
MongoDB:/data/db
Nginx静态站点:/usr/share/nginx/html,配置一般是 /etc/nginx/conf.d
Elasticsearch:/usr/share/elasticsearch/data
应用上传文件:看业务配置,常见是 /app/uploads、/data/uploads、/var/www/uploads
这里补充一点,数据库类容器不要只想着把“备份目录”挂出来。备份目录是兜底,真正在线运行的数据目录必须持久化。否则容器重建后数据库目录还是新的,备份文件在不在都不影响当前库已经空了这个事实。
Docker持久化主要用两种方式:bind mount 和 named volume
Docker里常用的持久化方式有两类:bind mount 和 named volume。两者都能解决数据丢失问题,但适用场景不一样。
bind mount 是把宿主机上的某个目录直接挂进容器,比如把 /data/mysql 挂到容器的 /var/lib/mysql。优点是直观,宿主机上直接能看到文件,备份、迁移、排查都方便。缺点是权限、SELinux、目录结构要自己管。
named volume 是 Docker 管理的卷,比如 mysql_data:/var/lib/mysql。优点是交给 Docker 管理,路径不容易写错,适合标准服务。缺点是新手不一定知道数据实际放在 /var/lib/docker/volumes 下面,排查时要多查一步。
线上更偏向哪种?如果是数据库、对象存储、业务上传目录,通常会用 bind mount,路径统一放到 /data 或 /data/docker 下,方便纳入备份。如果是临时环境、测试环境、标准中间件,named volume 用起来更省事。
用 bind mount 部署 MySQL,重启宿主机后数据还在
以 MySQL 为例,宿主机先准备目录:
mkdir -p /data/docker/mysql/data /data/docker/mysql/conf
然后启动容器时挂载数据目录:
docker run -d --name mysql8 --restart=always -p 3306:3306 -e MYSQL_ROOT_PASSWORD='StrongPassword123' -v /data/docker/mysql/data:/var/lib/mysql -v /data/docker/mysql/conf:/etc/mysql/conf.d mysql:8.0
这条命令里真正关键的是 -v /data/docker/mysql/data:/var/lib/mysql。容器里的 /var/lib/mysql 写入的数据,实际落到了宿主机 /data/docker/mysql/data。宿主机重启后,Docker按 --restart=always 自动拉起容器,容器还是挂同一个宿主机目录,数据自然还在。
如果之前已经跑了一个没挂载数据卷的 MySQL 容器,不要直接删。先确认数据还在原容器里,可以用 docker cp 把数据导出来,或者通过 mysqldump 做逻辑备份。生产环境更建议 mysqldump 或 xtrabackup,不建议直接复制运行中的数据库文件。
例如先导出:
docker exec mysql_old mysqldump -uroot -p --all-databases > /root/all.sql
再按持久化方式启动新容器,导入:
cat /root/all.sql | docker exec -i mysql8 mysql -uroot -p
用 named volume 部署,别把卷名写成随机的
named volume 的写法更短:
docker volume create mysql_data
docker run -d --name mysql8 --restart=always -p 3306:3306 -e MYSQL_ROOT_PASSWORD='StrongPassword123' -v mysql_data:/var/lib/mysql mysql:8.0
查看卷位置:
docker volume inspect mysql_data
这里有个实际使用中很容易踩的坑:用 docker run 每次都不指定固定卷名,或者用某些面板创建容器时自动生成卷名。容器被重建后,新容器挂的是新卷,旧数据还在旧卷里,但业务看不到。这个时候不是数据真的没了,而是“挂错卷了”。
可以通过下面命令看有哪些卷:
docker volume ls
再 inspect 可疑卷,看 Mountpoint,进去确认文件是否还在。数据库目录里如果能看到 ibdata1、binlog、auto.cnf、数据库子目录,说明旧数据大概率还在。
docker-compose 里要把 volumes 写死,不要只依赖容器名
现在很多项目都用 docker-compose 部署。compose 文件里如果没有写 volumes,容器重建后一样会丢运行数据。正确写法类似这样:
services:
mysql:
image: mysql:8.0
container_name: mysql8
restart: always
environment:
MYSQL_ROOT_PASSWORD: StrongPassword123
ports:
- "3306:3306"
volumes:
- /data/docker/mysql/data:/var/lib/mysql
- /data/docker/mysql/conf:/etc/mysql/conf.d
如果想用 named volume,也可以这样:
services:
mysql:
image: mysql:8.0
restart: always
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
多说一句,docker compose down 默认不会删除 named volume,但如果加了 -v,就会把卷一起删掉。很多测试环境数据丢失就是 docker compose down -v 造成的。生产环境操作 compose 时,看到 -v 要特别敏感。
权限问题别忽略,挂了卷不代表服务一定能写
bind mount 最常见的问题是权限。宿主机目录创建好了,容器启动也没报太明显的错,但服务内部写不进去,最后表现成数据没落盘、启动失败、反复重启。
排查方式很直接:
docker logs 容器名
如果看到 Permission denied、Read-only file system、Can’t create/write to file 这类报错,就要看目录权限。
比如某些镜像不是用 root 用户运行,而是用 uid 1000、999、1001 这类用户。宿主机目录归 root,权限又比较收,就会写失败。可以用下面方式看容器内进程用户:
docker exec -it 容器名 id
然后在宿主机上调整目录属主:
chown -R 999:999 /data/docker/mysql/data
具体 uid/gid 要看镜像,不要照抄。MySQL、PostgreSQL、Redis、Elasticsearch 的官方镜像用户都不完全一样。
如果宿主机开启了 SELinux,CentOS、Rocky Linux、AlmaLinux 上还可能遇到挂载目录权限正常但容器仍然不能写。可以临时验证:
getenforce
如果是 Enforcing,bind mount 可以加 :Z 或 :z,例如:
-v /data/docker/mysql/data:/var/lib/mysql:Z
生产环境不建议为了省事直接 setenforce 0 后就不管了,至少要知道问题来自 SELinux label。
宿主机重启后容器没起来,不是持久化问题,是 restart policy
还有一种情况,数据没丢,但容器没有自动启动。用户访问服务发现空白页、连不上数据库,就以为数据没了。
Docker容器是否随宿主机重启自动拉起,取决于 restart policy。常用的是 --restart=always 或 --restart=unless-stopped。
docker run 启动时加:
--restart=always
已有容器可以改:
docker update --restart=always 容器名
查看容器状态:
docker ps -a
如果容器是 Exited,再看日志:
docker logs --tail=100 容器名
这里要分清:restart policy 解决的是“重启后服务自动恢复”,volume 解决的是“容器重建后数据仍然在”。两个都要配,少一个都会出线上故障。
别把重要数据放在容器层,也别把宿主机系统盘写爆
容器默认可写层通常在 /var/lib/docker 下面。如果应用大量写日志、缓存、上传文件,又没有挂载到外部目录,宿主机系统盘很快会被写满。系统盘满了之后,Docker、SSH、数据库都有可能异常,严重时宿主机都起不稳。
实际部署时更建议把业务数据目录规划到独立数据盘,例如:
/data/docker/mysql/data
/data/docker/redis/data
/data/docker/nginx/conf
/data/docker/nginx/html
/data/app/uploads
再配合备份任务,把 /data 下需要持久化的目录纳入快照、rsync、对象存储或异地备份。容器镜像可以重新拉,配置和数据不能只存在容器层。
如果业务本身跑在云服务器上,选机器时也要看磁盘类型和网络场景。比如数据库和业务服务混部,磁盘IO、CPU稳定性比单纯看带宽更重要;如果是游戏、Web业务还容易被打,DDoS防护也要提前考虑。如果你也在找这种能直接承载 Docker 业务的云服务器,可以看看129云,像宁波高防-B型有 4C 8G、70G U.2、100Gbps防御,比较适合有防护需求的中小业务;泉州电信-B型是 4C 8G、60G SSD、100Gbps单机防御,电信线路场景用得比较多。需要确认线路和机器配置时,可以直接问客服热线 400-9177118。
迁移已有容器数据时,别边跑边硬拷数据库目录
很多人发现没做持久化后,第一反应是 docker cp /var/lib/mysql。这个动作在数据库运行中风险很高,因为文件可能正在写,拷出来的数据不一定一致。轻则启动报错,重则表损坏。
更稳的方式是逻辑备份:
MySQL 用 mysqldump 或 mysqlpump,数据量大时用 xtrabackup。
PostgreSQL 用 pg_dump、pg_dumpall,或者做物理备份时配合 pg_basebackup。
Redis 如果只是缓存可以不迁;如果有持久数据,确认 appendonly.aof、dump.rdb 是否完整,再停服务复制。
文件上传目录、静态资源目录可以用 rsync,但迁移期间最好停写,或者先全量同步,再短暂停机做增量同步。
一个常见处理流程是:先备份旧容器数据,停旧容器,用挂载卷方式启动新容器,导入数据,验证业务,再删除旧容器。不要一上来 docker rm,尤其不要带 -v。
排查“数据丢了”时可以按这个路径看
先看容器是否还是原来的容器:
docker ps -a --no-trunc
看创建时间、容器ID、镜像、启动命令。如果容器是新建的,旧容器可能已经没了,但旧 volume 或宿主机目录可能还在。
再看挂载关系:
docker inspect 容器名
重点看 Mounts 字段。Source 是宿主机路径或 volume 路径,Destination 是容器内路径。Destination 如果不是应用真正的数据目录,挂了也没意义。
继续查 Docker volume:
docker volume ls
docker volume inspect 卷名
如果是 compose 部署,还要看当前项目名。Docker Compose 会给 volume 加项目前缀,例如 project_mysql_data。换目录执行 compose、换 project name,都可能导致创建出另一个新卷。
最后看宿主机目录本身:
du -sh /data/docker/mysql/data
ls -lah /data/docker/mysql/data
如果目录大小明显不对,比如数据库跑了几个月目录只有几KB,说明之前根本没写到这个目录。这个时候要回头找旧容器、旧卷、备份,而不是继续在新目录里排查。
日志也要单独处理,不然早晚把盘打满
Docker默认 json-file 日志如果不限制大小,长期运行的容器会把 /var/lib/docker/containers 下的日志文件打得很大。宿主机重启后数据没丢,但磁盘满了,服务一样起不来。
可以在 /etc/docker/daemon.json 里配置日志轮转:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
然后重启 Docker:
systemctl restart docker
注意这个配置通常只对新创建的容器生效,已有容器可能需要重建。生产环境重启 Docker 前要确认业务容器 restart policy 正常,避免 Docker 重启后服务没回来。
生产环境建议把卷路径、备份、启动方式写进部署规范
容器持久化不是只写一个 -v 参数。真正线上稳定运行,要把目录、权限、备份、恢复、重启策略都固定下来。
比较常见的目录规划是:
/data/docker/服务名/data:核心数据
/data/docker/服务名/conf:配置文件
/data/docker/服务名/logs:应用日志,注意配合轮转
/data/backup/服务名:备份文件
部署脚本里不要随机创建卷,不要每次 docker run 一个新容器却不接原数据目录。compose 文件要进 Git,生产环境变更前看 diff。备份任务要定期做恢复演练,只看到备份文件生成不代表一定能恢复。
如果是海外下载、镜像分发、节点同步这类 Docker 场景,对大陆访问质量要求不高但带宽需求很大,可以看129云的德国大宽带,10Gbps峰值带宽更适合跑大流量分发类任务;如果面向国内用户,还是要按线路和延迟选,不要只盯着带宽数字。
一个容易被忽略的细节:容器删除和卷删除不是一回事
docker rm 容器名,默认只删除容器,不删除 named volume。docker rm -v 容器名,会删除容器关联的匿名卷。docker compose down 默认删除容器和网络,不删除 named volume。docker compose down -v 会删除 compose 文件里声明的 named volume。
匿名卷最容易让人混乱。比如镜像 Dockerfile 里声明了 VOLUME,但你运行容器时没指定卷名,Docker会自动创建一个匿名卷。容器重建后又生成一个新的匿名卷,旧数据就挂不到新容器里。
所以生产环境尽量不要依赖匿名卷。要么明确写宿主机路径,要么明确写 named volume 名称。
配置完成后,用一次重启测试验证
持久化卷配完不要只看容器能启动,最好做一次验证。比如 MySQL 里建一个测试库、写一条记录,确认数据目录宿主机有变化,然后重启容器、重启宿主机,再查记录是否还在。
测试命令可以很简单:
docker restart mysql8
reboot
宿主机回来后:
docker ps
docker exec -it mysql8 mysql -uroot -p
如果数据还在,Mounts 也指向预期目录,restart policy 正常,这个容器才算真正具备基础的持久化能力。
不要等线上业务跑了一个月才发现 /data 是空的,数据库一直写在容器层里。那个时候再迁移,就不是改一行 -v 参数的事了。