PostgreSQL 备份与恢复:用 Docker 从头做一遍

2026-10-16T15:01:00

备份命令跑完了,目录里也有一个文件,接下来该检查什么?

最直接的办法,是找一个空数据库,把文件恢复进去。看看旧数据还在不在,再试着新增一条。如果这一步没做过,真到需要恢复的时候,心里多少还是没底。

这篇用一份很小的博客数据做演示:3 篇文章、2 条站点设置。先备份,再恢复到另一个数据库,最后新增第 4 篇文章。所有操作都在新建的演示环境里完成,不需要连接正在使用的数据库。

下面的命令在 Linux 终端或 Windows 的 WSL 中执行,需要提前装好 Docker 和 Compose。也可以下载配置文件和示例数据。解压后进入 pg-backup-demo 目录,执行 mkdir -p backups,把 .env.example 复制为 .env,再从启动数据库那一步开始。

先准备原数据库

为了避免混淆,先约定两个名字:

  • db:原数据库,文章一开始存放在这里。
  • restored_db:新数据库,用来接收备份里的数据。

它们各有自己的存储空间。恢复操作都在 restored_db 上做,原数据库留着,方便随时比较。

先建目录:

mkdir -p ~/pg-backup-demo/backups
cd ~/pg-backup-demo
umask 077

后面的命令都在 ~/pg-backup-demo 里执行。backups 用来放备份文件;umask 077 会限制新文件的默认权限,备份里有业务数据时,不适合让其他账号随便读取。

设置演示密码

cat > .env <<'EOF'
BLOG_DB_PASSWORD=LabPg_20261016_x7
EOF
chmod 600 .env

上面是公开的演示密码。用于自己的数据库时,换成另一个密码,并保管好 .env 文件。

创建 compose.yaml

cat > compose.yaml <<'EOF'
name: pg-backup-demo

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_USER: blogdemo
      POSTGRES_DB: blogdemo
      POSTGRES_PASSWORD: ${BLOG_DB_PASSWORD:?请先创建 .env 并设置演示密码}
    volumes:
      - source_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U blogdemo -d blogdemo"]
      interval: 3s
      timeout: 3s
      retries: 20

  restored_db:
    image: postgres:17
    profiles: ["restore"]
    environment:
      POSTGRES_USER: blogdemo
      POSTGRES_DB: blogdemo
      POSTGRES_PASSWORD: ${BLOG_DB_PASSWORD:?请先创建 .env 并设置演示密码}
    volumes:
      - restored_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U blogdemo -d blogdemo"]
      interval: 3s
      timeout: 3s
      retries: 20

  adminer:
    image: adminer:5
    environment:
      ADMINER_DEFAULT_SERVER: db
    ports:
      - "127.0.0.1:18982:8080"
    depends_on:
      db:
        condition: service_healthy

volumes:
  source_data:
  restored_data:
EOF

文件看起来比较长,主要是因为写了两套数据库。先记住这几处:

source_data 保存原数据库的文件,restored_data 保存新数据库的文件。它们叫“数据卷”,容器停止后,卷里的数据仍然保留。

restored_db 下面的 profiles: ["restore"],让它暂时不启动,等到恢复时再用。

adminer 是一个查看数据库的网页工具。这里给它安排了 127.0.0.1:18982,数据库本身没有发布 5432 端口。网页工具通过 Docker 网络里的 db 这个名字连接数据库。

本例使用 PostgreSQL 17。先保持版本和数据目录不变,尤其不要给旧数据卷直接换上另一个大版本的镜像。

放入几条容易核对的数据

创建 seed.sql:

cat > seed.sql <<'EOF'
CREATE TABLE posts (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    slug text NOT NULL UNIQUE,
    title text NOT NULL,
    body text NOT NULL
);

INSERT INTO posts (slug, title, body) VALUES
('docker-nginx', 'Docker 反向代理配置', '入口 80,后端 8080。'),
('ssh-tunnel', 'SSH 本地端口转发', '只转发需要访问的端口。'),
('postgres-backup', 'PostgreSQL 备份演练', '备份后在另一只容器中恢复。');

CREATE TABLE site_settings (
    key text PRIMARY KEY,
    value jsonb NOT NULL
);

INSERT INTO site_settings VALUES
('site', '{"name":"演示博客","language":"zh-CN"}'),
('features', '{"comments":true,"page_size":10}');
EOF

posts 表里存文章,site_settings 表里存站点设置。文章有自动编号和中文标题,设置里还有 JSON 内容。恢复以后,可以同时看这些内容有没有回来。

启动原数据库和查看工具:

docker compose config -q
docker compose up -d --wait db adminer
docker compose ps -a

第一条检查配置,正常时没有输出。--wait 会等待数据库准备好;如果你的 Compose 不支持它,可以用 up -d 启动,然后查看 ps -a,等 db 显示 healthy 再继续。

如果停在拉取镜像、下载超时这一步,需要先解决 Docker 镜像下载问题。数据库还没启动,暂时不用改密码或 SQL。

接着把示例数据写进去:

docker compose cp seed.sql db:/tmp/seed.sql
docker compose exec -T db psql -U blogdemo -d blogdemo -v ON_ERROR_STOP=1 -f /tmp/seed.sql

第一条把 SQL 文件复制到容器,第二条执行它。ON_ERROR_STOP=1 表示遇到 SQL 错误就停止,方便及时发现哪一步没成功。

正常输出会包含 CREATE TABLE、INSERT 0 3 和 INSERT 0 2。这份建表脚本只执行一次,再执行会提示表已存在。

查询一下:

docker compose exec -T db psql -U blogdemo -d blogdemo -c "SELECT id, slug, title FROM posts ORDER BY id;"
docker compose exec -T db psql -U blogdemo -d blogdemo -c "SELECT key, value FROM site_settings ORDER BY key;"

应当看到编号为 1、2、3 的三篇文章,以及 features、site 两条设置。

也可以在网页里查看

Docker 运行在自己电脑上的,打开 http://127.0.0.1:18982/,按下面填写:

登录项填什么系统PostgreSQL服务器db用户名blogdemo密码.env 里设置的密码数据库blogdemo

系统默认可能是 MySQL,记得切换。服务器填 db,这里的 127.0.0.1 会指向 Adminer 自己,连不到另一个容器里的数据库。

如果 Docker 在云服务器上,在自己电脑的终端执行:

ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:18983:127.0.0.1:18982 your_user@203.0.113.10

替换 SSH 用户名和示例 IP,登录后保持终端打开,再用电脑浏览器访问 http://127.0.0.1:18983/。这样就不用把管理页面开放到公网。

登录后,在 posts 表旁边点击“选择”:

当前连接的是 db,表里有三篇文章。

导出备份,拿到一个 .dump 文件

回到演示目录的终端,执行:

docker compose exec -T db pg_dump -U blogdemo -d blogdemo -Fc -f /tmp/blogdemo.dump

这里真正做备份的是 PostgreSQL 自带的 pg_dump。-Fc 选择备份格式,-f 指定文件位置。命令成功时通常没有输出,文件保存在 db 容器的 /tmp/blogdemo.dump。

这种格式要用 pg_restore 恢复,不能直接当成 SQL 文件交给 psql。我们先让容器里的工具把文件写好,再复制出来,也避免了用终端文本重定向传递二进制文件的问题。

把文件复制到服务器的 backups 目录,顺手比较复制前后是否一致:

docker compose cp db:/tmp/blogdemo.dump backups/blogdemo.dump
chmod 600 backups/blogdemo.dump
ls -lh backups/blogdemo.dump
sha256sum backups/blogdemo.dump
docker compose exec -T db sha256sum /tmp/blogdemo.dump

最后两条会打印很长的一串字符,这是文件的 SHA-256 校验值。两串完全相同,表示复制前后的文件内容相同;不同就先重新检查或复制,别急着往下恢复。

现在要保管的是 backups/blogdemo.dump。容器里的 /tmp 只是临时位置,容器重建后文件可能消失。

这份演示使用固定文件名,方便对照。给自己的业务定期备份时,文件名最好带上日期,并把副本放到另一处存储,避免每次覆盖唯一的一份。

在新数据库里恢复

先确认目标是空的

启动前面暂时没有启动的 restored_db:

docker compose --profile restore up -d --wait restored_db
docker compose exec -T restored_db psql -U blogdemo -d blogdemo -Atc "SELECT count(*) FROM pg_tables WHERE schemaname='public';"

第二条查询它已经有多少张表。第一次运行应当输出 0。

如果不是 0,先看是不是以前做过这个演示。停止容器不会自动清掉数据卷,所以“重新启动”不等于“新建了空数据库”。确认目标后再继续,避免把备份导入一个已经有数据的库。

复制文件,执行恢复

docker compose cp backups/blogdemo.dump restored_db:/tmp/blogdemo.dump
docker compose exec -T restored_db pg_restore -U blogdemo -d blogdemo --exit-on-error --single-transaction --no-owner --no-privileges /tmp/blogdemo.dump

第一条把备份送进新容器,第二条用 pg_restore 恢复。注意两条命令写的都是 restored_db。

几个参数不用死记,知道这里为什么加它们就行:

参数这里起什么作用--exit-on-error报错就停,避免出错后还继续恢复--single-transaction把这次恢复放在一起处理,失败时撤回这次事务中的改动--no-owner由当前账号创建表,不要求目标里也有原来的对象所有者--no-privileges不照搬原库的授权设置,正式迁移时再按需要配置账号权限

成功时也可能没有输出。接下来直接查数据,比只看终端有没有报错更清楚。

旧文章回来了,新增文章能不能写进去

在新数据库里查同样两张表:

docker compose exec -T restored_db psql -U blogdemo -d blogdemo -c "SELECT id, slug, title FROM posts ORDER BY id;"
docker compose exec -T restored_db psql -U blogdemo -d blogdemo -c "SELECT key, value FROM site_settings ORDER BY key;"

应该仍然是 3 篇文章、2 条设置。对照前面的结果,看看中文标题、文章编号和 JSON 里的值是否一致。

数据多的时候,逐条看页面不太方便。还可以先把两边的文章表各导出一份 CSV,再比较内容。这一步要在新增文章之前做,此时两边都是原来的三条:

docker compose exec -T db psql -U blogdemo -d blogdemo -c "COPY (SELECT id,slug,title,body FROM posts ORDER BY id) TO STDOUT WITH CSV HEADER" > backups/source-posts.csv
docker compose exec -T restored_db psql -U blogdemo -d blogdemo -c "COPY (SELECT id,slug,title,body FROM posts ORDER BY id) TO STDOUT WITH CSV HEADER" > backups/restored-posts.csv
diff -u backups/source-posts.csv backups/restored-posts.csv

最后的 diff 没有输出,并且命令正常结束,表示两份结果相同。这里先按 id 排好顺序,避免同一批数据因为排列不同而显示出差异。用在自己的数据库时,要换成实际需要核对的表和字段。

再新增一篇文章

在新数据库里执行:

docker compose exec -T restored_db psql -U blogdemo -d blogdemo -c "INSERT INTO posts (slug,title,body) VALUES ('restore-check','恢复后新增的文章','这条只写入恢复库。') RETURNING id;"

这次实际返回的编号是 4。原来的三条数据能读,新文章也能接着编号写进去,这才完成了这份小样本的恢复检查。

在 Adminer 里登出,把服务器从 db 改为 restored_db,其他登录项保持相同。打开 posts 后会看到:

前三篇来自备份,第四篇刚刚写入。左侧连接名已经换成 restored_db。

最后分别查一下数量:

docker compose exec -T db psql -U blogdemo -d blogdemo -Atc "SELECT count(*) FROM posts;"
docker compose exec -T restored_db psql -U blogdemo -d blogdemo -Atc "SELECT count(*) FROM posts;"

原数据库是 3,新数据库是 4。这样也确认了新文章没有误写进原数据库。

新增语句执行一次就够了。重复执行会提示 slug 重复,因为 restore-check 这个值已经用过了。

主键和“不能重复”的限制还在吗

除了查看和新增文章,还可以检查表上的限制是否保留:

docker compose exec -T restored_db psql -U blogdemo -d blogdemo -c "SELECT conname, contype FROM pg_constraint WHERE conrelid='posts'::regclass ORDER BY conname;"

结果里应当有 posts_pkey 和 posts_slug_key。后一个要求每篇文章的 slug 不重复,下面这张图就是重复新增同一篇文章时的报错:

restore-check 已经用过,数据库拒绝再次写入同样的值。

自增编号偶尔跳过一个数字并不稀奇,失败的插入也可能占用编号。别只因为编号有间隔,就判断恢复失败。

恢复时报错,先看是哪一种

提示表已经存在

如果在刚恢复好的数据库上再恢复一遍,会看到类似错误:

pg_restore: error: could not execute query: ERROR: relation "posts" already exists

意思是目标里已经有 posts 这张表。先核对连接的是不是刚才用过的 restored_db,这时应另找空数据库继续试。

前面加了 --single-transaction,这次重复恢复失败后,原有四篇文章仍在。这一点也做过检查。

不要急着追加 --clean。它会尝试删除备份中对应的现有对象;如果目标已经有新的业务数据,需要先决定哪些要保留、哪些允许覆盖。

提示 end of file

先看备份能不能列出里面的内容:

docker compose exec -T db pg_restore --list /tmp/blogdemo.dump

正常能看到 posts、site_settings 等条目。不过,能列出来仍然不等于整个文件完好。

这次另外复制了一份备份,截掉副本末尾 100 字节,再试着恢复。它仍然能列出目录,恢复时却报:

pg_restore: error: could not read from input file: end of file

这表示读取过程中碰到了意外的文件结尾。实验是在另外新建的空库里做的,原备份没有改动;带着 --single-transaction 恢复失败后,空库里也没有留下恢复到一半的表。

碰到这个错误,先比较文件两端的校验值,看看是否复制中断、拿错文件或磁盘空间不足。原文件完整的话重新复制;原文件已经损坏,就需要重新备份或找其他副本。

改了 .env,网页登录仍提示密码错误

.env 中的密码用于第一次初始化数据库。数据卷已经存在后,修改这个文件再重启,不会自动改掉数据库里的账号密码。

先确认 Adminer 选的是 PostgreSQL,服务器名和用户名也填对了,再核对第一次启动时设置的密码。已有数据库需要通过数据库的账号管理功能改密码,不要为了登录而删除数据卷。

换成自己的数据库时,还要记住什么

这个演示只用了两张小表。正式搬迁网站时,下面几件事也要一起处理:

  • 图片和配置文件要另外备份。 pg_dump 备份数据库,不会顺便带上网站的上传目录、证书或对象存储里的附件。
  • 恢复后要让应用连接一次。 文章能查询只是第一步,还要试登录、发文章、改设置等实际功能。
  • 备份之后的新修改要有安排。 旧库继续产生的数据不会自动出现在这份文件里,切换前要安排最后一次同步或暂停写入。
  • 账号权限和扩展要提前准备。 本例使用镜像初始化的高权限账号做演示;真实应用按需要授权。旧库使用了扩展时,新环境也要支持它。

本文两边实际使用的都是 PostgreSQL 17.11,网页工具为 Adminer 5.5.1,运行在 Docker Desktop 的 Linux 容器中。跨大版本迁移时,再按 PostgreSQL 文档确认工具和目标版本是否兼容。

结束演示,数据先留着

cd ~/pg-backup-demo
docker compose --profile restore down

这会停止并移除本文的容器和网络,两个数据卷和 backups 目录都会保留。以后重新启动,仍然是这次留下的数据,不必再执行建表脚本。

用了 SSH 转发的,最后关闭对应终端。备份文件需要长期保存时,再复制到独立的备份位置,并定期取一份出来恢复检查。

参考:PostgreSQL pg_dump、pg_restore、PostgreSQL 官方镜像、Adminer 官方镜像。

当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »