DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

PostgreSQL 18 挂载 /var/lib/postgresql/data 后启动失败

官方 PostgreSQL 18 镜像改用主版本数据子目录,旧挂载可能报 runc 错误或 unused mount/volume。确认实际卷与数据版本,修正空目录部署,并区分挂载调整和数据库升级。

把官方镜像换成 postgres:18 后,原来能启动的数据库容器卡在挂载阶段,错误中出现:

error mounting ... at "/var/lib/postgresql/data": ... no such file or directory

也可能已经进入镜像启动脚本,日志却指出:

/var/lib/postgresql/data (unused mount/volume)

看到这两种信息,先核对挂载目标。官方 PostgreSQL 镜像从 18 开始,把默认 PGDATA 改成 /var/lib/postgresql/18/docker,声明的卷位置改成 /var/lib/postgresql。照搬 17 的 /var/lib/postgresql/data 配置,可能挂不上,也可能挂到了数据库根本不用的位置。

这篇处理的是官方 postgres 镜像默认目录布局下的故障。自建镜像、主动覆盖 PGDATA 或使用 postgres -D 的部署,要按实际数据目录判断,不能只按主版本替换路径。

同一个旧挂载,新旧镜像的报错可能不同

空数据目录挂到 /var/lib/postgresql/data,在 17.6 下能够启动,换到 18 后却可能在 runc 挂载阶段失败。18 的默认布局应挂载父目录 /var/lib/postgresql,让数据库使用下面的主版本子目录;直接挂载内部目录与父目录挂载的升级安排不同。

早期 18 的 Debian 构建在旧路径放置了一个使旧挂载失败的符号链接。2025 年 10 月 22 日起的相关调整移除了这个链接并增加检测,让需要改卷路径的部署得到更明确的提示。因此,procfd 或 no such file or directory 不能当成所有 18 镜像都必然出现的报错。

当前启动脚本会在默认新布局下检查旧数据和旧路径挂载;即使旧挂载没有数据库,也可能被列为 unused mount/volume。这条信息不证明目录里真的有旧数据库,更不说明可以删除它。

查看实际容器的挂载

在原 Compose 项目目录执行,下面假设数据库服务名为 db:

docker compose ps -a db
cid="$(docker compose ps -a -q db)"
docker inspect "$cid" --format '{{.Config.Image}}'
docker inspect "$cid" --format '{{json .Mounts}}'

ps -a 包含已退出的容器,-q 取得 ID;这里只适用于该服务有一个容器的情况。ID 为空时先确认项目名、Compose 文件和服务名,不要继续检查别的容器。启动时用了 -p 或多份 -f 文件,检查时也带上同一组参数。

从挂载列表找 Destination 为 /var/lib/postgresql/data 的条目,同时记录 Type、卷的 Name 或绑定目录的 Source。它们决定后面要保护哪一份数据。镜像字段若仍是 postgres:latest,只能说明创建容器时采用的标签,不能据此确认实际主版本;还要核对部署记录及所用镜像。

再确认配置有没有覆盖 PGDATA、数据目录启动参数或初始化脚本。容器能运行时,可以从数据库查询实际目录;容器尚未启动时,不能把 docker exec 失败当成新的数据库错误。

空目录的新部署:改为挂载父目录

只有确认数据目录为空、没有需要保留的数据库时,才可直接按新布局初始化。还要检查挂载列表中的其他卷,尤其是 /var/lib/postgresql 对应的卷。旧挂载目录为空,不代表数据库从未写入别的卷;存在业务数据时,应先找到实际集群。

官方镜像在 18 及以上要求挂载 /var/lib/postgresql,数据库写入下面的主版本子目录。它也允许旧版本显式采用这种布局,但默认的 17 及以下仍有自己的旧挂载要求,不能把所有版本都统一替换。

下面是独立验证项目的完整 compose.pg18.yaml。应放在新的测试目录,确认项目名 pg18-mount-check 和其数据卷从未承载业务数据;密码是占位值,启动前换成自己的值。示例不发布宿主机端口:

services:
  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: "REPLACE_WITH_A_STRONG_LOCAL_PASSWORD"
      POSTGRES_DB: appdb
    volumes:
      - pg18_data:/var/lib/postgresql

volumes:
  pg18_data:

启动这个独立项目:

docker compose -p pg18-mount-check -f compose.pg18.yaml up -d db
docker compose -p pg18-mount-check -f compose.pg18.yaml logs --tail=100 db

这个项目创建新的数据库,原目录的数据不会自动迁入。修复原来的空目录部署时,保留原来已确认的卷来源或绑定目录,只改容器内挂载目标;不要顺手改项目名、卷名或宿主机源路径,否则容易连到另一份新数据库。先保存旧配置,修改后用同一项目的 docker compose up -d --no-deps db 重建受影响的服务。重建会中断连接,应安排在可接受的时间;单纯 restart 不会把已存在容器的挂载变成新配置。

目录里已有数据库:改挂载不等于升级

先保留原数据和配置,不执行 down -v、docker volume rm,也不删除数据库目录。检查已记录的卷来源里是否有 PG_VERSION、base、global、pg_wal 等数据库文件,并按实际集群读取 PG_VERSION。数据库文件布局说明,这个文件记录主版本号。日志中的“旧数据”提示不能替代这一步。

如果 PG_VERSION 是 17,而你刚把镜像换成 18,问题已经涉及主版本升级。移动文件、改 PGDATA,或者把旧集群放进名为 18/docker 的目录,都不会转换数据库格式。应先核实旧集群状态和备份,再选择逻辑导出与恢复、pg_upgrade 等官方支持的升级路线。

若选择 pg_upgrade,检查阶段报 could not load library,可继续检查目标版本的扩展库与 adminpack 兼容问题。改挂载路径不会补齐扩展库,也不会恢复目标版本已移除的扩展。

若选择逻辑导出,而 pg_dump 报 server version mismatch,先核对实际执行备份的客户端版本,包括宿主机和备份容器中的版本。

如果实际数据已经是 18,只是以前使用了自定义目录,也不能当空目录重新初始化。先做好业务备份,在数据库正常停机后设计目录调整,保留完整文件、权限和旧配置;恢复前确认新的挂载和 PGDATA 指向同一个集群。不要在容器启动期间移动它正在写入的文件。

使用父目录挂载是为了在同一个文件系统下容纳主版本目录,便于安排升级;它不会自动执行 pg_upgrade,也不代表所有升级都应该使用 --link。具体方法的备份、停机和回退要求需要单独确认。

启动成功后,还要确认数据写进了目标卷

对上面的独立项目,使用初始化管理员查询目录和版本:

docker compose -p pg18-mount-check -f compose.pg18.yaml exec -T db \
  psql -U postgres -d appdb \
  -c 'SHOW data_directory;' -c 'SHOW server_version;'

没有覆盖配置时,18 镜像的目录应为 /var/lib/postgresql/18/docker。再检查该容器的挂载列表,确认 /var/lib/postgresql 对应你预期的命名卷或宿主机目录。能连接数据库只能证明服务起来了,不能证明写入位置正确。

原部署使用自己的初始化用户、数据库名和认证配置,不一定存在可登录的 postgres 角色;查询时按实际管理员身份替换。若本地连接也要求密码,按现有安全方式提供凭据,不要把密码贴进排错记录。

持久化验收应在隔离测试环境完成:写入一条可识别的测试记录,再使用同一项目和卷配置重建容器,检查记录仍在。原有业务则核对已知表、记录及应用连接结果,避免把一个空的新集群误认为修复成功。

验收完成前,保留原镜像引用、旧 Compose 文件和原数据副本。只改挂载的回退与主版本升级的回退是两回事;新集群已经接收业务写入后,直接切回旧卷会丢掉这些写入,需要先处理数据同步和一致性。