用 Docker 跑应用的人都会遇到同一个灵魂拷问:容器删了,数据还在吗? 答案取决于你把数据放在了哪里。这篇文章用一次典型的博客类应用部署复盘 bind mount 持久化和"目录即资产"的备份思路。

先记住一句话:容器是一次性的
镜像(image)是"模板",容器(container)是"运行中的实例"。容器可以被随意删除、重建、升级——这是容器最大的优点,也是最大的陷阱:如果你把数据写进了容器的可写层,容器一删,数据跟着蒸发。
# 反面教材:数据默认落在容器里
docker run -d --name blog-db mariadb:10.11
docker rm -f blog-db
# → 数据库文件全部没了
所以凡是"删了会心疼"的东西——数据库文件、上传的图片、用户配置——都必须住在容器外面。
把宿主目录挂进去:bind mount
让数据住在外面的标准做法是挂载宿主目录:
services:
db:
image: mariadb:10.11
container_name: blog-db
volumes:
- ./mysql:/var/lib/mysql # 宿主机 ./mysql 目录 = 数据库目录
web:
image: wordpress:php8.1-apache # 换成任意 Web 应用都同理
container_name: blog-web
volumes:
- ./html:/var/www/html # 宿主机 ./html 目录 = 整站程序 + 上传文件
之后容器随便 docker rm -f、docker compose up -d 重建,数据原样回来。以上面的目录布局为例,整个站点的全部身家,就是项目目录这一个文件夹。
和 named volume 怎么选? named volume(
-v blog_data:/var/lib/mysql)由 Docker 管理、更"官方",但文件藏在/var/lib/docker/volumes/深处,想看想备份都要绕一层。bind mount 直接把目录摊在明面上,ls、tar、rsync想怎么操作就怎么操作——小规模自托管,bind mount 更好用。
目录即资产:备份一条 tar 搞定
代码在镜像里(随时能重新拉),配置和数据都在目录里——所以备份整站 = 备份一个目录,不需要进容器东翻西找,也不怕漏文件。
# 稳妥起见先停容器,保证数据一致
cd ~/blog && docker compose stop
# 整个项目目录打成一个包
cd ~ && tar -czf blog_$(date +%Y%m%d).tar.gz blog
# 拷走(异地/对象存储),再拉起来
docker compose up -d

数据库在跑的时候直接打包文件有风险(可能备份到写到一半的数据),所以流程里先 stop 再打包。对评论、上传不活跃的小站点,每晚停几十秒做个快照完全无感;大流量站点可以改用 mysqldump 热备数据库 + 增量同步文件。
恢复演练:比备份更重要
备份做了不演练等于没有。真到灾难那天才第一次解包,大概率手忙脚乱。花十分钟完整走一遍:
# 新机器(或清空的目录)上
cd ~ && tar -xzf blog_20260101.tar.gz
cd blog && docker compose up -d
# 验证三件事
curl -sI https://blog.example.com/ | head -1 # 1. 站点起来了
ls html/wp-config.php # 2. 配置文件回来了
docker exec blog-db ls /var/lib/mysql | head # 3. 数据库文件齐了
能完整跑通一次"删库跑路演练",你才算真正睡了个安稳觉。
几个实战小提醒
- 目录权限:容器内进程常以非 root 用户运行(如 www-data),挂载目录属主要对,否则容器启动就报 Permission denied。
chown -R www-data:www-data是常见解药。 - 重启策略:
restart: unless-stopped一定要写,机器重启后容器自动拉起,不用半夜爬起来敲命令。 - 容器日志限容:compose 里给日志加
max-size: 10m/max-file: 3,不然日志文件能把磁盘吃满。 - 升级不丢数据:换镜像版本升级时,先备份目录,再
docker compose pull && up -d,出问题一条命令回滚到旧镜像——数据目录从头到尾没动过。
容器负责"跑",目录负责"存",两者分开,这套架构就拥有了最朴素的抗风险能力:镜像丢了能重拉,目录丢了能还原,最坏的情况也只是重新部署,而不是重新开始。