Java 项目部署之 Docker工具快速入门: Docker 数据卷命令与挂载实操

概述

容器里改个 nginx 首页要 docker exec 进去、还没有 vim;容器一删,改的东西全没。数据卷(volume)就是解决这两件事的:把容器内目录接到宿主机文件系统上,让数据活得比容器久。

纲要

  • 为什么需要数据卷:容器内数据与容器耦合带来的三个问题
    • 不便于修改(要进容器、没编辑器)
    • 数据不可复用(新容器得把操作重做一遍)
    • 升级维护困难(删容器 = 删数据)
  • 数据卷是什么:虚拟目录 → 指向宿主机 /var/lib/docker/volumes/<卷名>/_data
  • 数据卷命令:create / ls / inspect / prune / rm
  • 挂载写法:-v 卷名:容器内路径
  • 实操:创建 html 卷 → 挂到 nginx 的 /usr/share/nginx/html → 宿主机改文件 → 容器生效 → 删容器重建数据仍在
  • 两个高频坑:Docker Desktop 上 Mountpoint 不在宿主机真实文件系统里;prune 会删「未使用」而不是「无用」

容器与数据耦合的三个问题

先说清楚数据卷要解决的到底是什么。一个 nginx 容器跑起来,静态页面在 /usr/share/nginx/html,配置在 /etc/nginx,这些目录长在容器的可写层里,跟着容器走。

由此带来三个麻烦:

问题 具体表现
不便于修改 要 docker exec -it mn bash 进容器,容器里往往没有 vim,sed 改文件还容易踩转义
数据不可复用 在容器里改了一堆配置,新建容器时这些修改不会继承,只能重做一遍
升级维护困难 nginx 要升版本就得删旧容器重建,容器一删,里面改过的页面和配置跟着一起没

三个问题的根源是同一个:数据被封在容器内部。解法就是把它挪出来。

数据卷是什么

数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的一个真实目录。 这句话里的「虚拟」是关键------卷本身只是一层命名,真正存东西的是宿主机上的一个文件夹。

在 Linux 上,Docker 把所有卷的真实目录统一放在 /var/lib/docker/volumes/ 下:

  • 卷名 html → /var/lib/docker/volumes/html/_data
  • 卷名 conf → /var/lib/docker/volumes/conf/_data

注意那个 _data 子目录,卷目录和真正的数据目录差一层,新手 cd 的时候经常漏掉。

容器挂载卷之后,三者关系是这样:
#mermaid-svg-DD6UdWSC9GyCmM6e{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DD6UdWSC9GyCmM6e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DD6UdWSC9GyCmM6e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DD6UdWSC9GyCmM6e .error-icon{fill:#552222;}#mermaid-svg-DD6UdWSC9GyCmM6e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DD6UdWSC9GyCmM6e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DD6UdWSC9GyCmM6e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DD6UdWSC9GyCmM6e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DD6UdWSC9GyCmM6e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DD6UdWSC9GyCmM6e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DD6UdWSC9GyCmM6e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DD6UdWSC9GyCmM6e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DD6UdWSC9GyCmM6e .marker.cross{stroke:#333333;}#mermaid-svg-DD6UdWSC9GyCmM6e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DD6UdWSC9GyCmM6e p{margin:0;}#mermaid-svg-DD6UdWSC9GyCmM6e .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DD6UdWSC9GyCmM6e .cluster-label text{fill:#333;}#mermaid-svg-DD6UdWSC9GyCmM6e .cluster-label span{color:#333;}#mermaid-svg-DD6UdWSC9GyCmM6e .cluster-label span p{background-color:transparent;}#mermaid-svg-DD6UdWSC9GyCmM6e .label text,#mermaid-svg-DD6UdWSC9GyCmM6e span{fill:#333;color:#333;}#mermaid-svg-DD6UdWSC9GyCmM6e .node rect,#mermaid-svg-DD6UdWSC9GyCmM6e .node circle,#mermaid-svg-DD6UdWSC9GyCmM6e .node ellipse,#mermaid-svg-DD6UdWSC9GyCmM6e .node polygon,#mermaid-svg-DD6UdWSC9GyCmM6e .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DD6UdWSC9GyCmM6e .rough-node .label text,#mermaid-svg-DD6UdWSC9GyCmM6e .node .label text,#mermaid-svg-DD6UdWSC9GyCmM6e .image-shape .label,#mermaid-svg-DD6UdWSC9GyCmM6e .icon-shape .label{text-anchor:middle;}#mermaid-svg-DD6UdWSC9GyCmM6e .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DD6UdWSC9GyCmM6e .rough-node .label,#mermaid-svg-DD6UdWSC9GyCmM6e .node .label,#mermaid-svg-DD6UdWSC9GyCmM6e .image-shape .label,#mermaid-svg-DD6UdWSC9GyCmM6e .icon-shape .label{text-align:center;}#mermaid-svg-DD6UdWSC9GyCmM6e .node.clickable{cursor:pointer;}#mermaid-svg-DD6UdWSC9GyCmM6e .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DD6UdWSC9GyCmM6e .arrowheadPath{fill:#333333;}#mermaid-svg-DD6UdWSC9GyCmM6e .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DD6UdWSC9GyCmM6e .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DD6UdWSC9GyCmM6e .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DD6UdWSC9GyCmM6e .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DD6UdWSC9GyCmM6e .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DD6UdWSC9GyCmM6e .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DD6UdWSC9GyCmM6e .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DD6UdWSC9GyCmM6e .cluster text{fill:#333;}#mermaid-svg-DD6UdWSC9GyCmM6e .cluster span{color:#333;}#mermaid-svg-DD6UdWSC9GyCmM6e div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DD6UdWSC9GyCmM6e .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DD6UdWSC9GyCmM6e rect.text{fill:none;stroke-width:0;}#mermaid-svg-DD6UdWSC9GyCmM6e .icon-shape,#mermaid-svg-DD6UdWSC9GyCmM6e .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DD6UdWSC9GyCmM6e .icon-shape p,#mermaid-svg-DD6UdWSC9GyCmM6e .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DD6UdWSC9GyCmM6e .icon-shape .label rect,#mermaid-svg-DD6UdWSC9GyCmM6e .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DD6UdWSC9GyCmM6e .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DD6UdWSC9GyCmM6e .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DD6UdWSC9GyCmM6e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 容器
Docker 管理的卷
宿主机文件系统
映射
映射
-v html:/usr/share/nginx/html
-v html:/usr/share/nginx/html
-v conf:/etc/nginx
-v conf:/etc/nginx
/var/lib/docker/volumes/html/_data
/var/lib/docker/volumes/conf/_data
数据卷 html
数据卷 conf
nginx-1

/usr/share/nginx/html

/etc/nginx
nginx-2

/usr/share/nginx/html

/etc/nginx

图里能直接读出数据卷带来的三个好处:

  • 改数据不必进容器 :在宿主机 _data 里用任何编辑器改文件,容器内立刻可见,双向实时。
  • 多容器共享:两个容器挂同一个卷,配置、静态资源就复用了,不用重复劳动。
  • 删容器不删数据:容器删掉,卷和它指向的宿主机目录都还在,新版本容器重新挂上就能接着用旧数据。

数据卷命令速查

docker 是一级命令,volume 是它下面的二级子命令,create、ls、inspect 这些才是具体动作,结构和 docker image、docker container 一致。:

bash 复制代码
docker volume [COMMAND]

五个常用子命令:

命令 作用 常用参数
docker volume create 创建一个数据卷 -d local 指定驱动(默认 local);--label 打标签
docker volume ls 列出所有数据卷 -q 只输出卷名;-f dangling=true 过滤未使用的卷
docker volume inspect 查看卷详情,重点是 Mountpoint -f '{``{.Mountpoint}}' 直接取挂载点路径
docker volume prune 删除所有未使用的数据卷 -f 跳过确认;--filter 按 label 过滤
docker volume rm 删除指定的数据卷 后面可跟多个卷名

create:创建一个卷

bash 复制代码
# 创建名为 html 的数据卷
docker volume create html

# 带驱动和标签创建(local 驱动可省略 -d,默认就是它)
docker volume create --driver local --label project=tanhua html-data

create 只在卷不存在时创建;同名卷已存在会直接返回卷名,不会报错,也不会清空原有数据。这点在脚本里可以放心重复执行。

ls:列出所有卷

bash 复制代码
docker volume ls
text 复制代码
DRIVER    VOLUME NAME
local     html
local     1a9f0c8b3d2e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a

第二行那串 64 位十六进制是匿名卷 ------镜像里声明了 VOLUME(比如 mysql 镜像声明了 /var/lib/mysql),但你 docker run 时没给它起名,Docker 就自动生成。查磁盘占用异常、或者 prune 删多了,往往就是这些匿名卷在背锅。

只想要卷名给脚本用:

bash 复制代码
docker volume ls -q

inspect:查看卷挂载到宿主机哪个目录

bash 复制代码
docker volume inspect html
json 复制代码
[
    {
        "CreatedAt": "2026-09-29T10:12:33+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/html/_data",
        "Name": "html",
        "Options": null,
        "Scope": "local"
    }
]

唯一要盯的就是 Mountpoint。写脚本时不需要肉眼抠 JSON,用 Go 模板直接取:

bash 复制代码
docker volume inspect html -f '{{.Mountpoint}}'
# /var/lib/docker/volumes/html/_data

也可以一次查多个卷:docker volume inspect html conf。

prune:删掉未被引用的卷

bash 复制代码
docker volume prune
text 复制代码
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
html

Total reclaimed space: 12.4kB

这里是新手最容易误解的一条:prune 删的是「当前没有任何容器引用」的卷,不是「所有卷」,也不是「没用的卷」。 一个卷里可能放着 20G 的 MySQL 数据,只要它所属的那个容器被停掉(哪怕只是 docker stop),它就已经「未使用」了,prune 会照删不误。

反过来也一样:正在被容器引用的卷,prune 不会碰它,要删得先停容器或者直接 rm。

安全一点的做法是先看看会被删掉哪些,再决定:

bash 复制代码
# 只看将被 prune 删除的卷,不真删
docker volume ls -f dangling=true

rm:删除指定卷

bash 复制代码
# 删一个
docker volume rm html

# 一次删多个
docker volume rm html conf

卷还被容器挂着时会报这个错:

text 复制代码
Error response from daemon: remove html: volume is in use -
[8f3d1c9a7b2e...]

先停并删除引用它的容器,或者使用 docker rm -v(见下文「坑」一节)。

实操:给 nginx 挂载数据卷

完整走一遍:建卷 → 挂载 → 宿主机改文件 → 容器生效 → 删容器重建 → 数据还在。

准备素材

课程资料目录里跟这次实操有关的部分:

tree 复制代码
day03-Docker/资料/
├── cloud-demo/
│   ├── docker-compose.yml        # mysql 用目录挂载:$PWD/mysql/data:/var/lib/mysql
│   └── mysql/
│       ├── conf/
│       │   └── hmy.cnf           # datadir=/var/lib/mysql、character_set_server=utf8
│       └── data/                 # 真实落盘数据:ibdata1、ib_logfile0、cloud_order、cloud_user
├── mysql.tar
├── nginx.conf
└── hmy.cnf

那个 mysql/data/ 就是「数据与容器解耦」最直观的例子------它是从容器里拷出来的真实数据目录,重新起一个 mysql 容器挂上去,库表原样恢复。

创建卷并挂载

bash 复制代码
# ① 创建数据卷
docker volume create html

# ② 确认创建成功
docker volume ls

# ③ 看它落在宿主机哪个目录
docker volume inspect html -f '{{.Mountpoint}}'
# /var/lib/docker/volumes/html/_data

# ④ 创建 nginx 容器,把 html 卷挂到容器的静态资源目录
docker rm -f mn 2>/dev/null      # 清掉上一篇残留的同名容器
docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx

-v 的格式是 -v 卷名:容器内路径。卷不存在时 Docker 会自动创建,但显式 create 一遍更好------你能先确认名字没写错,也不会因为手滑拼错卷名而悄悄多出一个匿名卷。

讲义里那段示例有个小问题,照抄会踩坑:

bash 复制代码
# 讲义原文:少了 -d,且行尾多了续行符
docker run \
  --name mn \
  -v html:/root/html \
  -p 8080:80
  nginx \

没有 -d 就是前台运行,终端一关容器就停;结尾那个 \ 会把命令挂住等你继续输入,表现为「回车没反应」。写成一行、补上 -d 才是能跑的版本。

在宿主机改文件,容器内立刻生效

bash 复制代码
# 进入卷的真实目录
cd /var/lib/docker/volumes/html/_data

# 直接改首页(宿主机上有 vim,容器里没有)
cat > index.html <<'EOF'
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>volume demo</title></head>
<body><h1>hello from host filesystem</h1></body>
</html>
EOF

# 不用重启容器,直接访问
curl http://localhost

反向也成立------在容器里写文件,宿主机目录里马上能看到:

bash 复制代码
docker exec mn sh -c 'echo "written in container" > /usr/share/nginx/html/probe.txt'
cat /var/lib/docker/volumes/html/_data/probe.txt
# written in container

删掉容器,数据还在

bash 复制代码
# 删容器
docker rm -f mn

# 卷和它的目录都还在
docker volume ls
ls /var/lib/docker/volumes/html/_data
# index.html  probe.txt

# 起一个新容器,重新挂同一个卷
docker run --name mn2 -v html:/usr/share/nginx/html -p 80:80 -d nginx
curl http://localhost
# 还是刚才那个页面

这一步就是「升级维护不丢数据」的完整闭环:容器是消耗品,卷才是资产。
容器 mn / mn2 卷 html Docker 守护进程 宿主机 容器 mn / mn2 卷 html Docker 守护进程 宿主机 #mermaid-svg-tPs0e92nKOD9Ftde{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tPs0e92nKOD9Ftde .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tPs0e92nKOD9Ftde .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tPs0e92nKOD9Ftde .error-icon{fill:#552222;}#mermaid-svg-tPs0e92nKOD9Ftde .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tPs0e92nKOD9Ftde .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tPs0e92nKOD9Ftde .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tPs0e92nKOD9Ftde .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tPs0e92nKOD9Ftde .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tPs0e92nKOD9Ftde .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tPs0e92nKOD9Ftde .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tPs0e92nKOD9Ftde .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tPs0e92nKOD9Ftde .marker.cross{stroke:#333333;}#mermaid-svg-tPs0e92nKOD9Ftde svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tPs0e92nKOD9Ftde p{margin:0;}#mermaid-svg-tPs0e92nKOD9Ftde .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tPs0e92nKOD9Ftde text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-tPs0e92nKOD9Ftde .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tPs0e92nKOD9Ftde .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-tPs0e92nKOD9Ftde .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-tPs0e92nKOD9Ftde .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-tPs0e92nKOD9Ftde #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-tPs0e92nKOD9Ftde .sequenceNumber{fill:white;}#mermaid-svg-tPs0e92nKOD9Ftde #sequencenumber{fill:#333;}#mermaid-svg-tPs0e92nKOD9Ftde #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-tPs0e92nKOD9Ftde .messageText{fill:#333;stroke:none;}#mermaid-svg-tPs0e92nKOD9Ftde .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tPs0e92nKOD9Ftde .labelText,#mermaid-svg-tPs0e92nKOD9Ftde .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-tPs0e92nKOD9Ftde .loopText,#mermaid-svg-tPs0e92nKOD9Ftde .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-tPs0e92nKOD9Ftde .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tPs0e92nKOD9Ftde .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-tPs0e92nKOD9Ftde .noteText,#mermaid-svg-tPs0e92nKOD9Ftde .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-tPs0e92nKOD9Ftde .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tPs0e92nKOD9Ftde .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tPs0e92nKOD9Ftde .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tPs0e92nKOD9Ftde .actorPopupMenu{position:absolute;}#mermaid-svg-tPs0e92nKOD9Ftde .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-tPs0e92nKOD9Ftde .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tPs0e92nKOD9Ftde .actor-man circle,#mermaid-svg-tPs0e92nKOD9Ftde line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-tPs0e92nKOD9Ftde :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 卷与 _data 目录保留 docker volume create html 创建 /var/lib/docker/volumes/html/_data docker run -v html:/usr/share/nginx/html 绑定到容器目录 写入 index.html 落到 _data 目录 vi index.html 修改 容器内立即生效(无需重启) docker rm -f mn docker run -v html:/usr/share/nginx/html 新容器读到旧数据

实操中容易踩的坑

坑一:Mountpoint 在 macOS / Windows 上根本不存在

docker volume inspect 显示的 /var/lib/docker/volumes/html/_data 是 Docker 守护进程所在环境 的路径。Linux 上守护进程就在本机,这个路径能直接 cd 进去。

macOS 和 Windows 上用的是 Docker Desktop,它在后台起了一个 Linux 虚拟机(早期是 HyperKit / Hyper-V,新版本是 Apple 的 vmnet + containerd),守护进程跑在虚拟机里。所以你在 Mac 终端执行:

bash 复制代码
cd /var/lib/docker/volumes/html/_data
# cd: no such file or directory: /var/lib/docker/volumes/html/_data

这是新手最常踩的坑,而且报错信息完全不会提示「你在虚拟机外面」。

几种绕过去的办法:

bash 复制代码
# 办法一:起一个临时容器,把卷挂进去看/改内容
docker run --rm -it -v html:/data alpine sh
# 进入后:cd /data && ls

# 办法二:用 cp 在容器和本机之间搬运文件
docker run --rm -v html:/data -v "$(pwd)":/backup alpine \
  sh -c 'cd /data && tar cf /backup/html.tar .'

# 办法三(开发机推荐):直接用目录挂载,路径自己说了算
docker rm -f mn 2>/dev/null      # 清掉上一篇残留的同名容器
docker run --name mn -v "$(pwd)/html:/usr/share/nginx/html" -p 80:80 -d nginx

第三种在开发机上最省事,代价是目录得自己管理,而且要注意 Docker Desktop 的 File Sharing 白名单(Settings → Resources → File Sharing),没加进白名单的目录挂上去会是空的。

坑二:prune 与 -v 的误删风险

两个容易出事的操作:

bash 复制代码
# 危险:容器只是停了,卷就成了「未使用」,里面的数据会被删
docker stop mysql-prod
docker volume prune -f

# 危险:rm -v 会连容器的匿名卷一起删掉
docker rm -v mysql-prod

docker-compose down -v 同理,它会删掉 compose 文件里声明的命名卷,生产库数据就是这么没的。

排查与防护建议:

  • 删之前先 docker volume ls -f dangling=true 看清单,别盲删。
  • 重要的卷打 label 区分,prune 时用 --filter 排除:docker volume prune --filter "label!=keep"(不支持这样反向过滤时,就别用 prune,改用显式 rm)。
  • 生产环境的数据卷,备份放在卷之外,别指望卷自身提供安全性------卷只是让数据不随容器消失,不是备份。

顺带一提:卷挂载 vs 目录直接挂载

对比项 卷挂载 -v 卷名:容器目录 目录挂载 -v 宿主机目录:容器目录
目录谁管 Docker 管,统一在 /var/lib/docker/volumes/ 你自己管,路径随意
找起来 深,要 inspect 才知道在哪 一眼就能找到
跨平台 路径由 Docker 处理,行为一致 依赖宿主机路径,Windows 盘符、Docker Desktop 白名单都要处理
初始化 容器目录原有内容会被复制到新卷 宿主机目录为空时,容器内原有内容会被覆盖为空
适合场景 数据库数据、需要长期保存的状态 配置文件、源码、本地开发热重载

最后一行那个「覆盖」也是个坑:把空目录挂到 /etc/nginx 上,容器里的 nginx.conf 就没了,容器起不来。想保留容器内原有文件,就用卷挂载,或者先把文件拷到宿主机目录里。

官方文档

总结

  • 数据卷是「虚拟目录 → 宿主机真实目录」的一层映射,Linux 上落在 /var/lib/docker/volumes/<卷名>/_data,注意 _data 这一层。
  • 它解决的是同一个根源问题:容器内数据被封死。改起来麻烦、无法复用、删容器即丢数据,都是这个根源的三个表现。
  • 五条命令记牢分工:create 建、ls 看、inspect 定位(重点看 Mountpoint)、prune 清未被引用、rm 删指定。
  • prune 的「未使用」指的是「当前没有容器引用」,跟「没用」是两回事,停掉的容器卷照样会被删。
  • Docker Desktop 上 Mountpoint 在虚拟机里,宿主机 cd 不进去;开发机直接用目录挂载更省事。
  • 挂空目录会覆盖容器内原有文件,配置类目录要么用卷,要么先把文件拷出来。
相关推荐
吴声子夜歌1 小时前
Docker入门与实战——实战案例(编程开发)(一)
运维·docker·容器
云和数据.ChenGuang1 小时前
用 Claude Code 重构遗留系统:从评估到落地的完整指南
java·人工智能·spring boot·后端·重构·springai
霸道流氓气质1 小时前
AI 应用异常检测与根因分析:语义漂移检测、因果图推理与LLM-as-Judge的 Java 生产级实现
java·开发语言·人工智能
棉猴1 小时前
玩游戏学Python2-IDLE与pgzero入门
开发语言·python·游戏·玩游戏·pgzero·pgzrun
xuxigifxfh1 小时前
牛客:BISHI2 【模板】栈的操作
java·开发语言·windows·算法
房开民2 小时前
通俗总结 Octree(八叉树)
java·服务器·数据库
(Charon)2 小时前
【C++面试】线程并发与异步:区别、实现原理及使用场景
开发语言·c++
AIFQuant2 小时前
Python A股实时行情API接入:沪深股票数据与WebSocket推送实战
开发语言·python·websocket·金融·a股行情
2302_805771072 小时前
Day23:POM设计模式(封装)
java·开发语言·设计模式