6442 字
32 分钟
Docker 数据卷与备份恢复:整站迁移实战
2026-11-15

容器是可以随时删掉重建的——镜像拉个新的、docker compose up -d 一下就回来了。但容器里的数据不行:博客的文章、数据库的记录、面板的配置、上传的文件,这些东西一旦随着容器或者卷一起没了,没有任何「重新拉取」的办法。上一篇 《如何在Linux部署Docker&Compose》 讲了怎么把服务跑起来,这篇专门解决后顾之忧:数据到底存在哪、怎么备份、怎么恢复、出事了怎么把整站搬到另一台机器上。

先把一句话刻在脑子里:容器可删,数据不行。 所有备份方案的设计,都是围绕着把「数据」和「容器」分开管理展开的。

三种数据存法对比#

Docker 里给容器存数据一共三种方式,先看全貌:

存法数据实际位置谁管理备份难度典型场景
命名卷(volume)宿主机 /var/lib/docker/volumes/ 下Docker 全权管理中(要用容器辅助打包)数据库数据、应用持久数据
绑定挂载(bind mount)宿主机任意指定目录你自己低(就是普通文件,直接打包)配置文件、网站静态文件、代码目录
tmpfs宿主机内存Docker无法备份(重启即没)临时数据、敏感信息中转

再补两个容易混淆的点:

  • 容器可写层(不挂任何东西、直接往容器里写文件)不算一种「存法」:容器一删就全没,而且性能差。任何想留住的数据不许只写在容器可写层里。
  • 命名卷和绑定挂载的区别不在性能(Linux 上两者性能基本一致),而在管理方式:命名卷是 Docker 托管的「仓库」,绑定挂载是你自己当家做主的「自家柜子」。

再顺带说清两个容易踩的概念:

  • 匿名卷:有些镜像在 Dockerfile 里写了 VOLUME 指令,或者你 run 时只写了 -v /data 没给卷名,Docker 会自动生成一个名字是随机长字符串的匿名卷来接住数据。它能防止数据落进可写层,但名字不可读、没人记得住,时间一长 docker volume ls 里攒一堆不知道是谁的卷,备份时也容易漏掉。重要数据一律用显式命名的卷,匿名卷只配接无所谓的数据。用 docker volume ls -f dangling=true 可以列出所有没有容器在用的卷,逐个确认后再清理。
  • 先看清家底再谈备份:动手备份前,先用 docker system df -v 看一眼每个卷的实际大小和引用情况——哪个卷在用、占了多少空间、有没有早就该删的僵尸卷,这个命令一屏全交代清楚。备份空间要按卷的实际体积预留,别等打包到一半才发现目标磁盘不够。

选型口诀:

  • 数据库、应用内部数据 → 命名卷,交给 Docker 管,省心且跨平台行为一致;
  • 配置文件、要经常打开看/改的东西 → 绑定挂载,宿主机上直接能编辑;
  • 临时的、丢了无所谓的 → tmpfs 或者干脆写可写层。

命名卷操作全套#

基本命令#

Terminal window
# 创建一个命名卷
docker volume create mydata
# 列出所有卷
docker volume ls
# 看某个卷的详情(挂载点、创建时间、被哪些容器用)
docker volume inspect mydata
# 删除一个卷(必须先停掉并删除使用它的容器)
docker volume rm mydata
# 清理所有没有容器在用的卷(危险,见下方警告)
docker volume prune
WARNING

docker volume prune 和 docker system prune 一样是「一键后悔药没有」类命令:它会删掉所有当前没有容器引用的卷——包括你只是暂时把容器删了、打算一会儿重建后继续用的卷。执行前务必确认里面没有要留的数据。日常清理宁可手动 docker volume rm 一个个来。

卷在磁盘上的真实位置#

命名卷的数据实际存放在宿主机:

/var/lib/docker/volumes/<卷名>/_data/

比如卷 mydata 的内容就在 /var/lib/docker/volumes/mydata/_data/ 里。知道这个位置是为了排障时能看一眼里面有什么,但不建议直接去改这个目录:

  1. Docker 假定这个目录只有它自己在管,你绕过它直接增删文件,权限和属主很容易被写歪,容器里再访问就报错;
  2. 容器运行中直接改,应用层面的缓存和锁完全不知情,数据一致性没保证;
  3. 有些存储驱动下这个路径并不长这样(比如用了远程卷插件),按路径操作的代码不可移植。

正确姿势永远是:通过容器去读写卷——下面的备份和恢复都是这么做的。

Compose 里怎么声明#

services:
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=example
volumes:
- db_data:/var/lib/postgresql/data # 短语法:卷名:容器内路径
volumes:
db_data: # 顶层 volumes 段:声明本项目用到的命名卷

注意一个关键细节:Compose 创建的卷名会带项目名前缀。假设项目名(目录名)是 myapp,你在文件里写的 db_data,真实卷名是 myapp_db_data。用 docker volume ls 看到带前缀的卷别慌,那不是多出来的野卷子。排障一节会讲这个前缀带来的经典问题和解法。

只读挂载和绑定挂载的写法也顺带记住:

volumes:
- db_data:/var/lib/postgresql/data # 可读写(默认)
- ./config:/etc/myapp/config:ro # 绑定挂载 + 只读

绑定挂载的注意事项#

绑定挂载把宿主机目录直接「投影」进容器,方便是方便,坑也最集中在这一块。

坑一:宿主机路径不存在时#

docker run -v /host/path:/data ... 时如果 /host/path 不存在,Docker 会自动以 root 身份创建一个空目录。你以为挂的是自己准备好的目录,其实挂了个空的,应用写进去的数据落在一个 root 属主的陌生目录里。Compose 的短语法同样如此。用长语法可以明确禁止这种行为:

volumes:
- type: bind
source: ./config
target: /etc/myapp/config
read_only: true
bind:
create_host_path: false # 路径不存在就直接报错,而不是偷偷新建

坑二:属主与容器内 uid 对不上(最经典)#

容器里的进程用什么 uid 运行,取决于镜像,常见情况:

  • 官方 postgres 镜像里 postgres 用户是 uid 999;
  • nginx 镜像 worker 进程是 uid 101;
  • 很多镜像默认直接用 root(uid 0)。

绑定挂载不会做任何 uid 转换:宿主机上属主是 uid 1000 的文件,进了容器就还是 uid 1000。于是经典报错就来了——容器内进程是 uid 999,宿主机目录属主是 1000 且权限 755,进程没有写权限,直接 Permission denied 起不来。

排查和处理:

Terminal window
# 查宿主机上自己的 uid/gid
id
# 查目录现在的属主(显示数字 uid/gid)
ls -n /srv/myapp/data
# 把目录属主改成容器内进程需要的 uid(例:999)
sudo chown -R 999:999 /srv/myapp/data

两个原则:

  1. 不要无脑 chmod 777。777 能「解决」权限问题,也能让任何进程随便写你的数据,只是把报错换成了安全隐患。
  2. 能选的话优先用命名卷:Docker 创建卷时会把卷目录的属主初始化为镜像里对应路径的属主,上面这个坑基本不会出现——这也是数据库类服务推荐命名卷的原因之一。

坑三:只读挂载 :ro#

还有个写法上的细节:Compose 里绑定挂载用 ./config 这种相对路径时,它是相对于 Compose 文件所在目录解析的,不是你执行命令时所在的目录。项目目录搬家时绑定挂载会跟着 Compose 文件走,这通常正是你想要的;但如果在别的目录用 -f 指定 Compose 文件,就要意识到相对路径的锚点也随之变了,排障时先确认锚点再怀疑文件丢了。

配置文件类挂载建议一律加 :ro:

- ./nginx.conf:/etc/nginx/nginx.conf:ro

容器被攻破时写不进配置,也改不了你的站点文件,属于零成本的安全加固。注意 :ro 只约束容器这一侧,宿主机上你自己照样能改。

命名卷备份:标准做法#

命名卷的数据不在你的项目目录里,没法直接 tar 项目目录了事。标准做法是起一个临时容器,把卷挂进去,在容器里把卷内容打包到挂进来的备份目录。听起来绕,其实一条命令:

Terminal window
# 在项目目录下先建好备份存放点
mkdir -p ~/backups
# 备份卷 myapp_db_data(换成你自己的真实卷名)
docker run --rm \
-v myapp_db_data:/data:ro \
-v ~/backups:/backup \
alpine \
tar czf /backup/db_data-$(date +%F).tar.gz -C /data .

逐参数讲解,这条命令每一部分都有讲究:

  • docker run --rm:临时容器,用完自动删除,不留垃圾;
  • -v myapp_db_data:/data:ro:把要备份的卷挂到临时容器的 /data,加 :ro 只读——备份是读操作,只读挂载保证备份过程绝不可能误伤原数据;
  • -v ~/backups:/backup:把宿主机的备份目录挂到容器的 /backup,打包产物写这里,容器删了产物还在宿主机上;
  • alpine:用最小的工具镜像即可,它自带 tar。手边有 ubuntu 镜像也一样;
  • tar czf /backup/db_data-$(date +%F).tar.gz -C /data .:在容器里执行打包。-C /data . 的意思是「切到 /data 目录,把当前目录的内容打包」——打包的是内容而不是 data 这个目录本身,恢复时就不会多出一层目录(排障一节会讲这个经典失误);文件名里用 $(date +%F) 自动带上日期,如 db_data-2026-11-15.tar.gz,一眼能看出是哪天的备份。

另一种等价写法是 --volumes-from,直接借用正在运行的容器所挂的全部卷:

Terminal window
docker run --rm \
--volumes-from myapp-db-1 \
-v ~/backups:/backup \
alpine \
tar czf /backup/all-volumes-$(date +%F).tar.gz -C /data .

--volumes-from <容器名> 会把那个容器挂的卷按相同路径挂到临时容器上。适合一个容器挂了好几个卷、想一次全打走的情况;缺点是路径要以原容器的挂载点为准,写命令前先 docker inspect 确认清楚。新手建议优先用第一种 -v 卷名:/data 的写法,路径自己说了算,不容易错。

WARNING

备份文件生成后立刻验一下,别等恢复时才发现包是坏的:

Terminal window
# 能列出内容且不报错,包基本完好
tar tzf ~/backups/db_data-2026-11-15.tar.gz | head
# 顺手看一眼大小,0 字节或者小得离谱肯定有问题
ls -lh ~/backups/

命名卷恢复:标准做法#

恢复是备份的逆操作:先建一个空卷,再用临时容器把备份包解进卷里。

Terminal window
# 1. 创建目标卷(如果卷已存在且里面是旧数据,先确认再删掉重建)
docker volume create myapp_db_data
# 2. 把备份包解进卷
docker run --rm \
-v myapp_db_data:/data \
-v ~/backups:/backup \
alpine \
tar xzf /backup/db_data-2026-11-15.tar.gz -C /data
# 3. 验证:列一下卷里的内容,确认文件都在
docker run --rm -v myapp_db_data:/data alpine ls -la /data

要点:

  • 因为备份时打的是「/data 里面的内容」,恢复时直接解到 /data 就严丝合缝,不需要也不应该加 --strip-components;
  • 解包在容器里以 root 执行,文件属主会按包里记录的 uid/gid 还原(tar 默认保留属主),这正是我们想要的——原数据是 uid 999 的,回来还是 999;
  • 恢复前如果目标卷不是空的,旧文件会和解出来的文件混在一起。稳妥做法是恢复到新建的空卷,确认无误后再让服务切换过去。

验证不能只停在「文件在」。最后一定要把真正的服务挂上这个卷启动起来,看它能不能正常读数据:

Terminal window
docker compose up -d
docker compose logs -f db # 看数据库是否正常完成初始化/恢复

服务能起来、应用能读到历史数据,恢复才算完成。

最后强调一句容易被跳过的话:没实际恢复过的备份,等于没有备份。 备份文件能生成、能列表,不代表真出事时能还原——属主丢了、路径多一层、包在传输中损坏,都是恢复现场才会暴露的问题。建议每套备份流程第一次跑通后,立刻在一个临时卷上做一次完整的恢复演练,把命令和坑都验证一遍;之后每次大改备份脚本,也重复这个动作。演练花十分钟,换的是出事那天不用现学。

数据库备份的一致性:别直接拷文件#

到这里有个必须单独讲清的问题:对正在运行的数据库,直接打包它的数据卷,得到的备份可能是一个坏档。

原因不难理解:数据库运行时,数据一部分在磁盘文件里,一部分还在内存和预写日志(WAL)里,文件之间的一致性是靠数据库自己的机制在崩溃恢复时拼回去的。你在任意一个瞬间把文件整包拷走,拷到的是「半截子状态」——运气好,数据库启动时能靠日志回放自愈;运气不好,就是损坏、丢数据或者干脆起不来。文件级热拷贝对普通文件型应用(博客附件、上传目录)问题不大,对数据库是赌博。

正确做法有两条路:

路线一:逻辑导出(推荐,热备)#

用数据库自带的导出工具,得到的是一致的逻辑备份,服务不用停:

Terminal window
# PostgreSQL:导出为 SQL 文本
docker compose exec db pg_dump -U postgres myapp > ~/backups/mydb-$(date +%F).sql
# MySQL:--single-transaction 保证导出期间一致性且不锁表
docker compose exec db mysqldump -u root -p密码 --single-transaction myapp > ~/backups/mydb-$(date +%F).sql

恢复时反过来喂回去:

Terminal window
# PostgreSQL
cat ~/backups/mydb-2026-11-15.sql | docker compose exec -T db psql -U postgres myapp
# MySQL
cat ~/backups/mydb-2026-11-15.sql | docker compose exec -T db mysql -u root -p密码 myapp

逻辑导出的额外好处:跨版本、跨机器恢复都兼容(卷文件直接拷则不行,老版本的数据文件新版本可能拒绝加载)。

路线二:停机冷备(简单粗暴但可靠)#

能接受短暂停机的个人服务,直接停掉数据库再打包数据卷,文件处于静止状态,一致性天然成立:

Terminal window
docker compose stop db
docker run --rm -v myapp_db_data:/data:ro -v ~/backups:/backup alpine \
tar czf /backup/db_data-cold-$(date +%F).tar.gz -C /data .
docker compose start db

在 Compose 项目里固化成一条命令#

把逻辑导出写进一个小脚本放项目里(比如 backup.sh),手动和定时任务都能调:

./backup.sh
#!/bin/bash
set -e
mkdir -p ./backups
docker compose exec -T db pg_dump -U postgres myapp > ./backups/mydb-$(date +%F).sql
echo "备份完成:./backups/mydb-$(date +%F).sql"

注意 docker compose exec 在脚本里要加 -T(不分配伪终端),否则在 cron 这类非交互环境里会报错。

WARNING

还有一个隐蔽坑:pg_dump 出来的备份只包含数据库内容,不包含数据库的用户/角色等全局对象。要全量兜底可以用 pg_dumpall(整实例导出)。个人项目多数只用默认用户,知道有这回事即可,真迁移整实例时再换 pg_dumpall。

整站迁移实战:把服务搬到另一台服务器#

前面几节的技能拼起来,就是完整的整站迁移。场景很常见:VPS 到期换商家、机器配置不够要升级、或者单纯想把几个服务合并到一台。下面以一个典型的 Compose 项目(应用 + PostgreSQL,项目名 myapp,目录 /opt/myapp)为例,全程走一遍。

整体思路一句话:搬三样东西——项目目录(含配置)、数据卷、镜像清单,数据库单独逻辑导出最稳。

第一步(旧机):停服并打包项目目录#

先停服务,保证打包期间文件不再变化:

Terminal window
cd /opt/myapp
docker compose down

注意是 down 不是 down -v——-v 会把命名卷一起删掉,迁移现场千万别顺手加上。

打包整个项目目录(compose 文件、.env、绑定挂载的配置和数据都在里面):

Terminal window
tar czf ~/myapp-dir-$(date +%F).tar.gz -C /opt myapp
WARNING

.env 文件里通常有数据库密码、密钥等敏感信息,它会被打进这个包。传输和存放时按敏感文件对待:只走加密通道(scp/rsync over SSH 本身是加密的)、别丢到任何公共网盘或公开仓库。

第二步(旧机):打包数据卷 + 导出数据库#

命名卷逐个按前面的标准做法打包:

Terminal window
mkdir -p ~/backups
docker run --rm -v myapp_db_data:/data:ro -v ~/backups:/backup alpine \
tar czf /backup/myapp_db_data-$(date +%F).tar.gz -C /data .

数据库额外再做一份逻辑导出,双保险(卷包用于整卷还原,SQL 用于卷还原出问题时兜底)。逻辑导出要在停机前做,所以实际操作顺序是:先 pg_dump → 再 docker compose down → 再打目录包和卷包:

Terminal window
docker compose exec -T db pg_dump -U postgres myapp > ~/backups/mydb-$(date +%F).sql

记录镜像清单,到了新机好核对拉的版本对不对:

Terminal window
docker compose images > ~/backups/images-list.txt
cat ~/backups/images-list.txt

第三步:传输到新机#

Terminal window
# rsync 断点续传,大文件更稳
rsync -avz -e ssh ~/myapp-dir-*.tar.gz ~/backups/ root@新机IP:~/migrate/
# 或者简单粗暴 scp
scp ~/myapp-dir-*.tar.gz ~/backups/*.tar.gz ~/backups/*.sql root@新机IP:~/migrate/

第四步(新机):装 Docker 并放回目录#

新机先按 《如何在Linux部署Docker&Compose》 装好 Docker,然后:

Terminal window
mkdir -p ~/migrate && cd ~/migrate # 假设安装包都传到了这里
# 放回项目目录
tar xzf myapp-dir-*.tar.gz -C /opt
cd /opt/myapp
ls -la # 确认 docker-compose.yml 和 .env 都在

第五步(新机):逐卷恢复#

在启动服务之前先建卷并灌数据。注意:卷名必须和 Compose 将要使用的完全一致(带项目名前缀 myapp_db_data):

Terminal window
docker volume create myapp_db_data
docker run --rm -v myapp_db_data:/data -v ~/migrate:/backup alpine \
tar xzf /backup/myapp_db_data-*.tar.gz -C /data
docker run --rm -v myapp_db_data:/data alpine ls -la /data # 验一眼

然后告诉 Compose「这个卷是现成的,别自己新建」——在顶层 volumes 段给它标 external: true:

volumes:
db_data:
external: true
name: myapp_db_data

为什么需要这一步、忘了会怎样,排障一节细说。

第六步(新机):启动并按清单验证#

Terminal window
cd /opt/myapp
docker compose up -d
docker compose ps # 所有服务都是 Up
docker compose logs -f # 看一遍启动日志,有没有连不上数据库之类的报错

最后逐项过验证清单,三查缺一不可:

查什么怎么查通过标准
数据登录应用后台/查数据库记录数文章、用户、历史数据都在且是最新
证书浏览器打开站点、点小锁看证书HTTPS 正常、证书未过期(反代在宿主机或另一台时记得同步改指向)
DNSdig +short 你的域名 或直接访问解析已切到新机 IP 且已生效

DNS 切换有个时间差:建议迁移前一天先把域名 TTL 调小(比如 300 秒),切换当天生效快、回滚也快。全部验证通过、观察一两天没问题之后,再去注销旧机——旧机就是你最后的回滚方案,别急着删。

回滚预案也要在动手前想好:如果新机验证不过,最快的回滚就是把 DNS 解析切回旧机 IP(旧机在整个过程中只是停了服务、数据原封未动,切回后 docker compose up -d 即可恢复)。所以迁移的正确心态是:旧机全程只读冻结、新机随便折腾,任何一步不对劲都能全身而退。等新机稳定运行几天、且在新机上已经产生了一份新备份之后,旧机才算完成历史使命。

定时自动备份:脚本 + 轮转 + cron#

手动备份靠不住,迟早会忘。把前面的命令固化成脚本,cron 每天跑,并只保留最近 7 份:

/opt/scripts/backup-volumes.sh
#!/bin/bash
# 备份指定命名卷,只保留最近 7 份
set -e
BACKUP_DIR=/root/backups
DATE=$(date +%F)
# 要备份的卷,空格分隔,按自己机器上的真实卷名填
VOLUMES="myapp_db_data myapp_uploads"
mkdir -p "$BACKUP_DIR"
for vol in $VOLUMES; do
docker run --rm \
-v ${vol}:/data:ro \
-v ${BACKUP_DIR}:/backup \
alpine \
tar czf /backup/${vol}-${DATE}.tar.gz -C /data .
echo "已备份 $vol"
done
# 轮转:删掉 7 天以前的备份包
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete
echo "备份全部完成"
Terminal window
chmod +x /opt/scripts/backup-volumes.sh
# 先手动跑一次,确认能生成包、包能打开
/opt/scripts/backup-volumes.sh
ls -lh /root/backups/

挂到 cron,每天凌晨 3 点执行、日志留痕:

0 3 * * * /opt/scripts/backup-volumes.sh >> /var/log/volume-backup.log 2>&1

脚本里用 -mtime +7 -delete 做轮转有个前提:备份文件名各不相同(带日期)且目录里只放这类备份包,别把别的重要文件堆在这个目录里。

还有个运维习惯:每周顺手瞄一眼 /var/log/volume-backup.log 和备份目录里最新包的日期。定时备份最怕的不是报错,而是悄悄停摆——cron 被误删、磁盘满了、卷名改了,脚本天天失败但没人看日志,等真要恢复时才发现最新备份是三个月前的。确认「昨晚的包在、大小正常」,这个动作只要十秒。

WARNING

备份只存本机等于没完全备份。 磁盘损坏、机器被注销、误操作 rm,备份和原数据会一起没。脚本跑完后再加一步异地传输,哪怕只是传到另一台 VPS 或者对象存储:

Terminal window
# 脚本末尾追加:同步到另一台机器
rsync -avz -e ssh /root/backups/ root@备份机IP:/root/backups-主站/

原则是至少做到「两台机器、两份拷贝」,重要数据再考虑第三份到对象存储。

排障#

恢复后服务起不来,日志报 Permission denied#

九成是属主问题:备份打包或解包时如果没在容器里用 tar 而是手动 cp,或者解包工具没保留属主,文件会变成 root 或错误 uid 所有,应用进程(比如 uid 999 的 postgres)读不了自己的数据目录。

Terminal window
# 对比原卷属主:看数据目录里的文件是谁的
docker run --rm -v myapp_db_data:/data alpine ls -ln /data
# 典型修复:把属主改回应用需要的 uid(以 PostgreSQL 的 999 为例)
docker run --rm -v myapp_db_data:/data alpine chown -R 999:999 /data

根治办法是全程用本文的 tar 流程(tar 默认保留 uid/gid),别混用别的方法搬数据。

卷名对不上:Compose 新建了一个空卷,数据「消失」了#

现象:迁移后服务能起来,但里面是全新空数据;docker volume ls 里躺着两个长得像的卷。

原因就是前面说的项目名前缀:你在旧机手工建的卷叫 db_data,而 Compose 按项目名拼出来的真实卷名是 myapp_db_data,两者不是一个卷。Compose 找不到自己要的卷,就默默新建了一个空的。

解法二选一:

# 解法一(推荐):顶层 volumes 段声明用现成的卷
volumes:
db_data:
external: true
name: myapp_db_data # 写真实卷名
# 解法二:建卷时就直接用 Compose 期望的名字
# 即一开始就 docker volume create myapp_db_data 并往里恢复,
# Compose 发现同名卷已存在会直接使用

排查时先 docker volume ls 对一眼名字,能省下大量瞎猜的时间。

备份包解开多了一层目录#

现象:恢复完进卷一看,数据不在 /data 根下,而是躺在 /data/data/ 里,应用自然读不到。

原因:打包时写的是 tar czf ... /data(打了目录本身),包里所有路径都带个 data/ 前缀;解到 /data 就成了套娃。本文备份命令用的是 -C /data .(打目录里的内容),从源头避开这个问题。

如果手上的旧包已经是带目录层的,恢复时用 --strip-components 削掉一层补救:

Terminal window
docker run --rm -v myapp_db_data:/data -v ~/backups:/backup alpine \
tar xzf /backup/旧包.tar.gz -C /data --strip-components 1

打包到一半失败:no space left on device#

备份本质是「读一份、再写一份」,目标磁盘至少要留出比源数据更大的空闲空间(压缩后通常会小不少,但日志、媒体这类已经压缩过的数据几乎压不动,要按原体积预估)。先用 docker system df -v 确认卷的实际大小,再 df -h 看备份目录所在磁盘的余量。空间实在紧张时,可以让 tar 直接通过管道交给 gzip 以上的压缩,或者把备份目录挂到另一块盘上,但别把备份打到和源卷同一块快满的盘上自欺欺人。

卷删不掉:volume is in use#

docker volume rm 报在用,先查是谁在用:

Terminal window
docker ps -a --filter volume=myapp_db_data

停掉的容器也算「在用」,把对应容器 docker rm 后再删卷。确认里面数据真的不要了再动手。

小结#

  • 数据存法三选一:数据库和应用数据用命名卷、配置和常看常改的文件用绑定挂载、临时的用 tmpfs;永远别把重要数据只留在容器可写层;
  • 命名卷备份恢复的标准姿势是临时容器 + tar:备份时卷只读挂载、打包目录内容(-C /data .)、文件名带日期,恢复时解到新建的空卷并启动服务验证;
  • 数据库不要赌文件级热拷贝:能逻辑导出就 pg_dump/mysqldump,或者停机做冷备;
  • 整站迁移搬三样:项目目录(含 .env)、数据卷、镜像清单,上线后过数据、证书、DNS 三查,旧机留几天再注销;
  • 备份要自动化:脚本 + cron + 只留最近 7 份轮转,并且至少再往另一台机器同步一份。

把这套流程跑顺,你对这台机器上所有容器都可以做到一句话:随便删,随便搬,反正数据丢不了。

Docker 数据卷与备份恢复:整站迁移实战
https://blog.anexns.com/posts/docker-volumes-backup/
作者
Anexns
发布于
2026-11-15
许可协议
CC BY-NC-SA 4.0