跳转到主要内容

pgAdmin 4

以 pgcli 插件方式运行 pgAdmin 4——PostgreSQL 官方 Web 管理界面,单容器直接跑上游 dpage/pgadmin4 镜像,自动生成 Web 登录账号,可选预置服务器

pgAdmin 4 是 PostgreSQL 的官方 Web 管理界面——在 浏览器里浏览 schema、执行 SQL、观察服务器状态。pgcli 将其作为独立的顶层插 件运行,CLI 表面与其他 web/infra 插件一致(install / start / stop / logs / autostart / remove),全部通过 pg.yaml 管理。

它是插件族里唯一的 UI 插件,这一点决定了它的设计:

  • 它是 Web 应用,不是数据库前置。 pgAdmin 连接哪些服务器由你自己在其 UI 里注册决定;它不像 PostgREST 或 PgBouncer 那样专职前置某一个实例。这就是它做成顶层-only 插件 (addons.pgadmin.<name>)、没有实例 sidecar 的原因。
  • 它有自己的登录。 Web UI 需要邮箱 + 密码登录。这不是 PostgreSQL 密码 ——是 pgAdmin 自己的账号,首次安装时自动生成并存入 pg.yaml。之后用 pg addon password pgadmin 取回。
  • 持久化的会话存储。 pgAdmin 把配置、已保存的服务器、浏览历史存在 bind 挂载数据目录里的 sqlite 库中。它跨 pg addon remove(不带 --clean-data) 存活,重装同名实例即复活。
  • 可选的一次性预置。 --dsn 或 --pg-name <实例名> 在 install 时向 UI 的 servers.json 预注册一个服务器,让浏览器打开时列表里已有它。这只是一个便利 ——不是运行期耦合:pgAdmin 不会因它阻塞,之后可在 UI 里增删。

建议:每个数据库一个 pgAdmin,随开随用。 pgcli 管理的 pgAdmin 是单个轻 量容器,秒级启动、内存占用极小——完全可以当一次性工具用。与其跑一个共享的 pgAdmin 把所有服务器都注册进去,不如为当前正在操作的数据库创建专用实例,用完 即删:

# 快速查看 proj01
pg addon install pgadmin --name proj01 --pg-name proj01
# ... 浏览、查询、完成
pg addon remove proj01 --clean-data

# 检查远程生产只读副本
pg addon install pgadmin --name prod-ro \
    --dsn "postgres://reader@10.0.0.7:5432/proddb"
pg addon remove prod-ro --clean-data

每个实例有独立的端口(自动分配)、独立的登录、独立的 pgpass——数据库之间无 交叉污染,共享会话存储里不会残留陈旧连接。下次需要时,秒级重装。

不在范围内: 这里 pgAdmin 只提供明文 HTTP——没有接入 TLS 终止(需要 HTTPS 就在前面架反向代理)。servers.json 本身不能携带目标服务器的密码 (pgAdmin 拒绝导入),但当预置 DSN 带了密码时,pgcli 通过 pgAdmin 的 pgpass 支持预配置它,预置服务器连接时无需再输入——见预置服务器。

参数

单容器直接跑上游 docker.io/dpage/pgadmin4 镜像(pull-only——pgcli 从不构建 它),全部经由 entrypoint 读取的 -e 环境变量配置。下表列出每个旋钮及其默认 值:

参数 / pg.yaml 键 默认值 环境变量 含义
--image / image_tag docker.io/dpage/pgadmin4:9.18 — 直接指定镜像标签
--port / host_port 自动,取自 pgadmin_start_port(基址 5050) PGADMIN_LISTEN_PORT Web 宿主端口
--listen / listen 127.0.0.1 PGADMIN_LISTEN_ADDRESS 绑定地址(0.0.0.0 对外开放,见监听与暴露)
--email / email admin@pgcli.lan PGADMIN_DEFAULT_EMAIL Web 登录账号(会经过镜像校验,见排障)
--password / password 自动生成,20 字符 PGADMIN_DEFAULT_PASSWORD Web 登录密码——不是 PG 密码
--dsn / dsn 不设 —(渲染 servers.json + pgpass) 以 URI 预注册一个服务器——带密码时同时预配置(host 是从容器内拨号的,见预置的 host 与 listen 地址)
--pg-name 不设 —(渲染 servers.json + pgpass) 改为从本机已管理实例解析该服务器
--data-dir / data_dir <base-dir>/addon/pgadmin/<name>/data (挂载到 /var/lib/pgadmin) 会话存储位置
— autostart: false — 开机自启(pg autostart enable --pgadmin)

旋钮间的配合关系:

  • email + password 只在空数据目录上生效。 pgAdmin 只在初始化 pgadmin4.db 期间读取 PGADMIN_DEFAULT_*;该文件一旦存在,库里的账号就是 权威,环境变量被忽略。因此复活既有目录的重装会打印一个对它无效的密码(install 会明说)——要真正重置登录,先 --clean-data 移除;复活时也可以把 --password 钉成原值。
  • --dsn 与 --pg-name 互斥——二者都表示"预置这个服务器",一个给 URI, 一个从本实例解析端点。--name 是插件自己的键,与 --pg-name 无关。预置里的 host 是 pgAdmin 的连接出发地,不是你浏览器的位置——见预置的 host 与 listen 地址。
  • listen 默认 loopback——Web UI 有登录门,所以除非传 --listen 0.0.0.0 从别的主机访问(summary 会警告登录成了唯一防线),它保持 127.0.0.1。完整 情形见监听与暴露。
  • email 必须过得了镜像的校验器——entrypoint 用 email_validator 检查它, 直接拒绝保留 TLD(.local、.invalid、.test),命中就崩溃重启。默认值 admin@pgcli.lan 正是为通过校验而选;自己起名时同理(.lan、.internal 这类由你掌控的名字都行——不要求是真实可投递的地址,投递检查是关掉的)。

安装

# 默认:实例名 "pgadmin",端口自 5050 自动分配,loopback,admin@pgcli.lan
pg addon install pgadmin

# 命名 console,用你自己的 Web 登录邮箱
pg addon install pgadmin --name console --email me@example.com

# 用本机已管理的一个实例预置 UI
pg addon install pgadmin --name dev --pg-name proj01

# 用任意远程 DSN 预置
pg addon install pgadmin --name remote \
    --dsn "postgres://readonly@10.0.0.7:5432/appdb"

install summary 打印 URL 与(自动生成的)Web 登录:

✓ pgAdmin installed: "console"
  Container:  pgcli-pgadmin-default-console
  Image:      docker.io/dpage/pgadmin4:9.18
  Data:       ~/pg/addon/pgadmin/console/data
  URL:        http://127.0.0.1:5050/
  Seeded:     proj01
  Seed auth:  password pre-configured (pgpass) — no prompt on first connect

  Login email:    me@example.com
  Login password: <generated>
  (Web login only — not a PostgreSQL password. Retrieve it later with `pg addon password pgadmin`.)

  Add servers to browse in the web UI itself, or reinstall with --dsn/--pg-name to pre-seed one.

Seeded: / Seed auth: 行只在传了 --dsn/--pg-name 时出现。当 DSN 携带密 码时 Seed auth: 显示 password pre-configured (pgpass);不带密码时显示 no password in the DSN — first connect will prompt。重复 install 是幂等的: 运行中的容器原样保留,已停的被启动。要让改动的端口/listen/email/预置生效,加 --force(容器按配置重建;数据目录不动)。

用 --force 重建

不带 --force 的 pg addon install pgadmin 对运行中的容器是空操作——它不动 现有配置,只确认容器在跑。当你正要修改某些东西(端口、listen、email、镜像、 预置 DSN、--pg-name 目标、data_dir)时,这恰好是错误的行为:运行中的容器 钉在创建它时的配置上,编辑看似被接受,但直到下次重建前什么都不会改变。

--force 就是应用这些改动的旋钮。它停止运行中的容器、移除它,并按当前配置创建 一个新的;数据目录不动,已存服务器和浏览历史在新容器里回来:

# 把绑定地址改成暴露到网络
pg addon install pgadmin --listen 0.0.0.0 --force

# 把预置服务器换成另一个实例
pg addon install pgadmin --pg-name proj02 --force

# 彻底去掉预置(不带 --dsn / --pg-name + --force)
pg addon install pgadmin --force

# 切到另一个上游镜像标签
pg addon install pgadmin --image docker.io/dpage/pgadmin4:9.19 --force

两个值得说清的坑:

  • email / password 仍然只对空数据目录重新生效。 pgAdmin 只在初始化 pgadmin4.db 期间读取 PGADMIN_DEFAULT_*;该文件一旦存在,库里的账号就是 权威,环境变量被忽略。单 --force 不会重置 Web 登录——要真正重置账号,得配合 --clean-data(先把存储清掉)。
  • 运行中的容器会被重建,不是原样保留。 如果浏览器正开着 UI,--force 会杀 掉会话;下一次页面加载会重新认证新容器。

使用 Web UI

把浏览器指向 summary 里的 URL(在跑 podman 的那台主机上),用登录邮箱 + 密码 签名进入,pgAdmin 自己的服务器树就出现了。在 UI 里添加连接,或在安装时用 –dsn / –pg-name 预注册一个。

如果主机不在你的浏览器旁边:设 --listen 0.0.0.0(登录成了唯一防线):

监听与暴露

--listen(配置键 listen)是 pgAdmin HTTP 服务绑定的地址,默认 loopback。 三种姿态如下——都要配 --force,因为容器需重建才会采纳改动:

# 仅 loopback(默认)——只有跑 podman 的那台主机能访问
pg addon install pgadmin --name console --listen 127.0.0.1 --force

# 全部网卡——其他主机可访问;登录成了唯一防线
pg addon install pgadmin --name console --listen 0.0.0.0 --force

# 暴露过之后再收回 loopback
pg addon install pgadmin --name console --listen 127.0.0.1 --force
  • Linux 上容器共享宿主网络,所以 --listen 0.0.0.0 会在 <宿主IP>:<端口> 应答,--listen 127.0.0.1 只在 127.0.0.1:<端口>。放宽到 0.0.0.0 意味着 pgAdmin 之外只剩邮箱/密码登录这一道门——install 会警告这一 点——因此除非网络可信,保持 loopback。
  • macOS 上情况相反:bridge 网络在 podman machine 内部总是绑 0.0.0.0, pgcli 发布端口,所以 listen 在那里实际上是恒被放宽的,你从 Mac 经 gvproxy 用 127.0.0.1:<端口> 访问 UI。见平台支持。

取回 Web 登录

pg addon password pgadmin --name console          # 裸打印,供脚本用
pg addon password pgadmin --name console --file ./pw.txt   # 写成 0600 文件

pg addon list 显示 URL 和登录邮箱,但不加 --show-password 绝不打印密码。

预置服务器

--dsn 与 --pg-name 都渲染一份一次性的 servers.json,pgAdmin 在首次启动 时导入,浏览器打开时那个服务器已在树里。--pg-name 从本机已管理实例解析端点 (其 host/port/database/user 及密码,读自 pg.yaml);--dsn 以 URI 给出:

pg addon install pgadmin --name dev --pg-name proj01
# 或
pg addon install pgadmin --name dev --dsn "postgres://app:secret@127.0.0.1:5432/appdb"

预置携带什么、不携带什么:

  • 目标服务器的密码在 DSN 携带时被预配置。 servers.json 本身不能持有密码 (pgAdmin 拒绝导入),所以 pgcli 在旁边写一个 libpq pgpass 文件,并通过 ConnectionParameters.passfile 把预置条目指向它——这是 pgAdmin 9.18 导入/导 出服务器文档中正式记录的绝对路径机制。浏览器里的连接完全跳过密码输入。不带 密码的 DSN(如 postgres://app@host/db)只渲染 servers.json,pgAdmin 在首 次连接时照常提示输入。
  • pgpass 文件以明文在磁盘上持有密码。 它位于 <base-dir>/addon/pgadmin/<name>/pgpass,mode 0600,只有宿主用户和容器内的 uid 5050 可读——但它确实在磁盘上,不像 servers.json 从不持有机密。权衡与 所有使用 .pgpass 的工具相同:免密连接以磁盘上的一个文件为代价。移除 pgAdmin 实例(无论是否带 --clean-data)会把 pgpass 与 servers.json 一并删除——明文 机密不会比实例活得更久。
  • 它是一次性便利,不是耦合。 安装后服务器活在 pgAdmin 自己的 pgadmin4.db 里;在 UI 里增、删、改名都行。无论预置目标是否可达,插件照常运行。
  • PGADMIN_REPLACE_SERVERS_ON_STARTUP=True 在有预置时被设置,所以改了 --dsn/--pg-name 的 --force 重装会声明式地重新应用预置,而不是留下过期 条目。

要去掉预置并停止声明式重载,用 --force 重装且不带 --dsn/--pg-name。

预置的 host 与 listen 地址

这两个旋钮指向不同的机器,却常被混为一谈。--listen 决定谁能访问 pgAdmin 的 Web UI;预置里的 host 决定pgAdmin 从哪里拨数据库。拨号是服务端动作—— 由 pgAdmin 容器发起,不是你的浏览器——所以用 --pg-name 预置本机管理的实例 时,记录 127.0.0.1 是正确的:pgAdmin 与那个 PostgreSQL 共享 podman 宿主 的回环。把 UI 暴露到网络并不要求预置的 host 对浏览器可路由:

# pgAdmin 在 vm01 上前置 vm01 自己的实例,但 UI 供其他主机访问
pg addon install pgadmin --listen 0.0.0.0 --pg-name demo --force
# servers.json -> Host: 127.0.0.1, Port: <demo 的 pg 端口>(容器内拨号)

# 改为预置另一台主机上的服务器——该 host 必须从 pgAdmin 容器可达,
# 因为发起连接的是容器
pg addon install pgadmin --listen 0.0.0.0 \
    --dsn "postgres://app@10.0.0.12:35432/appdb" --force

反过来的坑:用 --dsn 预置一个远程 host,再从别处用浏览器访问那个 pgAdmin——连接依然源自 pgAdmin 容器内部,所以那个 host 必须从容器的网络 可达,而不是从你的笔记本可达。

端口

每个实例从 pgAdmin 自己的端口池 pgadmin_start_port(默认 5050)取一个 端口——与 Redis、Predixy、对象存储的池各自独立游标,所以一个 pgadmin、一个 redis、一个 minio 实例永不撞车:

pg addon install pgadmin --name console   # 5050
pg addon install pgadmin --name audit     # 5051

--port 可把实例钉在特定端口;自动分配会跳过任何已显式指定或宿主上已占用的 端口。

配置

实例位于 pg.yaml 的顶层 addons.pgadmin map:

namespace: default
pgadmin_start_port: 5050     # pgAdmin 自己的端口池
addons:
  pgadmin:
    console:
      container_name: pgcli-pgadmin-default-console
      name: console
      image_tag: docker.io/dpage/pgadmin4:9.18
      # data_dir: /srv/pgadmin         # 省略则为 <base-dir>/addon/pgadmin/console/data
      listen: 127.0.0.1                # 默认;0.0.0.0 对网络开放
      host_port: 5050
      email: me@example.com            # Web 登录账号(PGADMIN_DEFAULT_EMAIL)
      password: <generated>            # Web 登录密码;首次 install 时写入
      # dsn: postgres://app@127.0.0.1:5432/appdb   # 由 --dsn/--pg-name 设置:预置
      # server_name: app               # 预置服务器的显示名
      autostart: false                 # pg autostart enable --pgadmin --name console

对 listen、host_port、email、password、image_tag、dsn/ server_name、data_dir 的修改,在下一次 pg addon install pgadmin --name console --force(或对应 flags)后生效。记住: email/password 只对空数据目录重新生效,所以对既有存储改它们需要先 --clean-data(或接受生效账号仍是原账号)。

列出

pg addon list
Web add-ons (pgadmin):
  pgadmin (name: console)
    Status:      running
    URL:         http://127.0.0.1:5050/
    Login email: me@example.com
    Seeded:      proj01
    Data:        ~/pg/addon/pgadmin/console/data
    Image:       docker.io/dpage/pgadmin4:9.18
    Container:   pgcli-pgadmin-default-console

pg addon list 从不打印密码。要刻意揭示,加 --show-password(每实例追加一行 Login password:),或用 pg addon password pgadmin --name console 裸打印—— 并且优先用 --file,让机密留在文件里而不是 shell 历史与回滚缓冲。

启停

pg addon start pgadmin --name console
pg addon stop  pgadmin --name console

start 只启动已存在的容器(并以按配置重建来自愈不当状态)。数据目录跨 stop/start 保留,pgAdmin 的已存服务器与浏览历史随之回来。

开机自启

容器带 --restart unless-stopped 策略(管崩溃,不管重启)。要在宿主重启后拉起 实例:

pg autostart enable --pgadmin --name console

pgAdmin 自启是只启动(先 install)。开机时 entrypoint 重新 chown /var/lib/pgadmin,并从数据目录重放 pgadmin4.db,已存服务器随之复活。pgAdmin 独立于 PostgreSQL 栈——它是自己拨号的浏览器 UI——所以没有排序约束,开机 pg start --autostart 把它与其他非数据库插件一起启动。

移除

pg addon remove pgadmin --name console               # 容器 + 配置条目;数据保留
pg addon remove pgadmin --name console --clean-data  # 同时删除数据目录

不带 --clean-data 时宿主数据目录(连同 pgadmin4.db)原地保留——重装同名实 例即复活已存服务器与会话存储(复活后可用的登录是原密码,不是新生成的)。 --clean-data 删除它,当 rootless podman 的映射 uid 使 pgAdmin 的文件对宿主用 户不可删时,经 podman unshare rm 兜底。空的实例父目录被修剪;自定义 data_dir 的父目录从不动。servers.json(若预置过)宿主所有,两种移除都随实 例目录一并删掉。

日志

pg logs addon pgadmin --name console        # 最近 50 行
pg logs addon pgadmin --name console -f     # 跟踪

排障

  • 用打印的密码登录被拒 —— 你在保留的数据目录上重装了。能用的登录是 pgAdmin 在首次初始化时写进 pgadmin4.db 的那个;新生成的密码无效(install 会警告 “reviving an existing pgAdmin data dir”)。用 --clean-data 从零开 始,或用 pg addon password pgadmin 确认存的是什么,或重装时把 --password 钉成原值。
  • “pgAdmin could not start” / 容器秒退(或崩溃循环) —— 空目录上 entrypoint 需要 PGADMIN_DEFAULT_EMAIL 和 PGADMIN_DEFAULT_PASSWORD 都在;手改过 pg.yaml、密码为空且数据目录被清过,就两个都没有。让 install 生成,或传 --password。它还会校验邮箱并拒绝保留 TLD(.local、.invalid、 .test),报 “does not appear to be a valid email address”——容器随即退出, 在重启策略下反复自旋。这正是默认值取 admin@pgcli.lan 而非 .local 的原因。
  • 从别的机器打不开 UI —— listen 默认 127.0.0.1。--listen 0.0.0.0 --force 重装并接受 Web 登录成为唯一防线。
  • 预置的服务器显示但连不上 —— 若预置 DSN 不带密码,pgAdmin 在首次连接时 提示输入(预置服务器)。若 DSN 带了密码却仍然提示,说明 pgpass 文件失效了——最常见原因是容器启动时 entrypoint 的 chown 没跑(例如 直接传了 --user 5050 而不是 --user 0,导致文件属主是宿主用户,libpq 静默 丢弃它)。确认 host/port 从 pgAdmin 所在处可达。
  • install 时找不到镜像 —— docker.io/dpage/pgadmin4 按需拉取;离线宿主先 podman load 对应 tar(目录与导出步骤在仓库 docs/images.md),pull 会被跳 过。
  • 首次启动响应慢 —— pgAdmin 在 gunicorn 绑定之前先跑 sqlite 迁移;端口要 等它完成才回 302(跳 /login)。负载高的宿主上这可能要几十秒。

已知限制

  • 无 TLS —— 这里 pgAdmin 提供明文 HTTP。要 HTTPS 就在前面架反向代理(配合 PGADMIN_URL_SCHEME/PGADMIN_DISABLE_STATIC_FILE_SERVER 调参);pgcli 不接 这条线。loopback 默认让常见场景留在 127.0.0.1。
  • 不适合大数据量导入导出 —— pgAdmin 的数据流经 HTTP(导出:查询 → 容器 → 浏览器;导入:上传 → 容器 → 数据库),大表容易超时或内存溢出。应改用 pg export / pg import(pgcli 实例),或 pg exec --dsn "..." -- pg_dump(任意数据库)。详见 数据导入导出。
  • 预置密码以明文存于磁盘 —— servers.json 旁边的 pgpass 文件是 mode 0600 但没有加密;移除实例时一并清理。没有办法预配置密码而不落盘文件(pgAdmin 没有 KMS/keyring 集成)。
  • 登录只对新存储生效 —— pgAdmin 只在初始化 pgadmin4.db 期间读取 PGADMIN_DEFAULT_*;该文件一旦存在,库里的账号就是权威。没有原地改密码的路, 只有 --clean-data + 重装。
  • macOS 代码完备、未实测 —— bridge 路径(发布端口、bridge 下 proxyBindHost 把 loopback 放宽到 0.0.0.0)与 redis 同构;尚未在 Mac 上演 练过。