目录

Docker PostgreSQL 教程:安装、连接、备份与升级

PostgreSQL 18 的重要变更:从 postgres:18 开始,官方镜像的数据目录改为 /var/lib/postgresql/18/docker,Volume 必须挂载到 /var/lib/postgresql。网上大多数教程写的 /var/lib/postgresql/data 是 17 版以前的路径,用在 18 版会导致启动失败,下面有实测。

一条命令启动

$ docker run -d --name pg -e POSTGRES_PASSWORD=secret -p 127.0.0.1:5432:5432 -v pgdata:/var/lib/postgresql --restart unless-stopped postgres:18
1b34d05a195f347c86e431441162b760167a5c04016a528e10342d0199be016d

$ docker logs pg 2>&1 | tail -n 3
2026-10-13 00:53:32.071 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2026-10-13 00:53:32.075 UTC [72] LOG:  database system was shut down at 2026-10-13 00:53:31 UTC
2026-10-13 00:53:32.080 UTC [1] LOG:  database system is ready to accept connections

$ docker exec pg psql -U postgres -c "SHOW data_directory;"
        data_directory
-------------------------------
 /var/lib/postgresql/18/docker
(1 row)
参数说明
POSTGRES_PASSWORD超级用户的密码(必填)
-p 127.0.0.1:5432:5432仅开放给本机
-v pgdata:/var/lib/postgresql数据存放在 Volume pgdata 中。18 版需要挂载此路径

SHOW data_directory 确认实际数据目录是 /var/lib/postgresql/18/docker,位于我们挂载的 /var/lib/postgresql 下。目录名称包含大版本号,今后升级时,新旧版本的数据可以并存于同一个 Volume 中。

其他常用环境变量:

环境变量说明默认值
POSTGRES_USER超级用户名postgres
POSTGRES_DB默认数据库与 POSTGRES_USER 相同
PGDATA数据目录/var/lib/postgresql/18/docker(18 版)
TZ时区UTC

和 MySQL 一样,这些变量仅在数据目录为空时生效。之后修改 POSTGRES_PASSWORD 不会改变已有密码,需要使用 ALTER USER ... PASSWORD '...' 修改。

实测:18 版挂载旧路径会失败

按照旧教程将 Volume 挂载到 /var/lib/postgresql/data:

$ docker run -d --name pgold -e POSTGRES_PASSWORD=secret -v pgold:/var/lib/postgresql/data postgres:18
b480eb9c72b29fa99f1ed8df17972e2bfc2d6da4b86c058a36cf21fd63579bfa

$ docker ps -a --filter name=pgold --format "{{.Status}}"
Exited (1) 5 seconds ago

$ docker logs pgold 2>&1 | head -n 12
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

容器在几秒内以 Exited (1) 结束。错误信息说明 18 版改用按大版本命名的目录,并且在 /var/lib/postgresql/data 中发现了“未使用的挂载”。此检查是为了避免你以为数据已保存,实际上数据全写在容器里,删除容器后就会消失。将挂载路径改为 /var/lib/postgresql 即可。

使用 Compose 管理

实际项目建议使用 Compose。创建文件夹 pg-demo:

pg-demo/
├── compose.yaml
├── .env
└── initdb/
    └── 01-init.sql

.env:

DB_PASSWORD=change-this-password

initdb/01-init.sql(首次初始化时运行,用于创建扩展、数据表和示例数据;最后的 pg_sleep(8) 用于模拟耗时的初始化,例如导入大量数据,便于观察健康检查):

CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE TABLE products (
  id serial PRIMARY KEY,
  name text NOT NULL,
  price integer NOT NULL
);

INSERT INTO products (name, price) VALUES ('珍珠奶茶', 65), ('鳳梨酥', 45), ('牛肉麵', 180);

-- 模拟耗时的初始化(例如导入大量数据)
SELECT pg_sleep(8);

compose.yaml:

services:
  db:
    image: postgres:18
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app
      TZ: Asia/Taipei
    command: ["postgres", "-c", "max_connections=200", "-c", "shared_buffers=256MB"]
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql
      - ./initdb:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U app -d app"]
      interval: 2s
      timeout: 3s
      retries: 30
    shm_size: 256mb
    restart: unless-stopped

volumes:
  pgdata:
配置说明
POSTGRES_USER: app将超级用户改名为 app,不再有 postgres 这个用户
command: ["postgres", "-c", ...]使用 -c 传入 PostgreSQL 配置,无需另外准备配置文件
pg_isready -h 127.0.0.1PostgreSQL 内置的检查工具。必须添加 -h 127.0.0.1,原因见下方实测
shm_size: 256mbDocker 默认的共享内存 /dev/shm 只有 64 MB,较复杂的查询可能出现 could not resize shared memory segment 错误
$ docker compose up -d --wait
[+] up 3/3
 ✔ Network pg-demo_default Created                                                                                                                0.0s
 ✔ Volume pg-demo_pgdata   Created                                                                                                                0.0s
 ✔ Container pg-demo-db-1  Healthy                                                                                                               11.3s

$ docker compose exec db psql -U app -h 127.0.0.1 -c "SELECT * FROM products;"
 id |   name   | price
----+----------+-------
  1 | 珍珠奶茶 |    65
  2 | 鳳梨酥   |    45
  3 | 牛肉麵   |   180
(3 rows)


$ docker compose logs db 2>&1 | grep -E "running /docker-entrypoint-initdb.d|init process complete|listening on IPv4"
db-1  | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01-init.sql
db-1  | PostgreSQL init process complete; ready for start up.
db-1  | 2026-10-13 08:52:58.663 CST [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432

$ docker compose exec db psql -U app -c "SHOW max_connections;" -c "SHOW shared_buffers;" -c "SELECT now();"
 max_connections
-----------------
 200
(1 row)

 shared_buffers
----------------
 256MB
(1 row)

              now
-------------------------------
 2026-10-13 08:53:01.293902+08
(1 row)


$ docker compose exec db df -h /dev/shm
Filesystem      Size  Used Avail Use% Mounted on
shm             256M  1.1M  255M   1% /dev/shm

$ docker compose exec db psql -U app -c "\dx"
                                             List of installed extensions
  Name   | Version | Default version |   Schema   |                            Description
---------+---------+-----------------+------------+-------------------------------------------------------------------
 pg_trgm | 1.6     | 1.6             | public     | text similarity measurement and index searching based on trigrams
 plpgsql | 1.0     | 1.0             | pg_catalog | PL/pgSQL procedural language
(2 rows)
  • Healthy 用了 11.3 秒,正好是初始化加上 pg_sleep(8) 的时间。健康检查通过后立即使用 TCP 连接,数据都在,中文显示正常。
  • 日志显示初始化脚本已运行,并且在初始化完成后才开始监听 0.0.0.0:5432。
  • max_connections 为 200、shared_buffers 为 256MB,说明 command 的参数已生效;now() 显示 +08 时区。
  • /dev/shm 为 256M,说明 shm_size 已生效。
  • \dx 列出了已安装的扩展,pg_trgm 已由初始化脚本创建。

健康检查陷阱:pg_isready 不加 -h 会过早报告 healthy

网上常见的写法是 pg_isready -U app -d app(没有 -h)。实测改用这种写法:

$ docker compose up -d --wait
[+] up 3/3
 ✔ Network pg-demo_default Created                                                                                                                0.0s
 ✔ Volume pg-demo_pgdata   Created                                                                                                                0.0s
 ✔ Container pg-demo-db-1  Healthy                                                                                                                3.2s

$ docker compose exec db psql -U app -h 127.0.0.1 -c "SELECT count(*) FROM products;"
psql: error: connection to server at "127.0.0.1", port 5432 failed: Connection refused
	Is the server running on that host and accepting TCP/IP connections?

$ docker compose logs db 2>&1 | grep -E "pg_sleep|init process complete|listening on IPv4"

仅过了 3.2 秒就报告 Healthy,但通过 TCP 连接却得到 Connection refused。

原因与 MySQL 一样:初始化期间,启动脚本会先启动一个仅接受 Unix socket 连接的临时服务器来运行 initdb 中的脚本。pg_isready 不加 -h 时会走 socket,因此在初始化尚未完成时就报告可以连接。依赖它的应用(depends_on 的 condition: service_healthy)会在此时启动,通过 TCP 连接就会失败。添加 -h 127.0.0.1 强制使用 TCP,就能等到正式服务器启动。

连接与 psql

使用 psql 操作

容器内有 psql 客户端。交互式使用 docker compose exec db psql -U app(单独的容器则是 docker exec -it pg psql -U postgres),进入 app=# 提示符后输入 SQL 或下面的命令。也可以用 -c 直接执行:

$ docker compose exec db psql -U app -c "SELECT version();"
                                                      version
--------------------------------------------------------------------------------------------------------------------
 PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
(1 row)


$ docker compose exec db psql -U app -c "\l"
                                                 List of databases
   Name    | Owner | Encoding | Locale Provider |  Collate   |   Ctype    | Locale | ICU Rules | Access privileges
-----------+-------+----------+-----------------+------------+------------+--------+-----------+-------------------
 app       | app   | UTF8     | libc            | en_US.utf8 | en_US.utf8 |        |           |
 postgres  | app   | UTF8     | libc            | en_US.utf8 | en_US.utf8 |        |           |
 template0 | app   | UTF8     | libc            | en_US.utf8 | en_US.utf8 |        |           | =c/app           +
           |       |          |                 |            |            |        |           | app=CTc/app
 template1 | app   | UTF8     | libc            | en_US.utf8 | en_US.utf8 |        |           | =c/app           +
           |       |          |                 |            |            |        |           | app=CTc/app
(4 rows)


$ docker compose exec db psql -U app -c "\dt"
          List of tables
 Schema |   Name   | Type  | Owner
--------+----------+-------+-------
 public | products | table | app
(1 row)


$ docker compose exec db psql -U app -c "\d products"
                            Table "public.products"
 Column |  Type   | Collation | Nullable |               Default
--------+---------+-----------+----------+--------------------------------------
 id     | integer |           | not null | nextval('products_id_seq'::regclass)
 name   | text    |           | not null |
 price  | integer |           | not null |
Indexes:
    "products_pkey" PRIMARY KEY, btree (id)


$ docker compose exec db psql -U app -c "\du"
                             List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 app       | Superuser, Create role, Create DB, Replication, Bypass RLS
命令作用
\l列出数据库
\c 名称切换数据库
\dt列出数据表
\d 数据表查看数据表结构
\du列出用户(角色)
\dx列出已安装的扩展
\q退出

\du 显示 app 是 Superuser,\l 中所有数据库的 Owner 也都是 app:设置 POSTGRES_USER 后,app 就取代了默认的 postgres。

为什么在容器内无需输入密码

上面的 psql 都没有输入密码。查看数据目录中的 pg_hba.conf(PostgreSQL 的连接身份验证规则):

$ docker compose exec db grep -v "^#" /var/lib/postgresql/18/docker/pg_hba.conf | grep -v "^$"
local   all             all                                     trust
host    all             all             127.0.0.1/32            trust
host    all             all             ::1/128                 trust
local   replication     all                                     trust
host    replication     all             127.0.0.1/32            trust
host    replication     all             ::1/128                 trust
host all all all scram-sha-256

$ docker run --rm --network pg-demo_default -e PGPASSWORD=wrong postgres:18 psql -h db -U app -c "SELECT 1;"
psql: error: connection to server at "db" (172.19.0.2), port 5432 failed: FATAL:  password authentication failed for user "app"

$ docker run --rm --network pg-demo_default -e PGPASSWORD=change-this-password postgres:18 psql -h db -U app -c "SELECT current_user, inet_server_addr();"
 current_user | inet_server_addr
--------------+------------------
 app          | 172.19.0.2
(1 row)
  • 官方镜像的配置:容器内的连接(Unix socket、127.0.0.1、::1)均为 trust,不检查密码。
  • 其他来源的连接(最后一行 host all all all)使用 scram-sha-256 密码验证。
  • 从同一个 Compose 网络中的另一个容器连接到 db,密码错误时会得到 password authentication failed;密码正确才能连接。

因此,能够进入容器的人就能直接操作数据库,这也是需要保护 Docker 权限的原因(参见 Docker 安全性)。从主机通过 -p 映射的端口连接时,来源 IP 是 Docker 网络的网关,同样需要密码。

从主机和其他容器连接

postgres://app:密码@127.0.0.1:5432/app   # 从主机(GUI 工具、应用程序)
postgres://app:密码@db:5432/app          # 从同一 Compose 项目的其他服务

常见连接错误

$ docker compose exec db psql -c "SELECT 1;"
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL:  role "root" does not exist

$ docker compose exec db psql -U postgres -c "SELECT 1;"
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL:  role "postgres" does not exist

$ docker compose exec db psql -U app -d shop -c "SELECT 1;"
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL:  database "shop" does not exist

$ docker compose exec -e PGPASSWORD=wrong db psql -h 127.0.0.1 -U app -c "SELECT 1;"
 ?column?
----------
        1
(1 row)


$ docker compose exec db psql -U app -h 127.0.0.1 -p 5433 -c "SELECT 1;"
psql: error: connection to server at "127.0.0.1", port 5433 failed: Connection refused
	Is the server running on that host and accepting TCP/IP connections?
错误原因
role "root" does not existpsql 没有添加 -U,默认使用当前系统用户名(容器内是 root)
role "postgres" does not exist设置了 POSTGRES_USER,默认的 postgres 用户就不存在
database "shop" does not existPOSTGRES_DB 只在初始化时创建;其他数据库需要自行 CREATE DATABASE
Connection refused端口错误、数据库尚未启动完成,或连接到了错误的主机
password authentication failed密码错误,或 Volume 使用其他密码初始化

注意第 4 条命令:在容器内使用错误密码连接 127.0.0.1 也成功了,这就是上面提到的 trust 规则。

备份与还原

$ docker compose exec -T db pg_dump -U app app > app.sql

$ ls -lh app.sql && grep -E "^CREATE (TABLE|EXTENSION)" app.sql
-rw-r--r--    1 root     root        2.3K Oct 13 00:53 app.sql
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
CREATE TABLE public.products (

$ docker compose exec -T db pg_dump -U app -Fc app > app.dump

$ ls -lh app.dump
-rw-r--r--    1 root     root        3.1K Oct 13 00:53 app.dump

$ docker compose exec db psql -U app -c "DROP TABLE products;"
DROP TABLE

$ docker compose exec -T db pg_restore -U app -d app --clean --if-exists < app.dump

$ docker compose exec db psql -U app -c "SELECT count(*) FROM products;"
 count
-------
     3
(1 row)


$ rm app.sql app.dump
命令说明
pg_dump app > app.sql导出为 SQL 纯文本,可以直接阅读,也可以用 psql 导入
pg_dump -Fc app > app.dump自定义格式,已经压缩,用 pg_restore 还原,并且可以选择只还原部分数据表
pg_restore --clean --if-exists还原前先删除已存在的对象,避免重复
exec -TCompose 的 exec 默认会分配 TTY,使用重定向时需要添加 -T 关闭

备份中包含 CREATE EXTENSION,还原到新数据库时也会一并创建扩展。此示例的数据很少,因此自定义格式(3.1K)反而比纯文本(2.3K)更大;数据量较大时,自定义格式会小很多。

其他常用命令:

# 备份所有数据库和角色(用户、密码)
docker compose exec -T db pg_dumpall -U app > all.sql

# 还原 SQL 纯文本
docker compose exec -T db psql -U app -d app < app.sql

可以使用 cron 配合上面的命令定期备份,并将文件复制到其他机器或云存储。

Windows 用户的备份与还原

PowerShell 不支持 <,Windows PowerShell 5.1 的 > 会改变文件编码,-Fc 的二进制备份甚至会直接损坏。改用 pg_dump -f 在容器内生成文件,再用 docker compose cp 复制,这种写法适用于所有系统:

# 备份
docker compose exec db pg_dump -U app -Fc -f /tmp/app.dump app
docker compose cp db:/tmp/app.dump app.dump

# 还原
docker compose cp app.dump db:/tmp/restore.dump
docker compose exec db pg_restore -U app -d app --clean --if-exists /tmp/restore.dump

这组命令没有用到 $(pwd)、<、>,bash、PowerShell、CMD 都可以直接运行。实测(先创建数据表、备份、删除、还原):

$ docker compose exec db psql -U app -c 'CREATE TABLE t (id int); INSERT INTO t VALUES (1),(2);'
CREATE TABLE
INSERT 0 2

$ docker compose exec db pg_dump -U app -Fc -f /tmp/app.dump app

$ docker compose cp db:/tmp/app.dump app.dump
 pgwin-db-1 Copying pgwin-db-1:/tmp/app.dump to app.dump
 pgwin-db-1 Copied pgwin-db-1:/tmp/app.dump to app.dump

$ ls -l app.dump
-rw-rw-rw-    1 root     root          1251 Oct 13 05:30 app.dump

$ docker compose exec db psql -U app -c 'DROP TABLE t;'
DROP TABLE

$ docker compose cp app.dump db:/tmp/restore.dump
 pgwin-db-1 Copying app.dump to pgwin-db-1:/tmp/restore.dump
 pgwin-db-1 Copied app.dump to pgwin-db-1:/tmp/restore.dump

$ docker compose exec db pg_restore -U app -d app /tmp/restore.dump

$ docker compose exec db psql -U app -c 'SELECT count(*) FROM t;'
 count
-------
     2
(1 row)

大版本升级(例如 16 → 18)

小版本更新(例如 18.5 → 18.6)只需拉取新镜像并重新创建容器。大版本则不同:PostgreSQL 不同大版本的数据文件格式不兼容。

先使用 16 版创建一些数据:

$ docker run -d --name pg16 -e POSTGRES_PASSWORD=secret -v pg16data:/var/lib/postgresql/data postgres:16
20691764a98828d24c958eaaa71e83c08ebff29b984350ff7f6782ba8d74b4ca

$ docker exec pg16 psql -U postgres -c "CREATE TABLE notes (id serial PRIMARY KEY, body text); INSERT INTO notes (body) VALUES ('written by 16');"
CREATE TABLE
INSERT 0 1

$ docker stop pg16
pg16

直接更换镜像会失败

使用 postgres:18 打开 16 版的数据(这里用 PGDATA 指定旧路径,模拟直接把 tag 从 16 改成 18):

$ docker run --name pg18 -e POSTGRES_PASSWORD=secret -e PGDATA=/var/lib/postgresql/data -v pg16data:/var/lib/postgresql/data postgres:18 2>&1 | tail -n 3

2026-10-13 00:54:09.740 UTC [1] FATAL:  database files are incompatible with server
2026-10-13 00:54:09.740 UTC [1] DETAIL:  The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 18.6 (Debian 18.6-1.pgdg13+2).

$ docker rm pg18
pg18

database files are incompatible with server:数据目录由 16 版初始化,18.6 无法使用。

导出再导入

最简单安全的做法:使用旧版 pg_dumpall 导出所有数据,再导入新版的新 Volume。

$ docker start pg16
pg16

$ docker exec pg16 pg_dumpall -U postgres > upgrade.sql && ls -lh upgrade.sql
-rw-r--r--    1 root     root        3.4K Oct 13 00:54 upgrade.sql

$ docker run -d --name pg18 -e POSTGRES_PASSWORD=secret -v pg18data:/var/lib/postgresql postgres:18
ec651837b76a0c9cc8b2468ec5a2ff4002334dcf015514ef94490a6c38cfe73d

$ docker exec -i pg18 psql -U postgres -q < upgrade.sql
ERROR:  role "postgres" already exists
 set_config
------------

(1 row)

 set_config
------------

(1 row)

 setval
--------
      1
(1 row)


$ docker exec pg18 psql -U postgres -c "SHOW server_version;" -c "SELECT * FROM notes;"
        server_version
-------------------------------
 18.6 (Debian 18.6-1.pgdg13+2)
(1 row)

 id |     body
----+---------------
  1 | written by 16
(1 row)


$ rm upgrade.sql
  • 导入时的 ERROR: role "postgres" already exists 可以忽略:新的 18 版容器已经创建了 postgres 用户,pg_dumpall 的输出又尝试创建一次。
  • 最后确认版本为 18.6,16 版写入的 written by 16 数据仍然存在。
  • 注意,新容器的 Volume 挂载在 /var/lib/postgresql(18 版的新路径)。

确认数据和应用都正常后,再删除旧容器和 Volume。数据量很大、无法接受长时间停机时,可以研究 pg_upgrade 或逻辑复制(logical replication),但步骤复杂得多,请先在测试环境演练。

重点回顾

  • PostgreSQL 18 的 Volume 挂载到 /var/lib/postgresql。
  • 健康检查使用 pg_isready -h 127.0.0.1。
  • 容器内连接使用 trust,外部连接需要密码。
  • 大版本升级:旧版 pg_dumpall 导出,新版新 Volume 导入。

动手练习

练习 1:使用 psql 在 app 数据库中创建一个数据表 notes,再用 \dt 确认。

答案
docker compose exec db psql -U app -c "CREATE TABLE notes (id serial PRIMARY KEY, body text);"
docker compose exec db psql -U app -c "\dt"

练习 2:自定义格式(-Fc)和纯文本 SQL 备份有什么区别?

答案

自定义格式经过压缩,需要用 pg_restore 还原,并且可以选择只还原部分数据表;纯文本可以直接阅读,也可以用 psql 导入。

常见问题

postgres:18 启动后立即退出?

18 版需要挂载 /var/lib/postgresql,挂载到旧路径 /var/lib/postgresql/data 会因检测到不兼容的目录而退出,实测见本章。

小版本更新(18.5 → 18.6)怎么做?

拉取新镜像并重建容器即可:docker compose pull db && docker compose up -d db。只有大版本升级才需要导出再导入。

Windows 上 pg_restore 失败?

PowerShell 不支持 <,而 5.1 的 > 会破坏二进制备份。使用 pg_dump -f 在容器内生成文件,再用 docker compose cp 复制,本章附有写法。

更新于 2026年10月14日