Docker 数据卷(Volume)详解:Dockerfile 声明、Docker Compose 挂载宿主机目录及持久化实战

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 常见的数据存储方式有三种:

  1. Bind Mount(绑定挂载)
  2. Named Volume(命名卷)
  3. 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 最佳实践

在实际开发中,建议遵循以下原则:

  1. Dockerfile 主要负责构建镜像,不要依赖它指定宿主机路径。
  2. 使用 Docker Compose 管理部署环境相关配置。
  3. 数据库、上传文件等关键数据应明确设计持久化和备份方案。
  4. 配置文件优先使用只读挂载。
  5. 避免随意执行删除数据卷的命令。
  6. 注意容器用户与宿主机目录之间的权限关系。

十一、总结

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,让容器化部署更加规范和易于维护。


相关推荐
zh73141 小时前
Docker Desktop docker_data.vhdx 使用时间长了过大,解决办法
运维·docker·容器
墨神谕1 小时前
Docker Compose 从入门到实战:核心概念、YAML 配置详解、常用命令与多容器部署
运维·docker·容器
kyle~1 小时前
Linux --- CPU 调度机制
linux·运维·数码相机
晴天的雨.9921 小时前
【Linux】编译器gcc/g++
linux·运维·服务器
xiaoyunxuexibiji1 小时前
供应商认证管理怎么做?从资质审核到现场认证、样品认证
运维·服务器·人工智能
JoyCong19981 小时前
参数测评:ToDesk个人版、团队版、企业版的设备数、并发通道与安全能力对比
运维·服务器·网络·安全·智能手机·远程工作
Tronlong创龙1 小时前
TLT153工控机:Docker 环境部署与容器开发实践(上)
docker·嵌入式开发·硬件开发·工业控制·工业开发板
KIDULT°1 小时前
【K8s 实战】Volume 存储、PV/PVC、ConfigMap 与 Secret 完整实验
云原生·容器·kubernetes
无敌糖果1 小时前
Nginx 后端error.log疯狂刷盘问题分析
运维·nginx