很多人把 postgres:17 升到 postgres:18 之后,会遇到一个很诡异的现象:docker-compose.yml 里明明已经写了命名卷,结果容器启动后还是多出一个随机名字的数据卷,真正的数据还常常写进了那个随机卷里。
这篇文章把这个问题完整讲透:
- 现象是什么
- 会造成什么后果
- 根因到底在哪
- PostgreSQL 18 应该怎么挂卷
- 如果已经出现了随机卷,怎么把数据迁回命名卷
- 后续如何避免再踩坑
一句话结论
这通常不是 POSTGRES_DB 的问题,也不是 Docker Compose “失效”了,而是 PostgreSQL 官方 Docker 镜像在 18 版本修改了默认 PGDATA 和 VOLUME 路径。如果你还沿用旧版本的挂载路径,Docker 就会自动创建一个匿名卷,真正的数据往往写进匿名卷,而不是你以为的命名卷。
1. 现象:明明写了命名卷,结果还是多出一个随机卷
最常见的现象是这样的。
你写了一个 PostgreSQL 18 的 Compose 配置:
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
volumes:
- app_pgdata:/var/lib/postgresql/data
volumes:
app_pgdata:
你以为数据会落在 app_pgdata 里。
但容器起来后,执行:
docker volume ls
你会发现除了 app_pgdata,还多了一个随机名字的卷,比如:
DRIVER VOLUME NAME
local app_pgdata
local 8c5c1a4f0b8f1d6e4f9b2a7c3d1e9ab6e0b4f2d3f5a6c7d8e9f0a1b2c3d4e5f6
再检查容器挂载:
docker inspect pg18-demo --format '{{json .Mounts}}'
你往往会看到两类挂载同时存在:
- 你自己指定的命名卷,挂到了
/var/lib/postgresql/data - Docker 自动生成的匿名卷,挂到了
/var/lib/postgresql
更关键的是,PostgreSQL 实际写数据的目录,经常根本不是你挂的那个旧路径。
可以直接进容器确认:
docker exec -it pg18-demo sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT current_database(), current_setting('\''data_directory'\'');"'
你很可能会看到类似结果:
app_db|/var/lib/postgresql/18/docker
这就说明:数据库真实数据目录并不在 /var/lib/postgresql/data,而是在 /var/lib/postgresql/18/docker。
2. 这个现象会造成什么后果
这个问题最危险的地方在于,它不只是“多了一个卷不好看”,而是会直接影响数据安全和运维判断。
2.1 你以为数据在命名卷里,其实不在
你以为:
- 数据在
app_pgdata - 删除容器再重建没问题
- 备份
app_pgdata就够了
但实际上:
- 真正的数据可能写进了匿名卷
- 你备份的是“错卷”
- 你删容器、删匿名卷、清理 dangling volume 时,可能把真数据一起删掉
2.2 容器重建后“数据丢了”
如果后续:
docker compose down
docker compose up -d
或者你在某次清理中把匿名卷删了,数据库看起来就像“突然重置”了一样。
其实不是 PostgreSQL 把数据吃了,而是 你一直连的不是自己以为的那个卷。
2.3 容器越多,环境越乱
这种问题一旦在多个项目里重复出现,会导致:
- 一个服务对应多个卷
- 同一台机器上堆出很多随机卷
- 团队成员很难判断哪个卷才是真卷
- 备份、迁移、回滚都变得高风险
2.4 会误导排查方向
很多人一开始会怀疑:
POSTGRES_DB没生效- Compose 没加载对
- PostgreSQL 初始化脚本有问题
- Docker Desktop 有 bug
实际上,这些通常都不是根因。
3. 根因排查:问题不在数据库名,而在数据目录和挂载路径不匹配
这个问题要从三个层面看。
3.1 POSTGRES_DB 只决定默认数据库名,不决定卷挂载
POSTGRES_DB 的作用是:
- 初始化时创建哪个默认数据库
它不决定:
- 数据写到哪个卷
- 卷挂载到哪里
- Docker 会不会自动创建匿名卷
所以,数据库名和卷名是两回事。
3.2 PostgreSQL 18 官方镜像改了默认数据目录
根据 PostgreSQL 官方 Docker 镜像说明,PostgreSQL 18 起,镜像默认的 PGDATA 改成了版本化路径:
/var/lib/postgresql/18/docker
同时,镜像声明的 VOLUME 也改成了:
/var/lib/postgresql
这意味着在 PostgreSQL 18 里,正确的挂载思路不再是旧时代的:
/var/lib/postgresql/data
而应该围绕新的上层目录:
/var/lib/postgresql
3.3 Docker 为什么会自动创建随机匿名卷
Docker 的行为本身并不奇怪:
- 如果镜像声明了某个
VOLUME - 但你运行容器时没有正确覆盖那个目标路径
- Docker 就会为这个路径自动创建一个匿名卷
匿名卷的名字通常就是一串随机 ID。
在 PostgreSQL 18 这个场景里:
- 镜像要用的是
/var/lib/postgresql - 真实
PGDATA默认在/var/lib/postgresql/18/docker - 但你还在挂老路径
/var/lib/postgresql/data
结果就是:
- 你指定的命名卷挂在旧位置
- 镜像真正要用的上层路径没被你正确接管
- Docker 自动补了一个匿名卷
- PostgreSQL 把真实数据写进匿名卷
这就是“明明指定了卷名,结果还是多了随机卷”的根因。
4. 版本差异:为什么以前能用,到了 18 就出事
这个问题最容易让人困惑的一点,是同一份 Compose 配置可能在 PostgreSQL 17 没问题,升级到 18 就出问题。
可以把规律记成下面这张表:
| PostgreSQL 版本 | 默认 PGDATA | 镜像声明的 VOLUME | 推荐挂载目标 |
|---|---|---|---|
| 17 及以下 | /var/lib/postgresql/data | /var/lib/postgresql/data | /var/lib/postgresql/data |
| 18 及以上 | /var/lib/postgresql/18/docker | /var/lib/postgresql | /var/lib/postgresql |
也就是说:
- 17 及以下:挂
/var/lib/postgresql/data - 18 及以上:挂
/var/lib/postgresql
如果路径挂反了,就非常容易触发匿名卷。
5. 正确配置:PostgreSQL 18 应该怎么写
5.1 容易出问题的旧写法
下面这种是 PostgreSQL 18 里最容易踩坑的写法:
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
volumes:
- app_pgdata:/var/lib/postgresql/data
volumes:
app_pgdata:
问题在于:你挂的是旧路径。
5.2 推荐写法一:让 PostgreSQL 18 使用默认新路径
这是更贴近官方说明的写法:
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
volumes:
- app_pgdata:/var/lib/postgresql
volumes:
app_pgdata:
name: app_pgdata
这时 PostgreSQL 18 默认会把数据写到:
/var/lib/postgresql/18/docker
而这个目录位于你挂载的 /var/lib/postgresql 之下,数据就会稳定落在命名卷里。
5.3 推荐写法二:显式声明 PGDATA
如果你希望配置更直白,可以显式写出 PGDATA:
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
PGDATA: /var/lib/postgresql/18/docker
volumes:
- app_pgdata:/var/lib/postgresql
volumes:
app_pgdata:
name: app_pgdata
这样有两个好处:
- 读配置的人一眼就知道 PostgreSQL 实际数据目录在哪
- 日后排查时更容易对照
current_setting('data_directory')
5.4 如果你还在用 PostgreSQL 17 或更早
那就不要套用 PostgreSQL 18 的挂法。
17 及以下推荐:
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
volumes:
- app_pgdata:/var/lib/postgresql/data
volumes:
app_pgdata:
name: app_pgdata
6. 已经出现随机卷了,怎么排查哪个卷才是真数据卷
如果你已经遇到了这个问题,不要先删卷,先确认“谁才是真正的数据卷”。
6.1 先看容器挂载
docker inspect pg18-demo --format '{{json .Mounts}}'
重点看:
- 哪个卷挂到了
/var/lib/postgresql - 哪个卷挂到了
/var/lib/postgresql/data
如果 PostgreSQL 18 里你看到了:
- 命名卷挂
/var/lib/postgresql/data - 匿名卷挂
/var/lib/postgresql
那通常匿名卷更值得怀疑。
6.2 再看 PostgreSQL 实际数据目录
docker exec -it pg18-demo sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT current_setting('\''data_directory'\'');"'
如果结果是:
/var/lib/postgresql/18/docker
那就基本可以确认:
- 真正的数据在挂到
/var/lib/postgresql的那个卷里 - 如果它是随机名卷,那真数据就在随机卷里
6.3 再用卷列表交叉确认
docker volume ls
docker volume inspect app_pgdata
docker volume inspect 8c5c1a4f0b8f1d6e4f9b2a7c3d1e9ab6e0b4f2d3f5a6c7d8e9f0a1b2c3d4e5f6
你可以重点看:
Mountpoint- 哪个卷被容器挂载到了真实
data_directory对应路径
7. 如果已经变成随机卷了,怎么“改名”
先说结论:
7.1 Docker 卷没有通用的 CLI “重命名”命令
Docker CLI 的 docker volume 子命令里,官方列出的主要操作是:
createinspectlsprunermupdate
也就是说,没有一个稳定通用的 docker volume rename 工作流。
所以现实里的“改名”,正确做法通常不是直接改名,而是:
- 新建一个你想要的命名卷
- 把旧卷里的数据复制过去
- 修改 Compose 配置指向新卷
- 重建容器
- 验证无误后再删除旧卷
严格说,这叫“迁移卷”,不是“给卷改名”。
7.2 安全迁移步骤
下面假设:
- 旧随机卷:
8c5c1a4f0b8f... - 新目标命名卷:
app_pgdata - 容器名:
pg18-demo
第一步:停库
先停止数据库容器,避免复制过程中数据还在变化。
docker compose stop db
或者:
docker stop pg18-demo
第二步:创建目标命名卷
docker volume create app_pgdata
第三步:把旧卷数据复制到新卷
推荐用一个临时容器做卷到卷复制:
docker run --rm \
-v 8c5c1a4f0b8f1d6e4f9b2a7c3d1e9ab6e0b4f2d3f5a6c7d8e9f0a1b2c3d4e5f6:/from \
-v app_pgdata:/to \
alpine sh -lc 'cd /from && tar cf - . | (cd /to && tar xpf -)'
这个做法的优点是:
- 不依赖宿主机直接操作 Docker 数据目录
- 不要求你手动去
/var/lib/docker/volumes/...下复制文件 - 一般比手抄宿主机路径安全
第四步:修改 Compose 配置到 PostgreSQL 18 正确挂法
改成这样:
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
PGDATA: /var/lib/postgresql/18/docker
volumes:
- app_pgdata:/var/lib/postgresql
volumes:
app_pgdata:
name: app_pgdata
第五步:重新启动容器
docker compose up -d
第六步:验证是否已经切到命名卷
先看容器挂载:
docker inspect pg18-demo --format '{{json .Mounts}}'
再看 PostgreSQL 真实数据目录:
docker exec -it pg18-demo sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT current_database(), current_setting('\''data_directory'\'');"'
还可以再看业务数据是否还在,例如表数量:
docker exec -it pg18-demo sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '\''public'\'';"'
第七步:确认无误后删除旧随机卷
只有在你确认:
- 数据完整
- 容器已经稳定使用新命名卷
- 备份已做好
之后,再删除旧随机卷:
docker volume rm 8c5c1a4f0b8f1d6e4f9b2a7c3d1e9ab6e0b4f2d3f5a6c7d8e9f0a1b2c3d4e5f6
8. 一个实用排查脚本
如果你怀疑自己又踩到了这个坑,可以用下面这组命令快速检查:
docker compose ps
docker compose logs db --tail 60
docker inspect pg18-demo --format '{{json .Mounts}}'
docker exec -it pg18-demo sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT current_database(), current_setting('\''data_directory'\'');"'
看这两点就够了:
- 容器实际挂载了哪些卷
- PostgreSQL 的
data_directory到底指向哪里
只要这两者对不上,你就不该继续删卷或重建容器。
9. 建议:怎么避免以后再踩
9.1 不要把 postgres:latest 直接扔进生产
如果你之前是:
image: postgres:latest
那你很容易在不知情的情况下从 17 跳到 18。
更稳妥的做法是固定大版本:
image: postgres:18-alpine
或者:
image: postgres:17-alpine
9.2 升级大版本前,先核对镜像文档里的 PGDATA 和 VOLUME
数据库镜像升级时,不要只看 PostgreSQL 本身的 release note,也要看 Docker 镜像说明。
因为这类坑经常不是数据库语义变了,而是 镜像封装方式变了。
9.3 命名卷最好显式写 name
比如:
volumes:
app_pgdata:
name: app_pgdata
这样至少能避免因为 Compose project 名变化,生成一套新的卷前缀,进一步增加混乱。
9.4 每次部署后,都检查一次真实数据目录
比起“我觉得配对了”,更可靠的是直接查:
docker exec -it pg18-demo sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT current_setting('\''data_directory'\'');"'
这是判断数据库真正写到哪里的最硬证据之一。
9.5 不要在没确认之前就执行 docker volume prune
匿名卷一旦参与了真实数据存储,prune 就可能变成误删工具。
在你确认卷用途前,不要着急做清理。
9.6 写一条团队约定
如果团队里有人会维护 Docker Compose,最好明确写进文档:
- PostgreSQL 17 及以下挂
/var/lib/postgresql/data - PostgreSQL 18 及以上挂
/var/lib/postgresql - 升级镜像大版本前先检查数据目录和卷挂载策略
这比口头提醒有效得多。
10. 最后的判断标准
以后再遇到“指定命名卷后又冒出随机卷”,不要先问:
- 为什么数据库名没生效
- 为什么 Compose 多建了卷
先问这三个问题:
- 这个镜像当前版本的默认
PGDATA是什么? - 这个镜像声明的
VOLUME是什么? - 我挂载的路径,是不是正好覆盖了镜像真正使用的数据目录上层?
只要这三件事想清楚,大多数“随机卷”问题都能快速定位。
参考资料
- Docker 官方 Postgres 镜像说明 https://hub.docker.com/_/postgres/
- Docker 官方卷文档 https://docs.docker.com/engine/storage/volumes/
- Docker CLI
docker volume官方参考 https://docs.docker.com/reference/cli/docker/volume/ - Docker CLI
docker volume create官方参考 https://docs.docker.com/reference/cli/docker/volume/create/ - Docker 官方 Postgres 镜像源码仓库 https://github.com/docker-library/postgres