目录
Docker Volume 教程:使用 Volume 和 Bind Mount 持久化数据
容器内写入的文件存在容器的可写层中,容器一旦删除,数据也会随之消失。数据库、用户上传的文件这类需要保存的数据,必须放在容器外部。Docker 提供三种挂载方式:
| 类型 | 数据存在哪里 | 适用场景 |
|---|---|---|
| Volume | Docker 管理的区域 | 数据库、生产环境中的持久数据 |
| Bind mount | 主机上由你指定的文件夹 | 开发时同步代码、读取主机配置文件 |
| tmpfs | 内存 | 临时数据、不想写入硬盘的数据 |
Volume:删除容器,数据还在
用 PostgreSQL 实际验证。启动数据库,将数据存入名为 pgdata 的 Volume:
docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql postgres:18
-v pgdata:/var/lib/postgresql 的意思是:将名为 pgdata 的 Volume 挂载到容器内的 /var/lib/postgresql。如果 Volume 不存在,Docker 会自动创建。
创建一张表,写入一条数据,然后删除容器,再用同一个 Volume 启动一个全新的容器:
$ docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql postgres:18
119ed37ba5c6dc12a773a32d4ef296523529552f94a2ab1ee56113c1b990d7ae
$ docker ps --filter name=db
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
119ed37ba5c6 postgres:18 "docker-entrypoint.s…" 8 seconds ago Up 8 seconds 5432/tcp db
$ docker exec db psql -U postgres -c "CREATE TABLE notes (id serial, body text); INSERT INTO notes (body) VALUES ('hello volume');"
CREATE TABLE
INSERT 0 1
$ docker exec db psql -U postgres -c "SELECT * FROM notes;"
id | body
----+--------------
1 | hello volume
(1 row)
$ docker rm -f db
db
$ docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql postgres:18
d94fb2966b3dc5790239b4a48d180aedd193f6fc2c846d6878a7c5fcc450f7cd
$ docker exec db psql -U postgres -c "SELECT * FROM notes;"
id | body
----+--------------
1 | hello volume
(1 row)
新容器读取到了旧容器写入的数据:数据存储在 Volume 中,与容器的生命周期无关。
PostgreSQL 18 的数据目录
很多旧教程使用的是
-v pgdata:/var/lib/postgresql/data。这是 PostgreSQL 17 及更早版本的写法,在postgres:18中会直接启动失败:
$ docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:18
c1cec1074a4d440c1f6d106d994171d290cd8ea9da3266d798c0c81662bd8281
$ docker ps -a --filter name=db
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c1cec1074a4d postgres:18 "docker-entrypoint.s…" 8 seconds ago Exited (1) 7 seconds ago db
$ docker logs db
Error: in 18+, these Docker images are configured to store database data in a
format which is compatible with "pg_ctlcluster" (specifically, using
major-version-specific directory names). This better reflects how
PostgreSQL itself works, and how upgrades are to be performed.
See also https://github.com/docker-library/postgres/pull/1259
Counter to that, there appears to be PostgreSQL data in:
/var/lib/postgresql/data (unused mount/volume)
This is usually the result of upgrading the Docker image without
upgrading the underlying database using "pg_upgrade" (which requires both
versions).
The suggested container configuration for 18+ is to place a single mount
at /var/lib/postgresql which will then place PostgreSQL data in a
subdirectory, allowing usage of "pg_upgrade --link" without mount point
boundary issues.
See https://github.com/docker-library/postgres/issues/37 for a (long)
discussion around this process, and suggestions for how to do so.
$ docker rm -f db
db
$ docker volume rm pgdata
pgdata
PostgreSQL 18 的官方镜像改为将数据存放在 /var/lib/postgresql/18/docker 这类按主版本区分的子目录中,以便日后使用 pg_upgrade 升级。新的做法是挂载上一层目录 /var/lib/postgresql:
$ docker volume ls
DRIVER VOLUME NAME
local 5775c1d90defc3dc23a92cdde10cd62b182d9c774ae43f0a40595fe62131a63c
local pgdata
$ docker volume inspect pgdata
[
{
"CreatedAt": "2026-10-13T00:09:44Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
"Name": "pgdata",
"Options": null,
"Scope": "local"
}
]
$ docker exec db sh -c 'echo $PGDATA; ls /var/lib/postgresql'
/var/lib/postgresql/18/docker
18
PGDATA现在是/var/lib/postgresql/18/docker。docker volume inspect中的Mountpoint是 Volume 在 Docker 主机上的实际位置。在 Docker Desktop 上,它位于 Docker 的 Linux 虚拟机中,无法直接通过 Windows / macOS 的文件资源管理器查看,因此备份要使用下面介绍的方法。
管理 Volume
docker volume ls # 列出所有 Volume
docker volume inspect pgdata # 查看详细信息
docker volume rm pgdata # 删除(没有容器使用时才能删除)
docker volume prune # 删除未使用的匿名 Volume
docker volume prune -a # 连具名 Volume 也一起删除(小心!)
docker volume prune -a和docker system prune --volumes会删除数据库数据。执行前请先确认,或先进行备份。
匿名 Volume 的来源
未使用 -v 指定时,很多数据库镜像仍然会创建 Volume,因为它们的 Dockerfile 中有 VOLUME 指令:
$ docker volume ls
DRIVER VOLUME NAME
$ docker run -d --name anon -e POSTGRES_PASSWORD=secret postgres:18
1db583d1b34d0482cfa458a5fe4a7fc7c1320633bde0339407a0c24fe62ad630
$ docker volume ls
DRIVER VOLUME NAME
local 0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8
$ docker inspect --format '{{range .Mounts}}{{.Type}} {{.Name}} -> {{.Destination}}{{println}}{{end}}' anon
volume 0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8 -> /var/lib/postgresql
$ docker rm -f anon
anon
$ docker volume ls
DRIVER VOLUME NAME
local 0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8
$ docker volume prune -f
Deleted Volumes:
0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8
Total reclaimed space: 40.49MB
- 只运行了
docker run,docker volume ls就多了一个名称是一长串乱码的匿名 Volume,挂载在/var/lib/postgresql。 docker rm -f删除容器后,匿名 Volume 仍然保留,占用 40 MB。很多人的 Docker 越用越大,原因之一就是这些被遗忘的匿名 Volume。docker volume prune会清理未使用的匿名 Volume。删除容器时加上-v(docker rm -fv anon)也会一并删除它的匿名 Volume。
建议始终使用具名 Volume,这样一看就知道它的用途。
Bind mount:将主机文件夹挂载到容器中
docker run -d --name site -p 8080:80 -v "$(pwd)/site:/usr/share/nginx/html:ro" nginx:1.30
docker run -d --name site -p 8080:80 -v "${PWD}/site:/usr/share/nginx/html:ro" nginx:1.30
docker run -d --name site -p 8080:80 -v "%cd%/site:/usr/share/nginx/html:ro" nginx:1.30

在主机上使用编辑器修改 site/index.html 的标题并保存,然后刷新浏览器:

这是开发时最常见的用法:代码留在主机上,用你熟悉的编辑器修改,容器负责运行。
注意事项:
- 主机路径必须写成绝对路径。
$(pwd)会替换为当前目录(Windows PowerShell 使用${PWD})。 - 如果写的是名称而不是路径(例如
site:/data),Docker 会将其视为 Volume 名称。 - 在 Linux 上,容器以 root 写入的文件,在主机上也属于 root。可以使用
-u $(id -u):$(id -g)让容器以你的身份运行。 - 只读挂载要加上
:ro。
-v 与 --mount 遇到不存在的路径
$ ls -d nothere
ls: nothere: No such file or directory
$ docker run --rm -v "$(pwd)/nothere:/data" alpine:3.23 ls -la /data
total 8
drwxr-xr-x 2 root root 4096 Oct 13 00:09 .
drwxr-xr-x 1 root root 4096 Oct 13 00:09 ..
$ ls -ld nothere
drwxr-xr-x 2 root root 4096 Oct 13 00:09 nothere
$ docker run --rm --mount type=bind,src="$(pwd)/nothere2",dst=/data alpine:3.23 ls /data
docker: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /work/nothere2
Run 'docker run --help' for more information
Windows 对应写法(点击标签页切换):
docker run --rm -v "$(pwd)/nothere:/data" alpine:3.23 ls -la /data
docker run --rm --mount type=bind,src="$(pwd)/nothere2",dst=/data alpine:3.23 ls /data
docker run --rm -v "${PWD}/nothere:/data" alpine:3.23 ls -la /data
docker run --rm --mount type=bind,src="${PWD}/nothere2",dst=/data alpine:3.23 ls /data
docker run --rm -v "%cd%/nothere:/data" alpine:3.23 ls -la /data
docker run --rm --mount type=bind,src="%cd%/nothere2",dst=/data alpine:3.23 ls /data
-v遇到不存在的主机路径时,会自动创建一个空文件夹(而且属于 root)。容器可以成功启动,但挂载进去的是空目录,很容易让人以为“明明已经挂载却没有文件”。--mount type=bind遇到不存在的路径时会直接报错,更容易发现路径写错了。
--mount 的写法比较长,但更明确:
docker run -d \
--mount type=volume,src=pgdata,dst=/var/lib/postgresql \
--mount type=bind,src="$(pwd)/init.sql",dst=/docker-entrypoint-initdb.d/init.sql,readonly \
-e POSTGRES_PASSWORD=secret postgres:18
docker run -d `
--mount type=volume,src=pgdata,dst=/var/lib/postgresql `
--mount type=bind,src="${PWD}/init.sql",dst=/docker-entrypoint-initdb.d/init.sql,readonly `
-e POSTGRES_PASSWORD=secret postgres:18
docker run -d ^
--mount type=volume,src=pgdata,dst=/var/lib/postgresql ^
--mount type=bind,src="%cd%/init.sql",dst=/docker-entrypoint-initdb.d/init.sql,readonly ^
-e POSTGRES_PASSWORD=secret postgres:18
tmpfs:存储在内存中
$ docker run --rm --tmpfs /cache:size=64m alpine:3.23 df -h /cache
Filesystem Size Used Available Use% Mounted on
tmpfs 64.0M 0 64.0M 0% /cache
数据只存在内存中,容器停止后就会消失,适合存放临时文件(仅限 Linux 容器)。
备份与恢复 Volume
Volume 的位置由 Docker 管理,最通用的备份方式是启动一个临时容器,将 Volume 打包成 tar 文件:
$ docker run --rm -v pgdata:/data:ro -v "$(pwd):/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
$ ls -lh pgdata.tar.gz
-rw-r--r-- 1 root root 4.6M Oct 13 00:10 pgdata.tar.gz
$ docker volume create pgdata-restore
pgdata-restore
$ docker run --rm -v pgdata-restore:/data -v "$(pwd):/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
$ docker run --rm -v pgdata-restore:/data alpine:3.23 ls /data/18/docker
PG_VERSION
base
global
pg_commit_ts
pg_dynshmem
… (以下省略)
$ rm pgdata.tar.gz
Windows 对应写法(点击标签页切换):
docker run --rm -v pgdata:/data:ro -v "$(pwd):/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata-restore:/data -v "$(pwd):/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
docker run --rm -v pgdata:/data:ro -v "${PWD}:/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata-restore:/data -v "${PWD}:/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
docker run --rm -v pgdata:/data:ro -v "%cd%:/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata-restore:/data -v "%cd%:/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
- 第一个命令:将
pgdata以只读方式挂载到/data,将当前文件夹挂载到/backup,用 tar 将/data打包成pgdata.tar.gz,并保存到主机的当前文件夹。 - 恢复时创建一个新的 Volume,用相同的方式将文件解压进去。恢复后的 Volume 中可以看到
18/docker下的 PostgreSQL 数据文件。
直接打包正在运行的数据库文件,可能会得到不一致的数据。上面的示例是在数据库停止后备份的。数据库建议使用它自己的导出工具,例如
pg_dump或mysqldump,详见使用 Docker 运行 PostgreSQL。
应该使用哪一种?
- 数据库、需要长期保存的数据 → Volume
- 开发时想实时看到代码修改 → Bind mount
- 将主机配置文件交给容器读取 → Bind mount(只读)
- 临时数据、不需要保存 → tmpfs 或直接写入容器
重点回顾
- 数据库等需要保存的数据使用具名 Volume。
- 开发时使用 Bind mount,让容器直接使用主机上的代码。
-v遇到不存在的主机路径时会创建空文件夹;--mount会直接报错。- 备份 Volume:使用临时容器,同时挂载 Volume 和主机文件夹,再用 tar 打包。
动手练习
练习 1:创建 Volume notes,用一个容器写入文件,删除容器后再用另一个容器读出文件。
答案
docker volume create notes
docker run --rm -v notes:/data alpine:3.23 sh -c "echo hello > /data/a.txt"
docker run --rm -v notes:/data alpine:3.23 cat /data/a.txt
输出 hello。Volume 的管理命令见 docker volume。
练习 2:docker run -v data:/app 和 docker run -v ./data:/app 有什么区别?
答案
data 不含斜杠,会被视为 Volume 名称;./data 是路径,是 Bind mount,挂载主机当前文件夹下的 data。
常见问题
Volume 的数据存在哪里?
Linux 上在 /var/lib/docker/volumes/名称/_data。Docker Desktop 的 Volume 位于它的 Linux 虚拟机中,Windows 文件资源管理器或 Finder 看不到,可以通过 Docker Desktop 的 Volumes 界面查看。
Windows 上 -v 的主机路径怎么写?
PowerShell 使用 "${PWD}/site:/app",CMD 使用 "%cd%/site:/app",本章的命令都提供了三种写法的标签页。
Bind mount 的文件权限不对?
Linux 上容器以 root 写入的文件,在主机上也属于 root。使用 -u $(id -u):$(id -g) 让容器以你的身份运行。
更新于 2026年10月14日