Docker 数据卷(Volume)详解:Dockerfile 声明、Docker Compose 挂载宿主机目录及持久化实战
前言
在使用 Docker 部署 Spring Boot、MySQL、Redis、Nginx 等服务时,经常会遇到以下问题:
- Docker 容器删除后,如何保证数据不丢失?
- Dockerfile 中的
VOLUME指令有什么作用? - Dockerfile 能否直接指定宿主机目录?
- Docker Compose 如何挂载宿主机目录?
- Bind Mount 和 Docker Volume 有什么区别?
- Dockerfile 和 Docker Compose 同时配置数据卷时,会发生什么?
这些问题都涉及 Docker 的数据持久化机制。
本文将从 Docker 数据卷的基本概念出发,结合 Dockerfile、Docker Compose 和 Spring Boot + MySQL 实战案例,详细介绍 Docker 的数据持久化与目录挂载。
一、为什么需要 Docker 数据卷?
1.1 Docker 容器的数据存储问题
Docker 容器具有轻量化、易部署、易销毁等特点。
默认情况下,应用写入容器可写层的数据与容器生命周期密切相关。
例如,启动一个 Ubuntu 容器:
bash
docker run -it --name test-ubuntu ubuntu bash
在容器内创建文件:
bash
mkdir -p /app/data
echo "Hello Docker" > /app/data/test.txt
退出并删除容器:
bash
docker rm -f test-ubuntu
然后重新创建一个容器:
bash
docker run -it --name test-ubuntu ubuntu bash
此时再查看:
bash
cat /app/data/test.txt
会发现原来的文件已经不存在。
原因:
之前的数据保存在旧容器的可写层中。删除容器后,该容器的可写层也会随之删除。
需要注意的是,单纯停止容器并不会删除容器的可写层;删除并重新创建容器才是这里数据丢失的关键。
因此,对于数据库、日志、上传文件等需要长期保存的数据,不能只依赖容器可写层。
1.2 Docker 数据卷的作用
Docker 通过数据卷和挂载机制,可以将需要持久化的数据存储在容器可写层之外。
例如:
text
宿主机:
/data/docker/logs
Docker 容器:
/app/logs
将这两个目录建立挂载关系:
text
宿主机目录 容器目录
/data/docker/logs <--> /app/logs
当容器向 /app/logs 写入文件时,数据实际上保存在宿主机的 /data/docker/logs 目录中。
即使容器被删除并重新创建,只要重新挂载相同目录,就可以继续访问原来的数据。
这就是 Docker 数据持久化的基本思想。
二、Docker 数据存储的三种方式
Docker 常见的数据存储方式有三种:
- Bind Mount(绑定挂载)
- Named Volume(命名卷)
- Anonymous Volume(匿名卷)
2.1 Bind Mount:绑定挂载
Bind Mount 是将宿主机指定文件或目录挂载到容器中。
例如:
bash
docker run -d \
--name nginx \
-v /data/nginx/html:/usr/share/nginx/html \
nginx
其中:
text
/data/nginx/html
表示宿主机目录。
text
/usr/share/nginx/html
表示容器中的目录。
二者形成映射关系。
特点:
- 可以明确指定宿主机目录。
- 宿主机和容器可以访问同一份挂载数据。
- 适合配置文件、日志、上传文件等场景。
- 需要关注宿主机路径及文件权限问题。
2.2 Named Volume:命名卷
Named Volume 是由 Docker 负责创建和管理的数据卷。
例如:
bash
docker volume create mysql_data
启动容器:
bash
docker run -d \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-v mysql_data:/var/lib/mysql \
mysql:8.4
这里:
text
mysql_data
是数据卷名称,而不是宿主机目录。
查看 Docker 数据卷:
bash
docker volume ls
查看数据卷信息:
bash
docker volume inspect mysql_data
在常见的 Linux Docker Engine 环境中,本地数据卷通常位于类似下面的路径:
text
/var/lib/docker/volumes/mysql_data/_data
但这不是固定路径,实际位置取决于 Docker 的数据根目录、卷驱动和运行环境。
特点:
- 由 Docker 管理数据卷生命周期。
- 不需要手动指定宿主机存储目录。
- 适合数据库等需要持久化的数据。
- 可以被多个容器挂载,但同时访问时需要考虑应用的数据一致性。
2.3 Anonymous Volume:匿名卷
匿名卷不需要指定数据卷名称。
例如:
bash
docker run -d \
--name nginx \
-v /usr/share/nginx/html \
nginx
Docker 会创建一个匿名卷,并将其挂载到:
text
/usr/share/nginx/html
匿名卷与命名卷都由 Docker 管理,主要区别是匿名卷没有用户显式指定的固定名称。
注意:
匿名卷并不意味着容器一删除,卷就一定立即删除。
它是否被删除,取决于容器删除方式、--rm 参数以及后续的卷清理操作。
2.4 三种方式对比
| 对比项 | Bind Mount | Named Volume | Anonymous Volume |
|---|---|---|---|
| 指定宿主机目录 | 支持 | 默认不需要 | 不需要 |
| Docker 管理卷 | 否 | 是 | 是 |
| 是否指定卷名 | 不适用 | 是 | 否 |
| 数据持久化 | 支持 | 支持 | 支持 |
| 方便复用 | 支持 | 支持 | 相对不便 |
| 常见场景 | 配置、日志、文件 | 数据库、持久化数据 | 临时或自动创建的卷 |
三、Dockerfile 中如何使用 VOLUME?
3.1 VOLUME 指令的作用
Dockerfile 中可以通过 VOLUME 指令声明数据卷挂载点。
基本语法:
dockerfile
VOLUME ["/app/data"]
也可以声明多个目录:
dockerfile
VOLUME ["/app/data", "/app/logs"]
例如,一个 Spring Boot 项目:
dockerfile
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/app.jar app.jar
VOLUME ["/app/logs"]
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
其中:
dockerfile
VOLUME ["/app/logs"]
表示声明 /app/logs 为数据卷挂载点。
当 Docker 创建容器时,如果没有显式指定该路径的其他挂载,Docker 会自动为它创建匿名卷。
3.2 Dockerfile 能否指定宿主机目录?
不能。
例如:
dockerfile
VOLUME ["/data/docker/logs:/app/logs"]
这种写法不能用于建立宿主机目录与容器目录之间的映射。
Dockerfile 中的 VOLUME 只能声明容器内部的挂载路径。
这是因为 Dockerfile 的主要职责是定义镜像,而不是定义运行环境。
假设镜像部署在不同服务器上:
text
开发环境:
/home/dev/logs
测试环境:
/data/test/logs
生产环境:
/data/prod/logs
不同服务器的宿主机目录可能完全不同。
如果在镜像中写死宿主机目录,就会降低镜像的可移植性。
因此,Docker 将镜像构建与容器运行配置进行了分离。
- Dockerfile:负责定义镜像及容器内的挂载点。
- Docker Run / Docker Compose:负责指定实际的宿主机目录或数据卷。
3.3 Dockerfile 定义 VOLUME 后如何指定宿主机目录?
首先构建镜像:
bash
docker build -t my-java-app:1.0 .
启动容器:
bash
docker run -d \
--name java-app \
-p 8080:8080 \
-v /data/docker/java/logs:/app/logs \
my-java-app:1.0
此时:
text
宿主机:
/data/docker/java/logs
容器:
/app/logs
已经建立绑定挂载关系。
需要特别注意:
即使 Dockerfile 中没有声明 VOLUME,也可以通过 docker run -v 实现目录挂载。
因此,VOLUME 不是进行宿主机目录挂载的必要条件。
四、Docker Compose 如何挂载宿主机目录?
在实际项目中,通常使用 Docker Compose 来统一管理服务、端口、网络、环境变量和数据卷。
4.1 Docker Compose volumes 基本语法
yaml
services:
app:
image: nginx
volumes:
- /data/nginx/html:/usr/share/nginx/html
挂载格式:
text
宿主机路径:容器路径:挂载模式
例如:
yaml
volumes:
- /data/app/logs:/app/logs:rw
表示允许容器读写。
yaml
volumes:
- /data/app/config:/app/config:ro
表示只读挂载。
其中:
rw:读写,默认模式。ro:只读,容器不能通过该挂载修改数据。
4.2 使用宿主机绝对路径
例如:
yaml
services:
app:
image: nginx
volumes:
- /data/docker/nginx/html:/usr/share/nginx/html
这里的宿主机目录为:
text
/data/docker/nginx/html
不受 Compose 文件位置影响。
适合需要明确指定服务器存储位置的场景。
4.3 使用宿主机相对路径
例如项目结构:
text
my-project/
├── docker-compose.yml
├── logs/
└── data/
Compose 配置:
yaml
services:
app:
image: nginx
volumes:
- ./data:/app/data
- ./logs:/app/logs
这里:
text
./data
./logs
表示相对路径。
在常见的单文件 Compose 使用场景下,相对路径以 Compose 文件所在目录为基准;使用多个 Compose 文件或显式设置项目目录时,应注意实际的路径解析基准。
例如:
text
/home/projects/my-project/docker-compose.yml
通常会被解析成:
text
/home/projects/my-project/data
/home/projects/my-project/logs
补充:
相对路径建议写成 ./data 或 ../data,以便与命名卷区分。
因为:
yaml
volumes:
- data:/app/data
这里的 data 表示命名卷,而不是当前目录下的 data 文件夹。
4.4 挂载单个配置文件
有时候并不需要挂载整个目录,只需要将某个配置文件映射到容器中。
例如:
yaml
services:
app:
image: nginx
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
这种方式非常适合:
- Nginx 配置文件。
- Spring Boot 配置文件。
- 应用自定义配置文件。
需要注意,宿主机源文件应该提前存在。
例如:
bash
touch nginx.conf
当然,正式部署时还需要填入合法配置内容。
五、Docker Compose 使用命名卷
除了宿主机目录,Compose 还支持使用 Docker 管理的命名卷。
例如部署 MySQL:
yaml
services:
mysql:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: "123456"
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
这里存在两处 volumes。
第一处:
yaml
services:
mysql:
volumes:
- mysql_data:/var/lib/mysql
表示 MySQL 容器使用 mysql_data 数据卷。
第二处:
yaml
volumes:
mysql_data:
表示声明一个由 Compose 管理的命名卷。
启动:
bash
docker compose up -d
查看:
bash
docker volume ls
默认情况下,Compose 通常会给命名卷加上项目名前缀,例如:
text
myproject_mysql_data
如果希望使用固定的数据卷名称,可以指定:
yaml
volumes:
mysql_data:
name: my_mysql_data
这样实际卷名称就是:
text
my_mysql_data
六、Dockerfile 与 Docker Compose 同时配置数据卷
这是实际开发中容易产生疑问的场景。
假设 Dockerfile 中有:
dockerfile
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/app.jar app.jar
VOLUME ["/app/logs"]
ENTRYPOINT ["java", "-jar", "app.jar"]
Docker Compose 中配置:
yaml
services:
app:
build: .
volumes:
- /data/docker/app/logs:/app/logs
那么最终采用哪种挂载?
答案是:
Compose 中显式指定的宿主机目录挂载会用于 /app/logs。
实际挂载关系:
text
/data/docker/app/logs
|
|
v
/app/logs
不会因为 Dockerfile 声明了相同路径的 VOLUME,就再自动为这个目标创建一个匿名卷。
因此,在实际项目中,可以选择:
方案一:
Dockerfile 声明 VOLUME,Compose 指定宿主机路径。
方案二:
Dockerfile 不声明 VOLUME,直接通过 Compose 定义所有挂载。
对于需要在不同环境中灵活部署的项目,第二种方案通常更加直观。
七、实战案例:Spring Boot + MySQL 数据持久化
下面通过完整案例,演示如何使用 Docker Compose 管理 Spring Boot 项目的日志与 MySQL 数据。
7.1 项目目录结构
text
springboot-demo/
├── Dockerfile
├── compose.yaml
├── target/
│ └── app.jar
└── config/
└── application.yml
宿主机数据目录规划:
text
/data/docker/
├── springboot/
│ └── logs/
└── mysql/
其中:
text
/data/docker/springboot/logs
用于保存 Spring Boot 应用日志。
text
/data/docker/mysql
用于保存 MySQL 数据。
7.2 编写 Dockerfile
dockerfile
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
这里不需要声明 VOLUME。
日志目录的实际挂载由 Compose 决定。
7.3 编写 compose.yaml
yaml
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: springboot-app
ports:
- "8080:8080"
volumes:
# 持久化日志目录
- /data/docker/springboot/logs:/app/logs
# 外部配置文件
- ./config/application.yml:/app/config/application.yml:ro
environment:
SPRING_CONFIG_ADDITIONAL_LOCATION: file:/app/config/
depends_on:
mysql:
condition: service_healthy
restart: unless-stopped
mysql:
image: mysql:8.4
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: "123456"
MYSQL_DATABASE: demo
ports:
- "3306:3306"
volumes:
# MySQL 数据持久化
- /data/docker/mysql:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
restart: unless-stopped
说明:这里的数据库密码仅用于学习演示。正式生产环境应使用安全的密码管理方式,避免将真实密码提交到代码仓库,也应根据网络需求决定是否对宿主机开放数据库端口。
其中:
yaml
volumes:
- /data/docker/springboot/logs:/app/logs
用于持久化日志。
yaml
volumes:
- /data/docker/mysql:/var/lib/mysql
用于持久化数据库数据。
另外,应用的日志配置需要真正指向 /app/logs,例如通过 Logback 配置设置日志文件路径,否则挂载目录可能不会产生预期的日志文件。
7.4 启动服务
首先确保应用已经完成 Maven 打包,生成:
text
target/app.jar
并检查 config/application.yml 已存在。
创建宿主机数据目录:
bash
sudo mkdir -p /data/docker/springboot/logs
sudo mkdir -p /data/docker/mysql
确保目录具有适当的访问权限。
然后启动:
bash
docker compose up -d --build
查看容器:
bash
docker compose ps
查看日志:
bash
docker compose logs -f app
7.5 验证数据持久化
首先进入应用容器:
bash
docker exec -it springboot-app sh
在容器内执行:
bash
echo "Docker volume test" > /app/logs/test.log
退出容器。
然后在宿主机查看:
bash
cat /data/docker/springboot/logs/test.log
预期输出:
text
Docker volume test
说明容器和宿主机目录挂载成功。
接着执行:
bash
docker compose down
然后重新启动:
bash
docker compose up -d
再次查看:
bash
cat /data/docker/springboot/logs/test.log
如果文件仍然存在,就说明数据不依赖容器可写层。
这里的验证主要针对日志挂载。MySQL 数据持久化则建议通过实际创建数据库表、插入记录并重建容器进行验证。
八、Docker 数据卷常见问题
8.1 容器挂载目录后,原来的文件为什么不见了?
假设镜像中本来存在:
text
/app/config/application.yml
但是 Compose 配置:
yaml
volumes:
- ./config:/app/config
启动容器后发现配置文件不存在。
这是因为 Bind Mount 会遮挡容器镜像中挂载目标目录原有的内容。
如果宿主机的 ./config 是空目录,那么容器在挂载点看到的也是空目录。
解决方式:
提前将需要的文件准备到宿主机目录中。
或者只挂载需要覆盖的单个配置文件。
注意:Docker 命名卷首次挂载到镜像中的非空目录时,在默认情况下可能发生文件复制行为,不应将其与 Bind Mount 的遮挡行为完全混为一谈。
8.2 容器无法写入宿主机目录怎么办?
例如出现:
text
Permission denied
通常需要检查宿主机目录权限。
查看:
bash
ls -ld /data/docker/springboot/logs
还需要检查容器内应用进程的运行用户:
bash
docker exec springboot-app id
根据容器进程实际的 UID/GID,设置宿主机目录权限。
例如应用用户的 UID、GID 均为 1000,且该目录应归该用户管理:
bash
sudo chown -R 1000:1000 /data/docker/springboot/logs
这里的 1000:1000 仅为示例,需要根据实际情况调整。
不建议直接使用:
bash
chmod -R 777
因为这样会带来不必要的权限风险。
8.3 删除容器后,数据卷会删除吗?
需要区分不同情况。
第一种:
bash
docker compose down
默认会删除 Compose 创建的容器和网络,但不会删除命名卷和宿主机 Bind Mount 数据。
第二种:
bash
docker compose down -v
会额外删除 Compose 管理的命名卷,以及相关匿名卷。
但不会直接删除 Bind Mount 对应的宿主机目录。
第三种:
bash
docker rm -v container_name
会删除该容器关联的匿名卷,但不会因此删除命名卷。
因此,删除容器和删除数据卷并不是同一件事。
8.4 Docker Compose 如何查看实际挂载信息?
可以使用:
bash
docker inspect springboot-app
查看输出中的:
json
"Mounts": []
字段。
也可以使用格式化命令:
bash
docker inspect \
--format '{{json .Mounts}}' \
springboot-app
通常可以看到:
json
{
"Type": "bind",
"Source": "/data/docker/springboot/logs",
"Destination": "/app/logs",
"RW": true
}
上面的 JSON 是单条挂载记录的示意,实际输出是包含所有挂载记录的数组。
字段说明:
Type:挂载类型。Source:宿主机路径或卷来源。Destination:容器内的目标路径。RW:是否可读写。
8.5 数据卷可以备份吗?
可以。
对于 Bind Mount,可以通过宿主机上的备份工具备份指定目录。
例如:
bash
tar -czf app-logs.tar.gz /data/docker/springboot/logs
对于命名卷,也可以使用临时容器将卷数据导出。
但是需要特别注意:
数据库目录不能简单地等同于普通文件目录。
对于正在运行的 MySQL 数据库,直接复制数据库文件可能无法获得一致性备份。
生产环境建议优先使用数据库原生备份工具,或者采用经过验证的数据库一致性备份方案。
九、Dockerfile、Docker Run、Docker Compose 挂载方式对比
| 使用方式 | 配置示例 | 说明 |
|---|---|---|
| Dockerfile | VOLUME ["/app/data"] |
声明容器挂载点 |
| Docker Run | -v /host/data:/app/data |
绑定宿主机目录 |
| Docker Run | -v mydata:/app/data |
使用命名卷 |
| Docker Compose | - ./data:/app/data |
使用相对宿主机目录 |
| Docker Compose | - /data/app:/app/data |
使用绝对宿主机目录 |
| Docker Compose | - mydata:/app/data |
使用命名卷 |
| Docker Compose | - ./config.yml:/app/config.yml:ro |
只读文件挂载 |
十、实际项目中如何选择?
在实际生产部署中,可以根据业务类型选择合适的持久化方式。
10.1 Spring Boot 应用
建议:
- Dockerfile 负责打包应用。
- Compose 负责配置宿主机挂载。
- 日志、上传文件和业务文件根据需求持久化。
- 配置文件使用只读挂载,避免应用意外修改。
10.2 MySQL、Redis 数据库
如果没有必须指定宿主机目录的需求,可以优先考虑 Docker Named Volume。
例如:
yaml
volumes:
- mysql_data:/var/lib/mysql
如果公司对数据存储目录有统一要求,也可以使用 Bind Mount。
例如:
yaml
volumes:
- /data/docker/mysql:/var/lib/mysql
10.3 Nginx
Nginx 通常需要挂载配置目录和静态资源目录。
例如:
yaml
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./html:/usr/share/nginx/html:ro
- ./logs:/var/log/nginx
10.4 最佳实践
在实际开发中,建议遵循以下原则:
- Dockerfile 主要负责构建镜像,不要依赖它指定宿主机路径。
- 使用 Docker Compose 管理部署环境相关配置。
- 数据库、上传文件等关键数据应明确设计持久化和备份方案。
- 配置文件优先使用只读挂载。
- 避免随意执行删除数据卷的命令。
- 注意容器用户与宿主机目录之间的权限关系。
十一、总结
Docker 数据持久化是容器化部署中非常重要的一部分。
本文介绍了三种常见的数据存储方式:
- Bind Mount:将宿主机指定目录映射到容器目录。
- Named Volume:由 Docker 创建和管理的数据卷。
- Anonymous Volume:由 Docker 自动创建的匿名数据卷。
同时也介绍了 Dockerfile 和 Docker Compose 在挂载方面的区别。
Dockerfile 中:
dockerfile
VOLUME ["/app/data"]
只能声明容器内部的数据卷挂载点,不能指定宿主机目录。
Docker Compose 中:
yaml
volumes:
- /data/docker/app:/app/data
可以明确指定宿主机目录与容器目录之间的映射关系。
而:
yaml
volumes:
- app_data:/app/data
表示使用 Docker 管理的命名卷。
最核心的一句话:
Dockerfile 负责定义镜像,Docker Compose 负责定义容器如何运行以及数据如何挂载。
理解这一点,就能够更清楚地区分镜像构建、容器部署和数据持久化之间的关系。
对于 Spring Boot、MySQL、Redis、Nginx 等常见服务,也可以根据实际需求灵活选择 Bind Mount 或 Named Volume,让容器化部署更加规范和易于维护。