Docker 基础体系 · 第 52/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
数据库运行在 Docker:持久化、初始化、备份、资源和生产边界
数据库放进容器后,真正需要解决的不是“如何启动一个 PostgreSQL 容器”,而是以下几个状态问题:
- 数据究竟存在哪里,容器删除后是否还存在;
- 初始化脚本什么时候执行,为什么改了脚本却没有效果;
- 正在写入的数据库怎样备份,备份是否可恢复;
- 容器看到的 CPU、内存、共享内存和磁盘是否足够;
- 哪些能力适合开发环境,哪些边界决定了生产环境不能只依赖一个 Docker 容器。
本文以 Linux 容器和现代 Docker Engine、BuildKit、Compose 规范为背景,使用 PostgreSQL 作为主要示例。不同数据库镜像的目录、初始化变量和备份工具可能不同,因此示例中的 PostgreSQL 路径和命令不能机械地套用到 MySQL、MariaDB 或 MongoDB。
一、先区分三个对象:镜像、容器和数据库数据
1. 镜像是模板,不是数据库实例
镜像包含:
- 数据库服务器程序;
- 默认配置;
- 启动脚本;
- 可能存在的初始化文件;
- 文件系统中的静态内容。
镜像本身通常是只读分层。容器启动后,Docker 会在镜像层之上增加一个容器可写层。数据库进程在容器中创建或修改的文件,如果没有额外挂载存储,就会进入这个可写层。
因此,下面这种运行方式通常只适合临时测试:
docker run --name pg \
-e POSTGRES_PASSWORD=devpass \
-e POSTGRES_DB=appdb \
postgres:16
数据库文件写在容器文件系统中。执行:
docker rm -f pg
容器及其可写层一起被删除,数据库数据也随之丢失。
docker stop 只停止容器,不删除可写层;但这不代表容器可写层适合作为长期数据库存储。容器重建、清理脚本、迁移和回滚都会使这种数据保存方式变得脆弱。
2. 容器是进程和运行状态,不是数据本身
容器可以被停止、删除和重新创建。只要数据库数据位于独立的 Volume 或主机目录中,就可以用新容器重新挂载同一份数据:
数据库进程
│
▼
容器文件系统视图
│
▼
/var/lib/postgresql/data
│
▼
Volume 或 Bind Mount
│
▼
Linux 主机上的持久化存储
这里的关键是“挂载点”。容器内的 /var/lib/postgresql/data 只是数据库进程看到的路径;真正的数据可能位于 Docker 管理的 Volume,也可能位于主机指定目录。
3. 数据库的持久化要求比“文件没有消失”更严格
数据库持久化至少包含三层含义:
- 生命周期持久化:容器删除后,数据仍然存在;
- 崩溃恢复:数据库进程或主机突然停止后,WAL、日志和数据页能够用于恢复;
- 备份持久化:主存储损坏、误删或主机丢失后,仍有独立副本可恢复。
Volume 主要解决第一层,并为第二层提供存储位置;它不会自动解决第三层。一个没有备份的 Volume 仍然是单份数据。
二、Volume、Bind Mount 和 tmpfs 的差异
1. Named Volume:Docker 管理的持久化存储
Compose 可以声明一个命名卷:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
启动:
docker compose up -d
查看卷:
docker volume ls
docker volume inspect <实际卷名>
在默认 Compose 项目中,卷名通常会带项目名前缀,例如:
myproject_dbdata
这里有一个重要的生命周期规则:
docker compose down
默认停止并删除容器、网络,但通常保留 Named Volume。
而:
docker compose down -v
会删除 Compose 声明的卷。这个命令对开发环境很方便,但对数据库来说可能造成不可逆的数据删除。
Named Volume 的优点是:
- 不需要把主机目录结构暴露给项目;
- Docker 负责创建和挂载;
- 更容易在容器重建后复用;
- 可以通过 Volume Driver 接入某些外部存储。
但 local Volume 默认仍然只是当前 Docker 主机上的本地目录。它不是跨主机复制,也不是高可用存储。
2. Bind Mount:把主机目录直接映射进去
Bind Mount 示例:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
volumes:
- ./postgres-data:/var/lib/postgresql/data
它的语义是:主机上的 ./postgres-data 直接成为容器内的数据目录。
Bind Mount 适合以下场景:
- 需要明确控制数据存放位置;
- 需要使用主机备份系统、快照系统;
- 数据目录必须与某个主机路径绑定;
- 本地开发中希望直接查看文件。
但它会引入更多主机耦合:
- 主机目录的属主和权限必须适合容器内数据库用户;
- 相对路径依赖 Compose 项目目录;
- 目录可能被误删、误覆盖;
- Linux、Docker Desktop 和远程 Docker Engine 的路径语义不同。
如果主机目录非空,挂载后会覆盖容器原本同一路径下的内容。这个现象称为挂载遮蔽:路径仍然存在,但镜像层中原有的文件从容器视图中不可见。
3. tmpfs:内存中的临时文件系统
Linux 容器可以挂载 tmpfs:
docker run --rm \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
alpine sh
tmpfs 的数据保存在内存中,容器停止或删除后消失。它适合:
- 临时文件;
- 对磁盘落盘敏感的中间数据;
- 明确不需要持久化的缓存。
它不适合数据库主数据目录。把 PostgreSQL 的数据目录放在 tmpfs 上,容器重启后数据库自然会丢失。tmpfs 也会消耗主机内存,不能把它当成“无限制的快磁盘”。
4. 一个选择决策
| 类型 | 容器删除后数据 | 主机路径是否显式指定 | 典型用途 |
|---|---|---|---|
| Named Volume | 保留 | 否 | 数据库主数据、Docker 管理的持久化 |
| Bind Mount | 保留 | 是 | 主机目录、备份系统、明确路径 |
| tmpfs | 不保留 | 否 | 临时文件、缓存、敏感的短期数据 |
| 容器可写层 | 容器删除后丢失 | 否 | 临时实验,不适合数据库 |
真正的判断条件不是“Volume 一定好”或“Bind Mount 一定好”,而是:
数据库数据
→ 是否独立于容器生命周期
→ 是否位于可靠存储
→ 是否有独立备份
→ 是否经过恢复验证
三、初始化:空数据目录只会被初始化一次
官方数据库镜像通常包含一个入口脚本。以 PostgreSQL 官方镜像为例,启动过程可以抽象为:
容器启动
│
├─ 检查数据目录是否已初始化
│
├─ 如果为空:
│ ├─ 执行 initdb
│ ├─ 设置 POSTGRES_* 环境变量对应的初始对象
│ ├─ 执行 /docker-entrypoint-initdb.d 下的脚本
│ └─ 停止临时数据库并启动正式服务器
│
└─ 如果已有 PG_VERSION:
└─ 直接启动已有数据库,不重复初始化
PG_VERSION 是 PostgreSQL 数据目录已经初始化的一个重要标志。具体镜像版本的入口脚本可能有所变化,但“只对空数据目录执行初始化”是这类镜像的核心行为。
1. 一个完整的 Compose 初始化示例
目录结构:
demo/
├── compose.yaml
└── init/
├── 01-schema.sql
└── 02-seed.sh
compose.yaml:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
volumes:
- dbdata:/var/lib/postgresql/data
- ./init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 5s
retries: 10
volumes:
dbdata:
init/01-schema.sql:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
init/02-seed.sh:
#!/bin/sh
set -eu
psql -v ON_ERROR_STOP=1 \
--username "$POSTGRES_USER" \
--dbname "$POSTGRES_DB" <<'SQL'
INSERT INTO users (email)
VALUES ('alice@example.com')
ON CONFLICT (email) DO NOTHING;
SQL
赋予脚本执行权限:
chmod +x init/02-seed.sh
docker compose up -d
docker compose logs db
预期可以看到初始化日志,随后数据库进入可连接状态。验证:
docker compose exec db \
psql -U app -d appdb -c 'SELECT id, email FROM users;'
可能输出:
id | email
----+-------------------
1 | alice@example.com
(1 row)
这里每一步成立的原因是:
dbdata第一次使用时为空;- 官方入口脚本执行
initdb; - SQL 文件和可执行 Shell 脚本按文件名排序执行;
- 初始化完成后,数据写入 Volume;
- 后续重启时,目录不再为空,脚本不会再次运行。
2. 为什么修改初始化脚本没有效果
执行:
docker compose restart db
或:
docker compose up -d
不会重新执行初始化脚本,因为 dbdata 已经包含数据库集群。
如果只是开发环境,想从头初始化,可以:
docker compose down -v
docker compose up -d
这会删除 Volume 中的数据。生产环境不能用这种方式“修复初始化问题”,因为它的实际效果是删除数据库。
更安全的诊断顺序是:
docker compose ps
docker compose logs db
docker compose exec db \
psql -U app -d appdb -c '\dt'
如果需要把已有数据库升级到新结构,应使用版本化迁移工具,例如应用迁移框架或专门的数据库迁移流程,而不是再次依赖 /docker-entrypoint-initdb.d。
3. 初始化脚本中的失败状态
初始化脚本不是事务边界的统一保证。一个 SQL 文件可以使用事务,但多个文件之间不一定构成一个整体事务;Shell 脚本执行外部命令也可能只完成一半。
例如:
set -eu
psql ... -f /path/01.sql
psql ... -f /path/02.sql
如果 02.sql 失败,01.sql 可能已经提交。下次容器启动时,数据目录已经初始化,两个脚本通常都不会自动重跑。
因此,初始化脚本应当:
- 在 SQL 中使用
ON CONFLICT等幂等语句; - 使用
psql -v ON_ERROR_STOP=1,避免 SQL 错误后继续执行; - 把不可重复的操作放入明确的迁移系统;
- 把初始化失败视为需要人工检查的状态,而不是简单重启。
四、Volume 初始化复制与权限问题
1. 空 Volume 可能获得镜像目录的初始内容
Docker 在某些挂载场景下,会将容器镜像中挂载目标路径的已有内容复制到一个空 Volume 中。数据库官方镜像通常也依赖入口脚本创建数据目录。
但这不等于“镜像里的数据库数据会自动成为可靠备份”。复制只发生在特定的空 Volume 初始化阶段,后续镜像升级不会自动把新文件合并进已有数据目录。
数据库数据目录的升级必须由数据库自己的升级工具、入口逻辑或迁移流程完成。
2. 权限问题的本质
容器内的 postgres 用户是一个 Linux 用户,具有 UID 和 GID。Bind Mount 使用的是主机实际文件权限;容器内看到的 UID 数字需要对主机目录有写权限。
诊断命令:
docker compose exec db id postgres
docker compose exec db ls -ld /var/lib/postgresql/data
在主机上检查:
ls -ld ./postgres-data
stat -c '%u:%g %a %n' ./postgres-data
如果使用 Bind Mount,而目录归属不正确,日志可能出现:
initdb: error: could not change permissions of directory
Permission denied
修复方式必须基于实际镜像 UID,而不是盲目假设。可以先获取容器中的 UID:
PG_UID=$(docker run --rm postgres:16 id -u postgres)
PG_GID=$(docker run --rm postgres:16 id -g postgres)
sudo chown -R "$PG_UID:$PG_GID" ./postgres-data
然后再启动容器。
Named Volume 通常更容易处理权限,因为它由 Docker 创建,并且官方镜像入口脚本可以在初始化阶段调整目录权限。但这只是减少主机目录管理问题,不代表权限、SELinux 或存储驱动问题永远不存在。
五、数据库为什么通常不应把主数据放在容器可写层
容器可写层通常由 OverlayFS 等存储驱动实现。数据库会反复修改数据页、WAL、临时文件和目录元数据,而写时复制和分层文件系统会增加额外的路径复杂度。
更重要的是,容器可写层与容器生命周期绑定:
写入容器可写层
├─ stop:暂时保留
├─ start:仍可见
├─ rm:删除
└─ recreate:得到新的空可写层
Volume 或 Bind Mount 的数据流则是:
写入数据库目录
→ 挂载存储
→ 数据独立于容器对象
→ 新容器重新挂载
这不是说 OverlayFS 在技术上完全不能保存文件,而是数据库需要清晰、可管理、可备份的数据生命周期。将数据库数据目录放在专用 Volume 或主机存储中,能把“容器版本变化”和“数据版本变化”分开。
BuildKit 也不能改变这个事实。Dockerfile 中的 RUN 发生在镜像构建阶段,BuildKit 的缓存挂载是构建缓存,不是运行时数据库 Volume:
# 这是构建缓存,不是生产数据库数据
RUN --mount=type=cache,target=/root/.cache \
some-build-command
构建结束后,运行中的数据库不会自动使用这个缓存来持久化数据。
六、备份:复制文件不等于得到一致备份
数据库正在并发写入时,数据目录包含多种相互关联的状态:
- 数据文件中的数据页;
- WAL 或 redo log;
- 控制文件;
- 临时文件;
- 元数据和目录结构。
设数据库在时间 的一致状态为:
其中:
- 是数据页集合;
- 是 WAL 或重做日志状态;
- 是控制文件和元数据状态。
一个可恢复备份需要满足:
直观地说,备份中的数据文件、日志和控制信息必须能够对应某个可恢复的数据库状态。只是在数据库运行时执行:
tar czf db.tar.gz /var/lib/postgresql/data
不能自动保证这个条件成立。复制过程可能读到不同时间点的数据页,也可能漏掉尚未复制的文件。
1. 逻辑备份:使用数据库协议导出
PostgreSQL 的逻辑备份使用 pg_dump:
mkdir -p backups
docker compose exec -T db \
pg_dump \
-U app \
-d appdb \
--format=custom \
> backups/appdb-$(date +%Y%m%d-%H%M%S).dump
这里的 -T 很重要:它关闭伪终端,避免二进制或重定向输出被终端处理。> 是宿主机 Shell 执行的,因此备份文件直接落在主机的 backups 目录,而不是容器内部。
--format=custom 生成 PostgreSQL 自定义格式,适合使用 pg_restore 选择性恢复。验证文件不是空文件:
ls -lh backups/*.dump
docker compose exec -T db \
pg_restore --list < backups/appdb-20240101-120000.dump | head
pg_dump 使用数据库的一致性读取机制,可以在数据库继续提供服务时导出一个逻辑一致的时间点视图。它不会阻止其他事务写入,但导出的是逻辑对象和数据,不是原始数据目录。
角色、表空间等全局对象不完全由单个数据库的 pg_dump 覆盖,可以单独导出:
docker compose exec -T db \
pg_dumpall -U app --globals-only \
> backups/globals.sql
恢复到新数据库的一个完整流程可以是:
docker compose exec -T db \
createdb -U app restoredb
cat backups/appdb-20240101-120000.dump | \
docker compose exec -T db \
pg_restore -U app -d restoredb --exit-on-error
验证:
docker compose exec db \
psql -U app -d restoredb -c \
'SELECT count(*) FROM users;'
逻辑备份的取舍是:
- 优点:与底层文件系统解耦,可跨主机、跨部分版本迁移;
- 缺点:恢复需要重新创建对象和数据,速度通常受数据量及索引重建影响;
- 边界:它不是任意时刻的物理快照,也不能代替完整的角色、扩展和外部对象管理。
2. 文件级 Volume 备份:先停止数据库
如果目标是打包 Volume 中的原始数据目录,最容易解释和验证的一种方法是先停止数据库:
docker compose stop db
假设 Compose 文件中使用固定卷名:
volumes:
dbdata:
name: project-dbdata
此时可以备份:
mkdir -p backups
docker run --rm \
-v project-dbdata:/from:ro \
-v "$PWD/backups:/to" \
alpine:3.20 \
sh -c 'tar czf /to/project-dbdata.tgz -C /from .'
这里的组件关系是:
project-dbdata:/from:ro:把数据库 Volume 只读挂到临时容器;"$PWD/backups:/to":把宿主机备份目录挂到临时容器;tar -C /from .:打包 Volume 根目录内容;--rm:备份容器完成后自动删除。
停止数据库的原因是建立一个简单的静态状态:
停止数据库
→ 不再产生新的数据页、WAL 和目录变更
→ tar 读取完整数据目录
→ 启动数据库
恢复时不要直接覆盖一个正在运行的数据目录。可以先将备份解压到临时卷,验证目录,再切换容器:
docker compose stop db
docker volume create project-dbdata-restore
docker run --rm \
-v project-dbdata-restore:/to \
-v "$PWD/backups:/from:ro" \
alpine:3.20 \
sh -c 'tar xzf /from/project-dbdata.tgz -C /to'
docker run --rm \
-v project-dbdata-restore:/data:ro \
alpine:3.20 \
sh -c 'test -f /data/PG_VERSION && cat /data/PG_VERSION'
确认数据目录存在后,需要让 Compose 使用这个卷。实际切换可以通过修改卷声明、使用固定外部卷,或在一次性恢复环境中挂载该卷。不要在未确认备份有效前执行:
docker volume rm project-dbdata
如果数据库在备份期间没有停止,文件级备份可能仍然在 PostgreSQL 的崩溃恢复能力范围内,但这依赖数据库版本、存储语义、WAL 是否完整以及备份工具的正确用法。仅凭“tar 命令成功”不能推出“数据库一定能恢复”。
3. 快照、物理备份和逻辑备份不是同一个概念
在支持快照的存储上,可以对 Volume 底层存储做快照;但快照的一致性取决于:
- 数据库是否已暂停写入或执行了适当的 checkpoint;
- 存储是否保证写入顺序和崩溃一致性;
- 快照是否包含必要的 WAL;
- 恢复时数据库是否能完成 crash recovery。
物理备份适合大数据库和快速恢复,但通常与数据库版本、架构、扩展和存储布局关联更强。逻辑备份更适合迁移、选择性恢复和小型数据库。
无论选择哪一种,备份系统至少应记录:
备份时间
数据库版本
镜像版本或摘要
备份类型
数据范围
加密状态
校验值
保留期限
恢复步骤
最近一次恢复验证结果
把备份文件留在同一台主机的另一个目录,不能抵御主机磁盘损坏;把备份留在同一个 Volume 中,也不能抵御 Volume 被误删。
七、恢复演练:备份的完成条件是“能恢复”
备份命令成功只说明命令没有返回错误。恢复验证应当在独立环境中进行,避免破坏线上数据。
一个逻辑备份验证流程可以使用临时 Compose 项目:
mkdir -p restore-test
cd restore-test
创建临时数据库:
docker run -d --rm \
--name pg-restore-test \
-e POSTGRES_PASSWORD=testpass \
-e POSTGRES_DB=restoredb \
postgres:16
等待数据库就绪:
until docker exec pg-restore-test \
pg_isready -U postgres -d restoredb >/dev/null 2>&1
do
sleep 1
done
恢复:
docker exec -i pg-restore-test \
pg_restore -U postgres -d restoredb --exit-on-error \
< ../backups/appdb-20240101-120000.dump
验证表和数据:
docker exec pg-restore-test \
psql -U postgres -d restoredb -c '\dt'
docker exec pg-restore-test \
psql -U postgres -d restoredb -c \
'SELECT count(*) FROM users;'
完成后删除临时容器:
docker rm -f pg-restore-test
恢复演练应验证的不只是“能启动”,还包括:
- 关键表是否存在;
- 行数或业务校验值是否符合预期;
- 角色和权限是否恢复;
- 应用是否能连接;
- 扩展是否可用;
- 恢复耗时是否满足业务的 RTO。
其中:
- RPO 表示最多可以丢失多长时间的数据;
- RTO 表示故障后最多允许多长时间恢复服务。
例如,只有每天凌晨执行一次逻辑备份,理论上 RPO 可能接近 24 小时;即使恢复命令只需 10 分钟,RTO 也可能满足,但 RPO 不满足业务要求。备份类型和频率必须由这两个目标反推,而不能只看“是否有备份文件”。
八、资源限制:数据库看到的是 cgroup 里的世界
Docker 在 Linux 上通过 cgroup 对容器施加资源约束。数据库进程看到的 CPU 和内存,并不一定等于主机总资源。
一个 Compose 示例:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
volumes:
- dbdata:/var/lib/postgresql/data
cpus: 2.0
mem_limit: 2g
mem_reservation: 1g
pids_limit: 512
shm_size: 256m
这些字段的意义是:
cpus: 2.0:限制 CPU 使用上限,具体调度表现由 cgroup 和主机负载决定;mem_limit: 2g:容器可用内存上限;mem_reservation: 1g:软性内存需求,不能当成绝对保留;pids_limit: 512:限制进程和线程数量;shm_size: 256m:设置容器/dev/shm大小。
Compose 实际支持的资源字段和非 Swarm、Swarm 部署方式之间存在差异。deploy.resources 在不同 Compose 实现和部署目标上的处理不能一概而论,因此生产前应使用目标环境的 docker compose config 和运行时检查确认,而不是只看 YAML。
检查容器实际配置:
docker inspect <container_id> \
--format '{{json .HostConfig.Memory}} {{json .HostConfig.NanoCpus}}'
查看运行时资源使用:
docker stats
1. 内存限制与数据库内部内存不是同一层
PostgreSQL 的内存由多个部分构成:
shared_buffers;- 每个连接的工作内存;
- 排序、哈希和维护操作使用的内存;
- 后端进程、扩展和操作系统页缓存;
- 临时文件和共享内存。
因此,不能简单地设置:
shared_buffers = 容器内存上限
因为 PostgreSQL 还需要其他内存,且连接数越高,并发工作内存的总和越难估计。一个粗略的容量约束可以写成:
其中:
- 是共享缓冲区;
- 是并发连接数;
- 是单连接可能使用的工作内存;
- 是后台进程和扩展开销;
- 是页缓存等额外消耗。
这不是 PostgreSQL 的精确内存公式,而是说明为什么容器内存上限不能只减去一个配置项后就认为安全。
当 cgroup 内存不足时,可能出现:
- 容器被内核 OOM killer 终止;
- 数据库日志中断;
docker inspect显示 OOMKilled;- 应用看到连接断开;
- 数据库下次启动执行崩溃恢复。
诊断:
docker inspect <container_id> \
--format '{{.State.OOMKilled}} {{.State.ExitCode}}'
docker compose logs --tail=200 db
2. /dev/shm 太小会造成看似无关的失败
Docker 容器默认的 /dev/shm 往往较小。某些数据库并发查询、并行执行或扩展会使用 POSIX 共享内存;共享内存不足时,错误可能类似:
could not resize shared memory segment
No space left on device
这不一定表示数据库 Volume 磁盘已满,可能只是 /dev/shm 满了。分别检查:
docker compose exec db df -h
docker compose exec db df -h /dev/shm
shm_size 只能解决共享内存空间问题,不能替代数据库内存规划。
3. CPU 限制会改变延迟和恢复时间
CPU 配额不足时,数据库可能仍然“正常运行”,但表现为:
- 查询延迟增加;
- checkpoint、VACUUM 或索引创建变慢;
- 备份和恢复时间变长;
- 高并发下请求排队。
CPU 限制通常比内存 OOM 更隐蔽,因为进程不会立即退出。监控中应区分:
CPU 使用率高
CPU 配额被 throttled
数据库锁等待
磁盘 I/O 等待
这些现象的处理方式不同。单纯增加容器 CPU 不一定能解决锁竞争或慢磁盘。
4. 磁盘空间和 I/O 是数据库资源的一部分
检查 Volume 所在文件系统:
docker system df
df -h
df -i
df -h 检查容量,df -i 检查 inode。数据库可能在还有可用容量时因为 inode、WAL、临时文件或日志目录耗尽而写入失败。
Docker 的资源限制不自动提供高质量数据库 I/O。local Volume 通常使用主机文件系统;磁盘类型、文件系统、宿主机负载和存储驱动都会影响延迟。数据库的 fsync、WAL 和存储设备的写入持久性仍然需要单独验证。
九、启动顺序、健康检查与应用连接
depends_on 主要描述容器启动顺序,不等价于数据库已经可以接受连接。应用容器可能在数据库进程刚启动、还未完成恢复时就发起连接。
Compose 可以通过健康检查表达“服务可用”:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 20
api:
image: example/api:1.0
depends_on:
db:
condition: service_healthy
但健康检查也有边界:
pg_isready主要检查连接可用性;- 它不验证业务表是否存在;
- 不验证迁移是否完成;
- 不验证数据库是否有足够磁盘;
- 不验证读写延迟是否符合要求。
应用自身仍应实现连接重试和退避。数据库重启、主机迁移和恢复期间,连接失败是正常故障路径的一部分,而不是只靠 Compose 配置就能消除的情况。
十、数据库版本升级:镜像升级不等于数据升级
把:
image: postgres:16
改成:
image: postgres:17
只改变了数据库服务器镜像,不一定能直接启动已有的 PostgreSQL 16 数据目录。PostgreSQL 的主版本升级通常需要逻辑导出导入、pg_upgrade 或其他官方支持的升级路径。
正确的思路是先确认:
docker compose exec db psql -U app -d appdb -c 'SHOW server_version;'
然后选择升级方法:
旧版本容器和旧数据
→ 备份并验证
→ 准备新版本数据库
→ 使用兼容的升级方式迁移
→ 校验数据、权限和扩展
→ 切换应用连接
不要把“拉取了新镜像”和“完成了数据库升级”混为一谈。镜像标签也可能移动,生产环境应记录并审查镜像摘要:
docker image inspect postgres:16 \
--format '{{index .RepoDigests 0}}'
标签固定有助于复现,但不能替代数据库升级计划和恢复演练。
十一、哪些能力适合 Docker,哪些属于生产边界
1. Docker 能提供的能力
在单台 Linux 主机上,Docker 可以很好地提供:
- 可重复的数据库软件环境;
- 进程隔离;
- 配置和环境变量注入;
- Volume 挂载;
- 健康检查和重启策略;
- 资源限制;
- 本地开发、测试和 CI 环境;
- 通过备份工具实现可恢复的数据流程。
例如:
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 20
volumes:
dbdata:
restart: unless-stopped 只能在容器退出时尝试重启。它不能修复数据损坏,不能跨主机迁移,也不能保证数据库已经完成恢复。
2. Docker 单容器不能自动提供的能力
单个容器和单个本地 Volume 默认不提供:
- 跨主机高可用;
- 自动故障转移;
- 多副本一致性;
- 跨区域灾备;
- 时间点恢复;
- 备份加密和异地保留;
- 数据库主从复制管理;
- 监控、告警和容量自动扩展。
尤其需要区分:
容器重启 ≠ 高可用
Volume 保留 ≠ 备份
数据库副本 ≠ 备份
健康检查通过 ≠ 业务正确
镜像升级 ≠ 数据库升级
数据库副本可能同步复制误删除、错误更新或逻辑损坏,因此复制改善可用性和部分 RPO,却不能单独取代不可变备份。
3. 生产部署前必须回答的边界问题
一个数据库容器进入生产前,至少要明确:
- 数据目录位于哪个主机和文件系统;
- 主机丢失后,Volume 如何恢复;
- 备份是否存放在独立故障域;
- 恢复到新主机需要哪些镜像、配置、密钥和权限;
- 数据库主版本如何升级;
- 磁盘满、内存 OOM、WAL 增长和备份失败如何告警;
- 需要的 RPO、RTO 是否能由当前备份策略满足;
- 谁负责执行恢复,恢复步骤是否经过演练;
- 是否需要副本、自动故障转移或托管数据库;
- 数据库密码和备份文件如何加密、轮换和访问控制。
当需求包含跨主机故障切换、严格 RPO、自动副本管理或异地灾备时,仅使用单机 Docker Compose 通常已经越过了合理边界。此时应考虑数据库托管服务、专业编排平台或经过验证的数据库集群方案,而不是继续堆叠容器参数。
十二、常见失败表现与诊断路径
1. 容器删除后数据消失
检查是否使用了挂载:
docker inspect <container_id> \
--format '{{json .Mounts}}'
如果 Mounts 为空,数据库很可能写在容器可写层中。恢复旧数据的可能性取决于容器是否仍存在,以及底层存储是否已被清理;不能把这种恢复当作可靠方案。
2. 初始化脚本不执行
按以下顺序检查:
docker compose logs db
docker compose exec db ls -la /docker-entrypoint-initdb.d
docker compose exec db ls -la /var/lib/postgresql/data
常见原因包括:
- Volume 已经初始化;
- 脚本文件名排序不符合预期;
- Shell 脚本没有执行权限;
- SQL 语法错误;
- 环境变量只在首次初始化时生效;
- 挂载路径写错;
- 脚本依赖的用户或数据库尚未创建。
3. 数据库启动失败
先看状态和日志:
docker compose ps
docker compose logs --tail=200 db
再区分故障类型:
Permission denied
→ 主机目录属主、权限、SELinux 或挂载方式
No space left on device
→ 文件系统容量、inode、/dev/shm 或临时目录
database files are incompatible
→ 数据目录与数据库主版本不兼容
OOMKilled
→ cgroup 内存上限或主机内存压力
connection refused
→ 数据库尚未就绪、端口错误或进程已退出
不要在没有备份和诊断的情况下删除数据目录、重新初始化或强制降级镜像。对数据库来说,“让容器启动起来”可能意味着牺牲原有数据。
4. 备份文件存在但无法恢复
验证:
file backups/appdb.dump
docker compose exec -T db \
pg_restore --list < backups/appdb.dump
如果是文件级备份,则先在临时 Volume 中检查:
docker run --rm \
-v project-dbdata:/data:ro \
alpine:3.20 \
sh -c 'test -f /data/PG_VERSION && cat /data/PG_VERSION'
恢复失败的常见原因包括:
- 备份时数据库仍在并发写入,文件状态不一致;
- 备份不完整;
- 缺少角色、扩展或表空间;
- 使用了不兼容的数据库版本;
- 恢复目标权限不正确;
- 备份文件被截断或损坏;
- 只恢复了数据库内容,没有恢复全局对象。
因此,恢复验证必须使用实际的备份文件,而不是只验证备份命令的退出码。
十三、一个适合本地开发的完整最小方案
以下 Compose 配置覆盖了持久化、初始化、健康检查和资源边界:
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
volumes:
- dbdata:/var/lib/postgresql/data
- ./init:/docker-entrypoint-initdb.d:ro
ports:
- "127.0.0.1:5432:5432"
cpus: 2.0
mem_limit: 2g
pids_limit: 512
shm_size: 256m
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 20
volumes:
dbdata:
这里把端口绑定到 127.0.0.1,表示只允许本机访问,避免把开发数据库直接暴露到所有网卡。若应用也运行在同一个 Compose 网络中,应用通常应通过服务名 db:5432 连接,而不是依赖宿主机端口映射。
启动和验证:
docker compose up -d
docker compose ps
docker compose exec db \
psql -U app -d appdb -c 'SELECT current_database(), current_user;'
备份:
mkdir -p backups
docker compose exec -T db \
pg_dump -U app -d appdb --format=custom \
> backups/appdb.dump
查看卷:
docker volume ls
docker volume inspect "$(docker compose ps -q db | xargs docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{.Name}}{{end}}{{end}}')"
开发环境需要重置时:
docker compose down -v
执行前必须确认目标确实是开发项目,因为这个命令会删除数据库 Volume。
结语:把数据库生命周期从容器生命周期中分离出来
数据库运行在 Docker 中的核心模型可以压缩为:
镜像:数据库软件和初始化逻辑
容器:数据库进程和运行时配置
Volume/Bind Mount:主数据生命周期
备份:跨存储故障的独立副本
恢复演练:证明备份真正可用
资源限制:约束数据库实际运行环境
生产方案:在单容器能力之外补齐高可用和灾备
只要数据库数据仍然依附于容器可写层,容器删除就可能等于数据删除;只要备份没有经过恢复验证,就不能声称具备可恢复能力;只要 Volume 仍在单台主机上,就不能把它当成跨主机高可用。
Docker 可以成为可靠数据库运行环境的一部分,但它本身不是数据库运维系统。可靠性来自明确的数据边界、一致的备份方法、可重复的恢复流程、经过计算的资源限制,以及对单机存储和容器生命周期边界的诚实认识。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 时区、Locale 与 CA 证书:最小镜像中的运行时基础
- 下一篇:Compose 服务发现与依赖:DNS、Healthcheck、启动顺序和重连
- 延伸:Docker 存储:Volume、Bind Mount、tmpfs、权限和备份恢复
- 延伸:Docker 数据备份与主机迁移:卷、数据库、一致性和恢复演练
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论