ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

容器文件备份全攻略:docker cp、bind mount 与数据卷迁移实战

容器文件备份全攻略:docker cp、bind mount 与数据卷迁移实战 1. 备份方向怎么选一次性复制、实时同步还是整卷打包容器内文件与本地之间的双向复制备份几乎是每个用 Docker 的人都绕不过去的事情。我最初做这件事的时候靠的是 docker cp 一条命令后来才发现文件复制这件事分三个完全不同的层级选错层级轻则多干活重则丢数据。先说一个我自己的翻车现场。刚接触 Docker 那阵子我把一个 Nginx 容器部署好改完 /etc/nginx/nginx.conf 里的配置心满意足地去睡觉。第二天容器状态变成 Exited我顺手 docker rm 再 run 一个全新的然后整个人傻眼——配置全没了日志也没了改动全部归零。那会儿我才意识到容器里的文件默认活在一张“临时草稿纸”上容器一删草稿纸也就被扔了。后来我把“容器与本地双向复制备份”拆成三个层级来理解才彻底摆脱这种低级事故。第一个层级是一次性复制对应 docker cp。它适合“临时把某个文件捞出来”或者“临时塞一个文件进去”是一把顺手的小工具但你要靠它做定时备份、做持续同步它根本撑不住。第二个层级是持续双向同步对应 bind mount挂载目录。把宿主机的一个目录“借”给容器用容器里写文件宿主机立刻能看到宿主机放文件容器里也马上能用。它适合日志目录、配置文件、上传文件这类长期存活的场景。第三个层级是整卷打包和镜像迁移。数据卷备份用临时容器打 tar 包容器迁移用 docker export 与 docker save这是到“完整备份方案”这一步才用上的重武器。1.1 一次性复制适合哪些场景为什么不适合拿来当备份docker cp 的核心作用是在运行中或停止状态的容器与宿主机之间拷贝文件。它就像一个文件管理器一眼看得到的问题用起来很舒服。典型的场景包括从容器里导出某个应用的配置文件把宿主机上准备好的初始化 SQL 脚本灌进 MySQL 容器容器里跑崩了赶紧把日志目录捞出来分析。这些都属于“低频、小批量、人工操作”的需求docker cp 一条命令就能搞定。而且它有个非常实用的特点容器即使处于 Exited 状态只要没有 docker rm文件系统还在docker cp 依旧能拷。这意味着容器崩溃后你还有机会抢救数据。但拿它当备份工具就有问题了。第一docker cp 没有自动触发机制每次都要手动敲命令人总有忘记的时候第二如果容器里是大量小文件比如日志目录里有几十万个文件docker cp 在 Linux 宿主机上虽然可用但速度远不如先打包再拷贝第三在线复制数据库的数据目录很可能复制出一个不一致的坏备份这个问题后面专门说。所以我的经验是docker cp 定位成“救急工具”不要把它当成“备份方案”。1.2 bind mount 适合什么场景它的核心优势是什么bind mount 的思维方式和 docker cp 完全不同。docker cp 是“把文件搬过去”而 bind mount 是“让文件住在宿主机里”。你在启动容器时指定一个目录映射容器内的路径和宿主机的路径背后是同一个目录甚至同一个 inode。容器内写文件宿主机立刻看到宿主机写文件容器里也立刻看到不需要任何同步动作。这个方案特别适合三类内容一是日志容器里的 Nginx、Java 应用日志直接写到宿主机挂载目录方便宿主机上的采集工具去做日志分析二是配置把 nginx.conf、application.yml 放在宿主机目录改完挂载进去容器可以随时丢弃重建配置不丢三是业务产生的文件比如用户上传的图片、生成的报表这些数据绝对不能跟容器生命周期绑定。bind mount 最大的优势是让容器真正变得“可丢弃”。你对容器本身不再有感情因为所有有状态的文件都在宿主机上容器随便 rm 随便 rebuild数据毫发无损。这正是容器化运维里常说的一句话容器是无状态的有状态的是数据。1.3 数据卷打包与镜像迁移完整备份的最终手段到了完整备份这个层级就要用到 Docker 的 named volume命名卷和镜像导出工具了。数据卷的好处是它由 Docker 统一管理路径不用自己操心性能也比 bind mount 在桌面平台上的表现稳定得多。备份数据卷的标准做法是跑一个临时容器把卷挂进去再挂一个宿主机备份目录在临时容器里用 tar 打包。容器级的备份还有两个容易混淆的命令docker export 和 docker save。docker export 导出的是运行中容器的整个文件系统docker save 导出的是镜像层和元数据。很多新手分不清等会儿我在后面的章节里专门做一个对比表。你只需要记住要备份“跑着的业务实例”用 export 更贴近业务要迁移“镜像本身”用 save。这两条命令不是互相替代的关系。2. docker cp 双向复制实操命令、参数与文件属主处理2.1 两个方向的完整命令与停止容器复制技巧docker cp 的命令格式其实只有两条方向不同而已。从容器复制到宿主机docker cp 容器ID:/app/config.yml ./backup/config.yml从宿主机复制到容器docker cp ./init.sql 容器ID:/docker-entrypoint-initdb.d/init.sql如果复制的是目录语法同样支持。比如把容器里的整个日志目录拉到当前目录docker cp nginx:/var/log/nginx ./nginx_logs执行完你会发现在当前目录下生成了 nginx_logs 这个目录。这里有个比较容易踩的小坑如果目标目录已经存在docker cp 会把你复制的目录作为一个子目录合并进去而不是把内容平铺复制。举个例子宿主机上已经有一个 backup 目录你执行 docker cp nginx:/var/log/nginx backup最后得到的不是 backup 下散落一堆日志文件而是 backup/nginx/ 下才是日志文件。想要避免这种“多一层目录”的情况可以给源路径加上 /. 后缀比如docker cp nginx:/var/log/nginx/. ./nginx_logs这个写法在 Linux 系命令里很常见含义是把目录里的内容而不是目录本身拷贝过去。另外提醒一句docker cp 对停止状态的容器同样有效。所以容器挂了别急着删先用 docker ps -a 找到容器ID只要没被删除文件都还能抢救出来。2.2 两个容易被忽略的参数-a 和 -Ldocker cp 支持几个参数其中最重要也最容易被忽略的是 -a 和 -L。-a 表示 archive mode即归档模式它会在复制过程中尽量保留文件的 uid/gid、权限位和时间戳。做备份操作时这个参数几乎是必加的。比如你要把容器里的配置目录完整备份到宿主机一旦丢失属主信息恢复回去之后服务可能因为权限不对而起不来。我一直习惯写成 docker cp -a 容器ID:/etc/nginx ./nginx_conf_backup。-L 表示 follow symlink也就是跟随符号链接。docker cp 默认会复制符号链接本身而不是它指向的实际文件。容器里很多配置目录都会有软链接比如 /var/log 下面的某些日志文件链接到控制台输出。如果你备份时希望拿到链接指向的真实文件就加 -L。写全了是这样docker cp -aL nginx:/etc/nginx ./nginx_conf_backup这两个参数可以一起用备份配置和日志的时候很顺手。2.3 文件拷出来之后的权限和属主问题在 Linux 环境下docker cp 拷出来的文件经常带着容器里的 uid/gid。容器内 root 用户创建的文件到宿主机上通常是 root:root普通用户想直接编辑有时候会遇到 Permission denied。解决办法最粗暴的是 chown但这不适合自动化场景。更优雅的做法是运行容器的时候就用 --user 指定当前用户或者在宿主机上把备份目录的属主和容器用户对齐。容器里如果是非 root 用户拷出来的文件在宿主机上往往会显示成一个很陌生的数字比如 nginx 容器里的用户可能是 uid 101。如果你在宿主机上用 ls -l 看到文件属主显示为 101不要慌那不是文件坏了而是容器和宿主机维护的用户体系不一样。此时可以手动把这串 uid 映射到一个熟悉的用户名或者就用 chown 让它归当前用户。写代码的时候我不会在文章里劝你直接 chmod 777生产环境千万慎用。3. 用 bind mount 实现容器目录与本地目录的双向实时同步3.1 -v 与 --mount 的写法区别为什么我更推荐 --mount启动容器时挂载目录最传统的写法是 -vdocker run -d --name nginx \ -v /home/user/nginx_conf:/etc/nginx \ -p 8080:80 nginx:latest-v 写起来短但有两个问题一是在 Windows 路径下容易出现歧义比如 C:\Users 这种路径要转义很容易写错二是 -v 默认“隐藏”了挂载类型语义不清晰。相比之下我更推荐 --mount 写法docker run -d --name nginx \ --mount typebind,src/home/user/nginx_conf,dst/etc/nginx \ -p 8080:80 nginx:latest--mount 的参数全部写清楚type 明确是 bindsrc 是宿主机路径dst 是容器内路径。还可以追加 readonly 等选项。改配置文件的时候宿主机上可以直接编辑容器内立即生效。如果想防止容器内进程反向修改宿主机文件可以加一个 ro--mount typebind,src/home/user/nginx_conf,dst/etc/nginx,ro加完容器内对这个目录就只有读权限了。这种写法在 docker compose 文件里也有对应形式语义完全一致。3.2 双向同步背后其实是“同一个视图”不是“复制”很多人会误以为 bind mount 是一种同步机制是 Docker 在后台不停地把文件从一个目录复制到另一个目录。实际上完全不是。挂载成功后容器内路径与宿主机路径指向的是同一个文件系统位置。你在容器里写入文件相当于直接在宿主机那个目录里写入文件宿主机删除一个文件容器里那个文件也立即消失。不存在延迟更不存在同步方向。理解这一点你才能解释一些怪现象。比如你在宿主机上往挂载目录里放进一个文件但容器内的服务不认账多半是服务进程有缓存或者监听了错误路径而不是挂载本身出了问题。再比如你把宿主机一个空目录挂载到了 MySQL 的数据目录 /var/lib/mysql 下面MySQL 启动时发现目录为空可能直接拒绝启动或者自动初始化一个全新数据目录。这种时候你不是要“等同步”而是要先把空目录初始化为正确的数据目录结构或者换一个思路让容器先启动初始化完数据后再把目录挂进去。3.3 配合 rsync 实现定时增量同步bind mount 本身已经是双向实时共享了那还需要 rsync 干什么最常见的需求是“备份到另一个目录”或者“把数据定期归档”。因为 bind mount 里的目录和容器共享数据但你不想把备份直接堆在业务目录里而是希望每天归档到独立目录这时 rsync 就派上用场了。前提是容器里装了 rsync。很多精简镜像没有这个命令Nginx 系镜像默认没有Alpine 系需要 apk add rsyncDebian/Ubuntu 系需要 apt install rsync。装好之后假设你的宿主机备份目录已经通过 --mount 挂载到了容器的 /backup 目录docker exec myapp rsync -av --delete /app/data/ /backup/appdata/这条命令的含义是容器内把 /app/data 目录内容增量同步到 /backup/appdata。--delete 很关键它让源目录里已删除的文件在备份目录中同步删除从而保证备份和源是一致的。反向同步只需要把两个路径调换docker exec myapp rsync -av --delete /backup/import/ /app/import/定时执行就用 crontab比如每 15 分钟同步一次*/15 * * * * docker exec myapp rsync -av --delete /app/data/ /backup/appdata/ /var/log/rsync_container.log 21需要提醒一句rsync 不会帮你做事务一致性数据库文件仍然不建议用这种方式在线备份。4. 数据卷完整备份打包命令、定时脚本与容器迁移4.1 一条命令备份整个数据卷并快速恢复如果你用的是 named volume命名卷备份起来比 bind mount 更规范。标准命令长这样docker run --rm \ -v mydata:/volume \ -v $(pwd):/backup \ alpine tar czf /backup/mydata_backup.tar.gz -C /volume .我来拆一下这条命令。--rm 表示临时容器用完即删。-v mydata:/volume 把你要备份的命名卷挂到容器内的 /volume。-v $(pwd):/backup 把宿主机当前目录挂到 /backup这是用来放备份文件的。alpine 是小体积 Linux 镜像自带 tar打包性能足够。tar czf 指创建 gzip 压缩包-C /volume 的意思是先进入 /volume 目录再打包最后的 . 表示打包当前目录下的全部内容。这样生成的压缩包内不含无意义的顶层目录恢复时更干净。恢复数据卷时先创建命名卷再解包docker volume create mydata docker run --rm \ -v mydata:/volume \ -v $(pwd):/backup \ alpine tar xzf /backup/mydata_backup.tar.gz -C /volume恢复之前一定要先停掉使用这个数据卷的容器。如果容器还在写数据恢复就可能在解压过程中发生文件冲突。我自己的习惯是先在容器启动前恢复备份或者先把容器缩容停掉恢复完成后再启动这样最稳妥。4.2 可以直接抄的定时备份脚本dab 到生产环境光手工备份不够。给出一个我一直在用的容器级定时备份脚本模版。它按容器名字备份应用数据目录保留最近 7 天的备份超期自动清理。#!/bin/bash BACKUP_DIR/opt/docker_backups KEEP_DAYS7 CONTAINERS(nginx mysql-app file-server) STAMP$(date %F_%H-%M-%S) mkdir -p $BACKUP_DIR/$STAMP for c in ${CONTAINERS[]}; do if ! docker ps --format {{.Names}} | grep -qx $c; then echo [WARN] container $c is not running, skip continue fi echo [INFO] backuping $c ... docker cp -a $c:/app/data $BACKUP_DIR/$STAMP/${c}_appdata done # 清理超过 KEEP_DAYS 天的备份目录 find $BACKUP_DIR -maxdepth 1 -type d -mtime $KEEP_DAYS -exec rm -rf {} \; echo [DONE] backup finished at $(date %F %T)脚本里 docker cp 备份的是容器里的具体业务目录。如果目录结构不一样你需要把 /app/data 改成自己的实际路径。然后把脚本放到 crontab30 2 * * * /opt/scripts/docker_backup.sh /var/log/docker_backup.log 21我刻意用了日志重定向这样每次备份结果都有据可查出问题能快速定位是哪一天哪一步失败的。这个脚本不会记录备份内容是否可恢复所以还要定期做恢复演练。备份文件打不开从来不是故障当天发现的而是等你真要恢复的时候才发现的那才是最痛苦的。4.3 容器迁移场景下 export 与 save 怎么选很多人在做容器迁移时搞不清 docker export 和 docker save。我整理过一张对比表基本能解决选择困难。对比项docker exportdocker save操作对象运行中的容器文件系统镜像输出内容容器当前的全部文件不含镜像层历史镜像层与元数据恢复方式docker import 转成镜像docker load 回镜像典型用途快速迁移运行实例、容器级数据快照离线分发镜像、镜像迁移一致性运行中可执行但不保证数据一致与运行状态无关迁移的是镜像本身举两个实际场景。场景一你要把一台机器上正在运行的整个业务系统搬到另一台机器连容器里的运行痕迹和临时数据都要带过去用 docker export 更贴近业务现状。执行docker export myapp -o myapp_container.tar到新机器上执行 docker import然后基于导入的镜像重新 run 一个容器。场景二你要在另一个私有仓库离线部署一套镜像用 docker save 更合适因为 save 保留了镜像的分层结构和启动配置load 回来之后能完整还原出镜像本来的样子。docker save nginx:latest -o nginx_image.tar这里多提一句镜像安全尽量只在受信任环境里使用别人导出的镜像文件save/load 和 pull 一样都应当确认来源可靠不要什么包都往生产环境灌。5. 常见问题与排查技巧实录5.1 容器里新建的文件宿主机看不见这个问题十有八九是挂载路径没对上。排查第一步是用 docker inspect 看容器的挂载信息docker inspect 容器ID --format {{json .Mounts}}输出里会清楚列出 Source宿主机路径、Destination容器内路径和 RW 读写状态。确认路径之后进入容器实际看一下docker exec -it 容器ID sh ls -la /app/data df -h /app/data注意如果容器内 df 显示的路径和挂载的目标路径不一致说明应用实际写到了别的目录而不是挂载目录。很多服务会把临时文件写到 /tmp或者写到工作目录下日志框架也可能有自己的路径配置。这时不是挂载有问题而是配置路径有问题。5.2 挂载目录没有写权限Permission denied 反复出现Linux 下这个坑非常常见尤其是宿主机普通用户创建的目录挂载给容器后容器内应用以非 root 用户运行写入时报 Permission denied。排查思路分三层。第一层确认目录属主和容器用户 UID 是否匹配。可以在宿主机执行 id 查看自己的 UID在容器内执行 id 查看容器的用户 UID两个数字一致就不会有权限冲突。第二层如果不一致可以用 chown 调整宿主机目录属主。第三层SELinux 环境下挂载目录还需要考虑安全上下文此时 --mount 定义里要追加 :Z 或 :z 标签。还有一种情况是容器必须要以非 root 用户启动但宿主机上的目录需要当前用户可写。你可以让容器以临时用户参数启动docker run --user $(id -u):$(id -g) ...这样容器内进程的 UID 与宿主机当前用户一致同一目录的读写权限自然就打通了。5.3 docker cp 复制目录后多了一层目录这是个细节问题很多人踩过。docker cp 容器:/etc/nginx ./backup 之后backup 目录下不是散开的配置文件而是 backup/nginx/ 这个子目录。原因是 docker cp 默认复制整个目录本身。想要平铺内容给源路径加 /.docker cp 容器ID:/etc/nginx/. ./backup或者先在宿主机建好目标目录再复制mkdir -p ./nginx_conf docker cp 容器ID:/etc/nginx/. ./nginx_conf这个技巧同样适用于恢复场景。往容器里回灌配置的时候想直接把本地目录内容展开到容器目标目录记得源路径写 local_conf/.。5.4 Windows 桌面版挂载目录性能差到怀疑人生如果你的宿主机是 Windows 和 macOS通过 Docker Desktop 使用 bind mount性能比 Linux 原生挂载差很多。原因是桌面版的 Docker 跑在虚拟机里文件共享要经过一层虚拟化文件服务大量小文件的场景下延迟明显。这里我给出几条实测下来比较靠谱的调整思路。第一尽量把频繁读写的数据目录放到 named volume而不是 bind mount因为命名卷由 Docker 管理走的是虚拟机的本地文件系统。第二如果必须用 bind mount适合放少量配置文件不适合做日志和数据库目录。第三Windows 用户走 WSL2 后端时建议把项目文件放进 WSL2 自身的 Linux 文件系统内再通过 Docker 容器挂载比直接跨 Windows 路径操作快非常多。顺便提一句如果你的 Docker Desktop 启动就报虚拟化不支持的提示通常是 BIOS 里的虚拟化开关没打开或者宿主机没有开启 WSL2/Hyper-V这个要先解决否则后面一切容器操作都跑不起来。5.5 数据库文件不能直接 docker cp必须用自带备份工具这是整个话题里最值钱的一条经验。在线运行中的 MySQL、PostgreSQL 容器直接 docker cp /var/lib/mysql 目录出来产生的备份大概率是坏的。原理很简单数据库为了保证一致性会把数据写在数据文件、重做日志、二进制日志等很多地方复制瞬间这些文件可能处于互相不对齐的状态。你拷出来的备份可能缺日志或者为了性能数据库还在批量写缓存文件根本不是一个完整快照。正确做法是使用数据库自己的备份工具。MySQL 用 mysqldumpdocker exec mysql-app mysqldump -uroot -p testdb testdb_dump.sqlPostgreSQL 用 pg_dump 或 pg_basebackupSQLite 用 sqlite3 .backup。如果你确实需要文件级备份就先停容器再打包文件这样能保证一致性但代价是业务中断。千万别偷懒省这一步。6. 实际操作中我认为最值钱的三条经验第一容器里的一切都默认不持久。我会把配置、日志、上传文件全部用 --mount 或 named volume 挂到宿主机侧让容器随时可以被 rm、可以被 rebuild。这套习惯养成之后再没慌过容器挂掉。也因为这个原则我从来不在容器内部手工改配置所有改动都从宿主机侧修改再让容器重载。第二备份没有验证等于没备份。我吃过一次亏定时任务“看似”每天成功跑完日志也正常结果某天要恢复时发现备份目录里好几个包都是损坏的。现在我每周会抽一台测试机器从最新备份做一次完整恢复演练确认备份包能解压、服务能启动、数据能查出来。这个过程不复杂但对备份方案可信度极其重要。第三先看数据量再选方案。备份前我会先 docker exec 进容器执行 du -sh 和 df -h 看看实际数据量大小再决定是走 docker cp、tar 打包、rsync 增量还是直接挂载目录让宿主机工具去备份。大而全的整包备份不是不行而是没必要把所有场景都做成重方案。日常小体量用 cp持续同步用 bind mount完整备份用卷打包各层各司其职这才是容器文件备份的正确打开方式。
返回列表