pgAdmin:查询工作面、共享部署与本地资产治理
从本机实例到第一条查询
pgAdmin 是 PostgreSQL 的图形管理工作面:浏览器中的 Object Explorer 展示对象,Query Tool 在选定连接的会话中执行 SQL。先让两者指向同一个本机合成库,才能分辨界面缓存、连接身份和真正的数据库结果。
用本机合成库完成第一次登录和查询
以下操作在一台 Ubuntu Linux 主机上进行,宿主操作者是有 Docker 权限的普通用户;需要 Docker、OpenSSL、curl 和 ss。pgAdmin 固定使用官方容器镜像 dpage/pgadmin4:9.18,PostgreSQL 样本由下方准备脚本启动。先把数据库准备脚本下载到新的可写目录,阅读后执行 bash prepare.sh。它只在宿主回环发布 PostgreSQL 端口,打印独立的 client-lab.* 实验目录,建好 lab_reader 和三条合成记录;若端口15432已占用,用 CLIENT_LAB_PORT=15433 bash prepare.sh 创建新实验,不停止现有服务。
进入脚本打印的实验目录,读取它生成的 state.env,再启动只监听本机回环的 pgAdmin。PGADMIN_DEFAULT_PASSWORD 是合成登录密码,会留在 Docker 容器环境中;Docker 管理员可读取它,所以这里的做法只用于本机实验,不用作生产凭据交付。
set -euo pipefail
read -r -p 'prepare.sh 打印的实验目录绝对路径: ' LAB_DIR
[[ "$LAB_DIR" = /* ]] || { echo '请输入绝对路径'; exit 1; }
test -f "$LAB_DIR/state.env" || { echo '未找到本次实验状态文件'; exit 1; }
cd "$LAB_DIR"
source ./state.env
test "${PWD##*/}" = "$LAB_NAME"
test "$(docker inspect -f '{{index .Config.Labels "lab-name"}}' "$LAB_NAME")" = "$LAB_NAME"
PGADMIN_NAME="${LAB_NAME}-pgadmin"
PGADMIN_VOLUME="${LAB_NAME}-pgadmin-data"
test ! -e pgadmin.env || { echo '已有 pgadmin.env;不要覆盖旧登录凭据'; exit 1; }
if docker container inspect "$PGADMIN_NAME" >/dev/null 2>&1 ||
docker volume inspect "$PGADMIN_VOLUME" >/dev/null 2>&1; then
echo '本次 pgAdmin 容器或数据卷已存在;先检查旧实例' >&2
exit 1
fi
test -z "$(ss -H -lnt '( sport = :5050 )')" || { echo '5050 已占用'; exit 1; }
umask 077
printf 'PGADMIN_DEFAULT_EMAIL=lab-admin@example.com\nPGADMIN_DEFAULT_PASSWORD=%s\nPGADMIN_DISABLE_POSTFIX=1\nPGADMIN_LISTEN_ADDRESS=127.0.0.1\nPGADMIN_LISTEN_PORT=5050\n' \
"$(openssl rand -hex 24)" > pgadmin.env
chmod 600 pgadmin.env
docker volume create --label article-lab=data-client \
--label "lab-name=$LAB_NAME" --label role=pgadmin "$PGADMIN_VOLUME"
docker run -d --name "$PGADMIN_NAME" --network host \
--label article-lab=data-client --label "lab-name=$LAB_NAME" \
--env-file ./pgadmin.env \
--mount "type=volume,src=$PGADMIN_VOLUME,dst=/var/lib/pgadmin" \
dpage/pgadmin4:9.18
curl -q --noproxy '*' --fail-with-body --retry 20 --retry-connrefused \
--retry-delay 1 -L -sS -o /dev/null \
-w '%{http_code}\n' http://127.0.0.1:5050/
ss -H -lnt '( sport = :5050 )'curl 应返回 200,监听行的本地地址应为 127.0.0.1:5050。持续拒绝连接或返回 HTTP 错误时看 docker logs "$PGADMIN_NAME",不要改为全接口监听来绕过故障。重跑前先核对旧容器和 pgadmin.env,已有配置数据库时重新生成 PGADMIN_DEFAULT_PASSWORD 不会自动重置旧管理员密码。这里使用 Linux Docker host 网络,pgAdmin 容器中的 127.0.0.1 与宿主指向同一个回环网络栈;只因为 PGADMIN_LISTEN_ADDRESS 明确设为回环,Web 入口才没有暴露在其他宿主接口。镜像的变量和状态目录见 pgAdmin 9.18 容器文档。
在本机浏览器打开 http://127.0.0.1:5050/,用 lab-admin@example.com 和 pgadmin.env 中的合成密码登录。按注册 Server 的官方入口在左侧 Servers 节点右键选择 Register → Server,General 名称填 client-postgres-local-ro;Connection 页 Host 填 127.0.0.1、Port 填 state.env 的 LAB_PORT、Maintenance database 填 client_lab、Username 填 lab_reader,密码从同目录 secrets/reader.password 读取。若 pgAdmin 要求设置 Master Password,它用于保护已保存的 Server 密码,与 Web 登录密码和数据库密码是三个不同对象;本机实验也不应把它们混用。本机实验不启用 SSH/TLS;远程连接另按连接路径与证书身份配置。保存后展开 client_lab → Schemas → lab → Tables → client_guard_lab,在该 database 的 Query Tool 先执行前两条,再单独执行拒写语句:
SELECT current_database(),current_user,current_schema();
SELECT id,status FROM lab.client_guard_lab ORDER BY id;
UPDATE lab.client_guard_lab SET status='X' WHERE id=1;前两条应分别返回 client_lab / lab_reader / lab 和三行 CREATED、PAID、CREATED;第三条必须得到 PostgreSQL 权限拒绝。若 Query Tool 处于手动提交模式,先执行 ROLLBACK 退出失败事务,再查询 id=1,状态仍应为 CREATED。注册成功但缺表时先核对当前 database、schema 与准备脚本输出;认证失败先检查 lab_reader 密码,连接拒绝则核对 PostgreSQL 容器和 LAB_PORT。这些判断分别来自实际 Query Tool 结果,不能用 pgAdmin 登录页可打开代替。
对象树看得到,SQL 仍可能找不到表
Object Explorer 可以展开目标表,却不能替当前 Query Tool 会话回答“这条未限定 schema 的 SQL 最终解析到哪里”。Server 注册能看到表而应用查询仍报 relation does not exist 时,核对 pgAdmin 与应用的 database、登录 role、SET ROLE 和 search_path,不要从对象树推断当前编辑器的身份。
在 Query Tool 读取服务端状态:
SELECT current_user,
session_user,
current_database(),
current_schema(),
current_schemas(false);
SHOW search_path;
SHOW transaction_read_only;session_user 是最初登录身份,current_user 会在 SET ROLE 后改变并参与权限检查。对象树能用另一个连接刷新,Query Tool 也能切换连接;因此“左边看到了表”不是当前编辑器的身份材料。需要在终端或流水线复现同一判断时,使用 PostgreSQL 与 psql 的发行内命令行入口,不在 pgAdmin 页面再维护第二套 CLI 教程。
先保留本次 pgAdmin 容器与卷,后面的冷备和恢复还要使用刚注册的 Server、Query History 与登录身份。实验结束后的精确清理步骤放在恢复验证之后;仅关闭浏览器不会删除这些服务端状态。
Desktop mode 与 Server mode 不是同一份运行责任
pgAdmin 官方部署入口把可安装桌面包、Server deployment 与 Container deployment 分开说明。三种形态提供相近的对象浏览和 Query Tool,但账号、文件落点、网络暴露面与退出成本不同。
| 形态 | 进程和用户入口 | 状态主要落点 | 适合的责任边界 |
|---|---|---|---|
| Desktop mode | 单个开发者的本机进程 | 本机配置数据库、历史、证书、查询文件和导出物 | 个人深度排障,终端由设备管理体系接管 |
| Server mode | 团队通过浏览器登录 Web 应用 | 服务端配置数据库、每用户存储、会话和日志 | 统一入口、认证、版本与审计,但需要平台 owner |
| Container | 镜像交付的 Server mode | /var/lib/pgadmin、映射配置、证书与外部数据库 | 可复现交付,不会自动获得高可用或安全边界 |
Desktop mode 不是“没有服务端状态”,只是 Web runtime 跟随本地桌面进程运行,文件和配置数据库仍会留在工作站。Server mode 也不是把一个桌面窗口放到服务器上:它增加 pgAdmin 用户、会话、反向代理、HTTPS、存储配额、备份、恢复和补丁责任。若团队只是偶尔查开发库,为几个人长期维护共享 Web 系统未必划算;若受管终端无法安装工具,统一 Server mode 才可能抵消这些运维成本。
安装完成后,从 About 确认 pgAdmin 自身版本;从 Preferences 的 Paths/Binary paths 检查 pg_dump、pg_dumpall、pg_restore 与 psql 实际路径。界面升级不等于外部 PostgreSQL utilities 同步升级。Backup、Restore 或 PSQL Tool 行为异常时,先验证 binary path 和二进制版本,不要因为 Query Tool 能执行 SELECT 1 就把问题归给数据库服务器。
Container 需要保存的不是一个空目录
官方镜像为 dpage/pgadmin4。共享部署应锁定明确镜像版本,初始化密码通过容器 secret、受控 env file 或平台密钥系统注入,不能写进 Compose、终端历史或截图。此处选择 9.18:它的发行说明列出了此前 Webserver authentication 与 Restore/Maintenance 路径的安全修复;已运行旧版的共享入口应核对自己的认证方式和使用权限,安排受控升级。/var/lib/pgadmin 是核心工作目录,官方容器文档说明这里保存 session data、用户文件、配置文件与配置数据库;挂载后还要让容器内 pgadmin 用户拥有正确读写权限。
在专用 Linux 主机上接入共享 HTTPS 入口
这条路线使用一台仅承载 pgAdmin 的 Ubuntu 主机:Docker Compose 管理一个 pgAdmin 容器,宿主 Nginx 持有组织签发的域名证书,浏览器只从批准的管理网段访问 443。操作者需要 Docker 权限和对 Nginx 配置、证书文件的受控 sudo 权限;域名 pgadmin.example.com、证书路径与 192.0.2.0/24 都是待替换的示例值。证书必须覆盖实际域名且在客户端信任链内;数据库地址另按 Server 注册配置,不能把 Web 域名当成 PostgreSQL 主机。
在新主机上先确认 5050 没有现存监听、/srv/pgadmin-team 不存在,且该主机的 Nginx 没有承载其他站点。准备目录后,以操作者身份输入管理邮箱和已从受控密码系统取得的初始密码;密码文件交给容器内 UID 5050 读取,Compose 操作者通过受限组读取。不要把密码写进 .env、Compose YAML 或终端参数:
set -euo pipefail
docker compose version
sudo nginx -v
test ! -e /srv/pgadmin-team || { echo '目录已存在,先核对旧部署' >&2; exit 1; }
test -z "$(ss -H -lnt '( sport = :5050 )')" || exit 1
if docker volume inspect pgadmin-team-v918-primary >/dev/null 2>&1; then
echo '目标卷已存在' >&2
exit 1
fi
sudo install -d -m 0700 -o "$(id -un)" -g "$(id -gn)" /srv/pgadmin-team
cd /srv/pgadmin-team
umask 077
install -d -m 0700 secrets
read -r -p 'pgAdmin 管理员邮箱: ' PGADMIN_ADMIN_EMAIL
[[ "$PGADMIN_ADMIN_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+$ ]] || {
echo '邮箱只接受常见邮箱字符;不要把未审查的输入写入 .env' >&2
exit 1
}
read -r -s -p '受控密码系统中的初始密码: ' ADMIN_PASSWORD
printf '\n'
test -n "$ADMIN_PASSWORD" || exit 1
printf '%s' "$ADMIN_PASSWORD" > secrets/pgadmin_admin_password.txt
unset ADMIN_PASSWORD
sudo chown "5050:$(id -g)" secrets/pgadmin_admin_password.txt
sudo chmod 0440 secrets/pgadmin_admin_password.txt
printf 'PGADMIN_IMAGE_TAG=9.18\nPGADMIN_DATA_VOLUME=pgadmin-team-v918-primary\nPGADMIN_ADMIN_EMAIL=%s\n' \
"$PGADMIN_ADMIN_EMAIL" > .env
docker volume create --label article-lab=pgadmin-team pgadmin-team-v918-primary把下面内容保存为 /srv/pgadmin-team/compose.yaml。external: true 使卷名由 .env 明确指定;后续切换版本时不会让 Compose 根据当前目录悄悄创建另一份配置库。PGADMIN_DEFAULT_PASSWORD_FILE 是官方容器支持的密码文件入口,Compose Secret在容器内挂到 /run/secrets;配置库已存在时,重改此初始密码文件不会重置旧管理员密码。
name: pgadmin-team
services:
pgadmin:
image: dpage/pgadmin4:${PGADMIN_IMAGE_TAG}
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_ADMIN_EMAIL}
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pgadmin_admin_password
PGADMIN_LISTEN_PORT: "5050"
PGADMIN_DISABLE_POSTFIX: "1"
secrets:
- pgadmin_admin_password
volumes:
- pgadmin-data:/var/lib/pgadmin
ports:
- "127.0.0.1:5050:5050"
restart: unless-stopped
secrets:
pgadmin_admin_password:
file: ./secrets/pgadmin_admin_password.txt
volumes:
pgadmin-data:
external: true
name: ${PGADMIN_DATA_VOLUME}在该目录执行 docker compose config --quiet 和 docker compose up -d pgadmin。docker compose ps 应只有一个 pgAdmin 实例;本机 curl 应获得 HTTP 200,ss 只应看到 127.0.0.1:5050 的宿主发布端口。若容器退出,先用 docker compose logs pgadmin 检查密码文件可读性、UID 5050 的卷权限及启动迁移错误,不把端口改成全接口监听。
cd /srv/pgadmin-team
docker compose config --quiet
docker compose up -d pgadmin
docker compose ps
curl -q --noproxy '*' --fail-with-body --retry 20 --retry-connrefused \
--retry-delay 1 -L -sS -o /dev/null -w '%{http_code}\n' \
http://127.0.0.1:5050/
ss -H -lnt '( sport = :5050 )'把以下 Nginx server 块保存到专用主机的 /etc/nginx/conf.d/pgadmin-team.conf,将五处 pgadmin.example.com、证书与私钥路径、192.0.2.0/24 替换为实际 FQDN、证书位置和批准的管理网段。若 Nginx 已有默认站点或其他 80/443 监听,先审查现有站点与 server_name,不要覆盖其配置。如果域名还发布了 AAAA 记录,须同时配置并检查 IPv6 监听和访问控制;这份仅监听 IPv4 的示例不能证明 IPv6 路径可用。X-Scheme 与 X-Script-Name 必须由代理覆盖或移除客户端输入;pgAdmin 9.18 Server Deployment说明两者会被无条件信任。这里使用根路径,X-Script-Name 为空;X-Forwarded-For 也由当前代理重建,不把客户端提交的同名头接续进去。Nginx HTTPS 文档给出当前 listen 443 ssl 和 TLS 1.2/1.3 写法。
server {
listen 80;
server_name pgadmin.example.com;
return 308 https://pgadmin.example.com$request_uri;
}
server {
listen 443 ssl;
server_name pgadmin.example.com;
ssl_certificate /etc/ssl/pgadmin/fullchain.pem;
ssl_certificate_key /etc/ssl/pgadmin/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
allow 127.0.0.1;
allow 192.0.2.0/24;
deny all;
proxy_pass http://127.0.0.1:5050;
proxy_set_header Host pgadmin.example.com;
proxy_set_header X-Scheme https;
proxy_set_header X-Script-Name "";
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host pgadmin.example.com;
proxy_set_header X-Forwarded-Port 443;
proxy_redirect off;
}
}先检查域名解析和证书 SAN,sudo nginx -t 成功后才重载。用 --resolve 对本机 Nginx 作 TLS 主机名验证;此命令排除了 curlrc 和代理,--resolve 只改变该次连接地址,不改变证书检查所用域名。不能用 -k 绕过尚未配置好的信任链。若回环检查通过而管理网段访问失败,分别核对 DNS、网络 ACL、Nginx allow 与 TLS 错误;若 5050 可用而 443 返回 502,先看 Nginx error log 和回环上游,不改 pgAdmin 登录账号。
read -r -p '实际 pgAdmin FQDN: ' PGADMIN_FQDN
getent ahostsv4 "$PGADMIN_FQDN"
sudo openssl x509 -in /etc/ssl/pgadmin/fullchain.pem -noout -ext subjectAltName
sudo nginx -t
if sudo systemctl is-active --quiet nginx; then
sudo systemctl reload nginx
else
sudo systemctl start nginx
fi
curl -q --noproxy '*' --fail-with-body --resolve "$PGADMIN_FQDN:443:127.0.0.1" \
-L -sS -o /dev/null -w '%{http_code}\n' "https://$PGADMIN_FQDN/"从批准的管理网段打开 https://实际域名/,先用本次管理员登录,再创建非管理员的 pgAdmin 用户并用独立浏览器会话登录;该用户自行注册只读 PostgreSQL Server,执行前面的三行查询和拒写。管理员 Web 登录与数据库 role 是两套权限,不能让所有人共用管理员账号或一条高权限 PostgreSQL 连接。Web 登录、用户隔离、Server 注册、Query History 与文件存储都要作为共享入口的验收对象。servers.json 可以预置 Server 注册,preferences.json 可以初始化偏好,但持久化配置数据库存在后是否再次覆盖取决于启动策略;声明式文件不是天然持续调谐器。
同一个 /var/lib/pgadmin 不能让不同版本的主容器和 init/sidecar 随意同时修改。pgAdmin 配置数据库会向前迁移,新版本进程可能先改变 schema,旧进程随后读取便会失败。共享卷升级应让所有接触该卷的容器使用相同版本,并采用 Recreate 思路先停旧实例、再启动新实例。升级前备份配置数据库和用户文件,测试恢复后再切换;“滚动时两个版本都能打开登录页”不是兼容证明。
已有 9.17 单实例升到 9.18
以下只适用于按上面 compose.yaml 管理、.env 仍为 PGADMIN_IMAGE_TAG=9.17 的现存专用主机;新建的 9.18 实例无需执行。升级前通知用户停止 Query Tool、导出和文件上传,确认没有需提交的事务,并记录用户、Server 注册和一条可复查的历史查询。专用 Nginx 暂停后,外部请求不再进入;旧 pgAdmin 停止后才复制卷。旧卷始终保持不变,新版本只读写候选卷,配置数据库向前迁移不会污染回退来源。
在共享主机的 /srv/pgadmin-team 目录,以原 Compose 操作者执行预检。.env.preupgrade、.env.candidate 或候选卷已存在时先核对,不覆盖上一次尝试。实际挂载名必须与 .env 一致;若服务器使用外部配置数据库或多实例,这条单卷路线不适用,应同时规划其一致性与切换。
set -euo pipefail
cd /srv/pgadmin-team
test "$(grep -c '^PGADMIN_IMAGE_TAG=' .env)" -eq 1 || exit 1
test "$(grep -c '^PGADMIN_DATA_VOLUME=' .env)" -eq 1 || exit 1
test "$(grep -c '^PGADMIN_ADMIN_EMAIL=' .env)" -eq 1 || exit 1
PGADMIN_IMAGE_TAG="$(sed -n 's/^PGADMIN_IMAGE_TAG=//p' .env)"
PGADMIN_DATA_VOLUME="$(sed -n 's/^PGADMIN_DATA_VOLUME=//p' .env)"
PGADMIN_ADMIN_EMAIL="$(sed -n 's/^PGADMIN_ADMIN_EMAIL=//p' .env)"
test "$PGADMIN_IMAGE_TAG" = 9.17 || exit 1
[[ "$PGADMIN_DATA_VOLUME" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || exit 1
[[ "$PGADMIN_ADMIN_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+$ ]] || exit 1
test ! -e .env.preupgrade && test ! -e .env.candidate || exit 1
NEW_VOL=pgadmin-team-v918-candidate
if docker volume inspect "$NEW_VOL" >/dev/null 2>&1; then
echo '候选卷已存在;不要覆盖' >&2
exit 1
fi
CID="$(docker compose ps -q pgadmin)"
test -n "$CID" || exit 1
test "$(docker inspect "$CID" --format '{{range .Mounts}}{{if eq .Destination "/var/lib/pgadmin"}}{{.Name}}{{end}}{{end}}')" = "$PGADMIN_DATA_VOLUME" || exit 1
test "$(docker volume inspect -f '{{index .Labels "article-lab"}}' "$PGADMIN_DATA_VOLUME")" = pgadmin-team || exit 1确认对外停机窗口已经生效后,在同一 Bash 终端停止入口与旧进程,再从只读挂载的旧卷制作冷备;备份文件的归属交回宿主操作者。归档失败时维持停机状态、保留旧卷并排查,不执行下一段升版:
sudo systemctl stop nginx
docker compose stop --timeout 60 pgadmin
test "$(docker inspect -f '{{.State.Running}}' "$CID")" = false || exit 1
BACKUP_DIR="$(mktemp -d "$PWD/upgrade-backup.XXXXXX")" || exit 1
chmod 700 "$BACKUP_DIR"
docker run --rm --network none --user 0:0 \
-e BACKUP_UID="$(id -u)" -e BACKUP_GID="$(id -g)" \
--mount "type=volume,src=$PGADMIN_DATA_VOLUME,dst=/source,readonly" \
--mount "type=bind,src=$BACKUP_DIR,dst=/backup" \
alpine:3.21 sh -ec 'tar -C /source -czf /backup/pgadmin-preupgrade.tgz .; chmod 600 /backup/pgadmin-preupgrade.tgz; chown "$BACKUP_UID:$BACKUP_GID" /backup/pgadmin-preupgrade.tgz'
test -s "$BACKUP_DIR/pgadmin-preupgrade.tgz" || exit 1
(cd "$BACKUP_DIR" && sha256sum pgadmin-preupgrade.tgz > pgadmin-preupgrade.sha256)恢复归档到空的候选卷,交还容器内 UID/GID 5050;.env.candidate 指向该卷和 9.18,.env.preupgrade 保留旧 tag 与旧卷。Compose 会替换停止的服务容器,但不能同时运行旧进程与新进程去操作同一卷。
(cd "$BACKUP_DIR" && sha256sum -c pgadmin-preupgrade.sha256) || exit 1
docker volume create --label article-lab=pgadmin-team "$NEW_VOL"
docker run --rm --network none --user 0:0 \
--mount "type=volume,src=$NEW_VOL,dst=/restore" \
--mount "type=bind,src=$BACKUP_DIR,dst=/backup,readonly" \
alpine:3.21 sh -ec 'test -z "$(ls -A /restore)"; tar -C /restore -xzf /backup/pgadmin-preupgrade.tgz; chown -R 5050:5050 /restore'
cp -p .env .env.preupgrade
printf 'PGADMIN_IMAGE_TAG=9.18\nPGADMIN_DATA_VOLUME=%s\nPGADMIN_ADMIN_EMAIL=%s\n' \
"$NEW_VOL" "$PGADMIN_ADMIN_EMAIL" > .env.candidate
docker compose --env-file .env.candidate config --quiet
docker compose --env-file .env.candidate up -d --no-deps pgadmin
curl -q --noproxy '*' --fail-with-body --retry 20 --retry-connrefused \
--retry-delay 1 -L -sS -o /dev/null -w '%{http_code}\n' \
http://127.0.0.1:5050/HTTP 200 只到登录页。入口仍关闭时,可由获准的运维工作站使用 SSH 本地转发 ssh -N -L 15050:127.0.0.1:5050 管理员@共享主机,在工作站访问 http://127.0.0.1:15050/;SSH 身份、主机指纹和本地端口需先核对。以升级前的 pgAdmin 用户登录,检查原 Server 注册、Query History、Storage Manager 文件和只读 PostgreSQL 查询;配置数据库中的旧管理员密码不因初始 secret 文件而改变。应用日志若有 schema 迁移错误、页面缺用户或旧历史,不打开 Nginx。
验证通过后先将候选 .env 设为正式配置,再启动 Nginx,使用前文 HTTPS curl --resolve 命令与管理网段浏览器重新验证登录和查询。保留旧卷、.env.preupgrade 和冷备直至确认保留期限,不能在本次切换时执行 docker compose down -v。
mv .env.candidate .env
sudo systemctl start nginx
docker compose ps若在对外入口重新开放前失败,先停止候选服务,用 .env.preupgrade 重建旧版容器;旧卷尚未被新版本碰触,恢复后检查旧用户、历史和数据库查询,再恢复 Nginx。若已移动候选文件但尚未开放入口,下面也会先停止当前服务并恢复旧 .env。保留失败候选卷供诊断,不直接删除。
test -s .env.preupgrade || exit 1
if test -e .env.candidate; then
docker compose --env-file .env.candidate stop pgadmin
else
docker compose --env-file .env stop pgadmin
fi
cp -p .env.preupgrade .env
docker compose --env-file .env.preupgrade up -d --no-deps pgadmin
curl -q --noproxy '*' --fail-with-body --retry 20 --retry-connrefused \
--retry-delay 1 -L -sS -o /dev/null -w '%{http_code}\n' \
http://127.0.0.1:5050/
sudo systemctl start nginx对外重新开放后,新版本可能已经新增用户、Server 注册、历史或上传文件;此时旧卷是停写时的快照,不能直接用它回切并声称没有数据损失。先再次冻结入口,保留当前候选卷,核对新增状态并选择修复后继续运行 9.18,或制订明确的数据迁移与用户补录方案。配置数据库降级不属于这条操作链。
冷备配置卷并在独立入口验证恢复
前面的本机实例已有管理员登录、Server 注册和 Query History;它们保存在 pgAdmin 卷中,PostgreSQL 的表和数据仍由另一数据库容器保存。下面继续使用首次实验的同一 Bash 终端和 $LAB_DIR,先核对容器标签及实际挂载,再停 pgAdmin 取得一致的文件快照。备份目录由宿主当前用户持有,临时备份容器以 root 身份读取 UID 5050 的卷,写完把归档所有权交还宿主用户;本机 Docker 管理员仍可读卷和备份,不把它当作多租户保密方案。
test "$PWD" = "$LAB_DIR" || exit 1
test "$(docker inspect -f '{{index .Config.Labels "lab-name"}}' "$PGADMIN_NAME")" = "$LAB_NAME" || exit 1
test "$(docker inspect "$PGADMIN_NAME" --format '{{range .Mounts}}{{if eq .Destination "/var/lib/pgadmin"}}{{.Name}}{{end}}{{end}}')" = "$PGADMIN_VOLUME" || exit 1
test "$(docker volume inspect -f '{{index .Labels "lab-name"}}' "$PGADMIN_VOLUME")" = "$LAB_NAME" || exit 1
BACKUP_DIR="$(mktemp -d "$PWD/pgadmin-backup.XXXXXX")" || exit 1
chmod 700 "$BACKUP_DIR"
docker stop "$PGADMIN_NAME" || exit 1
docker run --rm --network none --user 0:0 \
-e BACKUP_UID="$(id -u)" -e BACKUP_GID="$(id -g)" \
--mount "type=volume,src=$PGADMIN_VOLUME,dst=/source,readonly" \
--mount "type=bind,src=$BACKUP_DIR,dst=/backup" \
alpine:3.21 sh -ec 'tar -C /source -czf /backup/pgadmin-data.tgz .; chmod 600 /backup/pgadmin-data.tgz; chown "$BACKUP_UID:$BACKUP_GID" /backup/pgadmin-data.tgz' || exit 1
test -s "$BACKUP_DIR/pgadmin-data.tgz" || exit 1
(cd "$BACKUP_DIR" && sha256sum pgadmin-data.tgz > pgadmin-data.sha256)
docker start "$PGADMIN_NAME" || exit 1BACKUP_DIR 中应有非空的 pgadmin-data.tgz 和摘要文件。若归档失败,原实例仍处于停止状态,先保留卷和日志、修复备份问题,再明确选择是否启动原实例;不要因备份失败直接删卷。docker export 不包含挂载卷内容,Docker 卷备份说明采用单独挂载卷并打包的方式。正式环境还需把归档和恢复所需的登录/加密材料交给受控备份系统,验证异地保留与解密,而不是只留下本机目录。
恢复不覆盖原卷。在 5051 空闲时创建独立卷,校验归档后解包,并把工作目录交回容器内的 5050 身份;恢复容器仍只在宿主回环监听。以下命令依赖同一 Bash 中的 BACKUP_DIR、LAB_NAME 和 PGADMIN_NAME:
test "$PWD" = "$LAB_DIR" || exit 1
test -s "$BACKUP_DIR/pgadmin-data.tgz" && test -s pgadmin.env || exit 1
(cd "$BACKUP_DIR" && sha256sum -c pgadmin-data.sha256) || exit 1
test -z "$(ss -H -lnt '( sport = :5051 )')" || { echo '5051 已占用'; exit 1; }
PGADMIN_RESTORE_NAME="${LAB_NAME}-pgadmin-restore"
PGADMIN_RESTORE_VOLUME="${LAB_NAME}-pgadmin-restore-data"
if docker container inspect "$PGADMIN_RESTORE_NAME" >/dev/null 2>&1 ||
docker volume inspect "$PGADMIN_RESTORE_VOLUME" >/dev/null 2>&1; then
echo '恢复目标已存在;不要覆盖已有容器或卷' >&2
exit 1
fi
docker volume create --label article-lab=data-client \
--label "lab-name=$LAB_NAME" --label role=pgadmin-restore "$PGADMIN_RESTORE_VOLUME"
docker run --rm --network none --user 0:0 \
--mount "type=volume,src=$PGADMIN_RESTORE_VOLUME,dst=/restore" \
--mount "type=bind,src=$BACKUP_DIR,dst=/backup,readonly" \
alpine:3.21 sh -ec 'test -z "$(ls -A /restore)"; tar -C /restore -xzf /backup/pgadmin-data.tgz; chown -R 5050:5050 /restore' || exit 1
docker run -d --name "$PGADMIN_RESTORE_NAME" --network host \
--label article-lab=data-client --label "lab-name=$LAB_NAME" \
--env-file ./pgadmin.env -e PGADMIN_LISTEN_PORT=5051 \
--mount "type=volume,src=$PGADMIN_RESTORE_VOLUME,dst=/var/lib/pgadmin" \
dpage/pgadmin4:9.18
curl -q --noproxy '*' --fail-with-body --retry 20 --retry-connrefused \
--retry-delay 1 -L -sS -o /dev/null -w '%{http_code}\n' \
http://127.0.0.1:5051/在 5051 用原管理员身份登录:原来注册的 client-postgres-local-ro 应仍在;打开它的 Query Tool 查询三条样本,并在 Query History 找到冷备之前的查询。Server 连接密码若由 Master Password 保护,仍需原 Master Password;若数据库端密码已轮换,要输入新密码,不能把认证失败误判为卷恢复失败。5051 能打开登录页但用户、Server 或历史缺失时,先检查恢复卷挂载、归档内容和启动日志,不切换共享入口。这个实验检验配置卷在同版 9.18 上的冷备恢复;它没有恢复 PostgreSQL 业务数据,也没有验证跨版本配置数据库降级。
Server 注册描述连接,不授予权限
Register Server 对话框把 PostgreSQL 连接参数拆到多个页签。Host、Port 与 Maintenance database 决定初始终点;Username 是登录身份,Role 可以在连接后请求切换;SSL mode、root certificate、client certificate 与 key 决定 libpq 的 TLS 行为;SSH tunnel 则在数据库连接之前多加一段跳板链路。
| 字段或状态 | 应验证的服务端事实 | 容易出现的误判 |
|---|---|---|
| Maintenance database | 登录 role 对该 database 是否有 CONNECT | 业务库可用,却因维护库拒绝而注册失败 |
| Username / Role | session_user、current_user 与成员关系 | 登录成功被当成已获得目标 role |
| SSL mode | 是否校验证书链与主机身份 | require 被当成 verify-full |
| SSH tunnel | 跳板身份、转发终点和私钥落点 | SSH 成功被当成 pg_hba.conf 已放行 |
| Save password | 凭证保存位置、轮换与退出动作 | 删除 Server 注册被当成所有副本已清除 |
安全敏感环境使用 verify-full,并让连接主机名与证书 SAN 对齐。SSH 页只证明隧道如何建立,数据库页仍要通过 pg_hba.conf、TLS 和 role 认证。Server mode 上传的 CA、客户端证书或私钥会进入服务端每用户存储与备份威胁模型;个人 SSH 私钥不应上传成公共平台文件。
连接后先在 Query Tool 执行身份 SQL,再做拒写实验。连接颜色、分组和名称可以降低误操作概率,却不能替代服务端 GRANT、行级安全与只读事务。只读账号应让下面的写操作收到权限拒绝:
CREATE TABLE app.should_fail(id integer);
UPDATE app.app_order SET status = 'CANCELLED' WHERE id = 2;若写操作成功,应追踪直接授权、role membership、PUBLIC 权限和对象 owner,而不是在 pgAdmin 里再加一个“readonly”标签。对象权限、search_path、MVCC、VACUUM、WAL 与恢复机制仍由 PostgreSQL 服务端负责,pgAdmin 只是连接和呈现入口。
Query Tool 最危险的默认值往往很方便
Query Tool 官方说明把 SQL Editor、Data Output、Messages、Notifications、Explain 与 Query History 放在同一工作面。执行整段脚本、执行选区和执行光标所在语句是不同动作;SQL 很长时,先确认被下划线识别的语句或明确选区,再点击执行。浏览器焦点和光标位置不应成为生产变更的唯一边界。
Auto commit 打开时,每条成功语句会自动提交;关闭后则要显式 COMMIT 或 ROLLBACK。Auto rollback on error、退出时提示未提交事务等偏好是本地防护,不会改变其他会话看到的锁和事务。Query Tool 状态栏应显示连接与事务状态,维护操作前再从服务端检查:
SELECT current_user, current_database(), current_schema();
SELECT txid_current_if_assigned();
SHOW transaction_read_only;图形 Explain 适合看节点、成本、估算行数和数据流,但它呈现的仍是 PostgreSQL 计划。EXPLAIN ANALYZE 会真实执行语句;对 DML 或高成本查询可能产生写入或负载。生产先读取不带 ANALYZE 的估算计划,需要 buffers、timing 或 WAL 实际数据时,应在只读事务、影子数据或批准的诊断窗口运行,并保留文本/JSON 计划以便比较。
Query Tool 的 server-side cursor 可以分批读取大结果集,降低一次性装入浏览器的数据量,但它要求事务模式,分页能力也会变化。它不是无限结果集许可证:数据库仍要执行查询,网络仍要传输数据,Server mode worker、浏览器内存和用户等待时间仍受影响。先给 SQL 加明确过滤和 LIMIT,再决定是否启用 server-side cursor。
View/Edit Data 与 Query Tool 共用部分界面,却不是相同风险。可编辑结果集可能直接生成 UPDATE;只有看到铅笔或锁图标并不能证明服务端权限合理。高权限连接尽量禁用随手编辑,生产观察账号只授予必要的 SELECT。Query Tool 的 AI Assistant 若被启用,还要单独确认 schema 元数据、提示内容和生成 SQL 发往哪个提供方;生成结果必须经过人工审阅和服务端权限裁决,不能直接执行。
Query History 是数据库维度的长期副本
Query History 会记录 SQL 文本、返回行数、执行耗时和服务端消息。官方说明其在 Query Tool 模式下按用户、按 database 跨会话保留;View/Edit Data 模式的历史行为不同。这里可能出现真实业务条件、内部域名、表名、令牌字面量和个人数据,不能把它当成无害的最近命令菜单。
共享 Server mode 需要明确历史保留上限、清理入口、配置数据库备份范围和管理员可见性。使用 Remove 或 Remove All 只处理当前 pgAdmin 保存的历史,不会同步删除浏览器剪贴板、下载目录、反向代理日志、数据库审计日志和备份副本。删除前保留必要的脱敏故障材料,随后按各自 owner 清理。
“保存应用状态”可以恢复 Query Tool、ERD、Schema Diff 或 PSQL workspace 的部分标签页状态,这会提高崩溃恢复体验,也会扩大服务端保存的编辑内容。共享平台启用前应确认加密、配置数据库、密钥与备份恢复路径;离职回收不能只禁用数据库 role,还要撤销 pgAdmin 用户、活动会话、保存的 Server、宏、历史和存储文件。
Storage Manager 会改变数据出域位置
Storage Manager中的 My Storage 是当前 pgAdmin 用户的服务端存储目录;管理员还可以配置 shared storage。Backup、Export 或 Import 产生的文件可能先落在 Server mode 主机或持久卷,再由浏览器下载到终端。一个按钮点击会同时跨越数据库、pgAdmin 存储和用户设备三层边界。
导出前先回答列级敏感性、最大行数、目标目录、保留时间和接收人。共享存储若开放写入,要防止用户覆盖他人文件或把未经审查的 SQL、CSV、证书放进公共区;只读共享目录也要限制内容来源。上传大小限制只能保护部分资源,不能替代磁盘配额、文件类型约束、恶意内容扫描、过期清理和备份策略。
pgAdmin 的 Backup/Restore 调用外部 PostgreSQL utilities。工具版本、binary path、格式、owner、ACL、extension 与恢复目标都影响结果;界面任务显示成功,不等于恢复后的 schema、权限、sequence 和数据完整。生产备份恢复应由 PostgreSQL 的正式恢复演练负责,pgAdmin 适合作为受控操作入口,不是备份制度本身。
Schema Diff 给出候选 SQL,不是自动迁移批准
Schema Diff 可以比较源与目标对象定义,并生成同步 SQL。Ignore Owner、Ignore Grant/Revoke、Ignore Tablespace 或 Ignore Whitespace 会改变差异视图;为了让页面变得“干净”而忽略 owner 与 grant,可能刚好隐藏最重要的权限漂移。
生成脚本先保存到版本库外的受控评审区,检查锁、重写表、extension、owner、默认权限和数据兼容,再交给 Flyway、Liquibase 或组织的数据库变更流程。pgAdmin 用户不应因为可以生成 DDL 就长期获得生产 DDL 权限。Schema Diff 适合发现漂移和辅助审查,不应成为绕过迁移历史的第二控制面。
共享 pgAdmin 是一个需要运维的 Web 系统
Server mode 的容量不能只按“同时打开几个页面”估算。每个用户可能打开多个 Query Tool 会话,每个标签页对应数据库连接、事务状态和结果集;反向代理连接、Web worker、配置数据库、每用户存储、导出带宽与浏览器内存都会进入容量模型。数据库连接数紧张时,应限制标签页和空闲会话,而不是单纯增加 Gunicorn threads。
团队认证可以接入 LDAP、Kerberos、OAuth/OIDC 或 Webserver authentication,但 pgAdmin 登录身份与 PostgreSQL role 仍是两层对象。共享一个数据库账号会让平台登录审计与数据库语句审计无法可靠对齐。更稳妥的做法是按人或工作负载分配可追踪数据库身份,至少把只读排障、迁移和管理员账号分开,保存密码有明确期限。
默认 SQLite 配置数据库适合单实例或轻量部署。需要多实例、高可用或集中备份时,可以评估 pgAdmin 支持的外部用户设置数据库,但这会增加一套依赖、迁移和恢复顺序。无论选择哪种形态,都要实际恢复 Server 注册、用户、偏好、历史和存储文件;只备份一个容器 YAML 无法恢复平台状态。
升级前固定镜像或安装包版本,备份配置数据库与用户文件,核对外部 binary path,在隔离实例回归登录、Server 注册、TLS、Query Tool、图形 Explain、Storage Manager、Schema Diff 和备份任务。升级失败时,配置数据库可能已经迁移,不能只把镜像标签改回旧版;回滚依赖升级前备份和经过验证的恢复步骤。
退出时要删的是一组资产
个人实验结束后,删除临时 Server 注册、上传的 CA/客户端证书、查询文件、CSV、备份与 Query History,确认 Query Tool 没有未提交事务。共享环境还要禁用 pgAdmin 用户、撤销认证映射、关闭活动会话、移交或删除每用户存储,轮换曾被保存或共享的数据库凭证。
平台退役时先导出仍需保留的 Server 定义和审计材料,验证目标平台可用,再停止入口。随后按确认后的绝对路径处理 /var/lib/pgadmin、外部配置数据库、代理配置、证书、日志和备份副本。卷删除是不可逆动作,必须在备份恢复演练通过且 owner 签字后执行。
本机合成实验若已完成恢复验证、确认两个界面都不再使用,可在保留原 Bash 变量的实验目录里清理。本段会永久删除本次 pgAdmin 原卷和恢复卷;pgadmin-backup.* 归档仍留在宿主目录,是否保留或销毁应单独判断。先预检两组目标均完整且属于本次实验,再逐个输入完整容器名删除;部分恢复失败只留下卷而没有容器时,预检会在删除任何资源之前停下:
test "$PWD" = "$LAB_DIR" || exit 1
source ./state.env
for role in pgadmin pgadmin-restore; do
if test "$role" = pgadmin; then
name="${LAB_NAME}-pgadmin"; volume="${LAB_NAME}-pgadmin-data"
else
name="${LAB_NAME}-pgadmin-restore"; volume="${LAB_NAME}-pgadmin-restore-data"
fi
if docker container inspect "$name" >/dev/null 2>&1; then has_container=1; else has_container=0; fi
if docker volume inspect "$volume" >/dev/null 2>&1; then has_volume=1; else has_volume=0; fi
test "$has_container" = "$has_volume" || { echo "$name 与 $volume 未成对存在,停止清理" >&2; exit 1; }
test "$has_container" = 1 || continue
test "$(docker inspect -f '{{index .Config.Labels "lab-name"}}' "$name")" = "$LAB_NAME" || exit 1
test "$(docker inspect "$name" --format '{{range .Mounts}}{{if eq .Destination "/var/lib/pgadmin"}}{{.Name}}{{end}}{{end}}')" = "$volume" || exit 1
test "$(docker volume inspect -f '{{index .Labels "lab-name"}}' "$volume")" = "$LAB_NAME" || exit 1
done
for role in pgadmin pgadmin-restore; do
if test "$role" = pgadmin; then
name="${LAB_NAME}-pgadmin"; volume="${LAB_NAME}-pgadmin-data"
else
name="${LAB_NAME}-pgadmin-restore"; volume="${LAB_NAME}-pgadmin-restore-data"
fi
docker container inspect "$name" >/dev/null 2>&1 || continue
read -r -p "将永久删除 $name 和 $volume;输入完整容器名确认:" answer
test "$answer" = "$name" || exit 1
if test "$(docker inspect -f '{{.State.Running}}' "$name")" = true; then
docker stop "$name" || exit 1
fi
docker rm "$name" || exit 1
docker volume rm "$volume" || exit 1
done然后把数据库清理脚本保存到同一实验目录,阅读后执行 bash cleanup.sh 并按提示输入本次 PostgreSQL 容器名。只有 pgAdmin 和数据库两部分都退出后,本机实验才算结束;归档、pgadmin.env、CSV、截图和浏览器下载物仍需按各自保留规则处理。
权威资料与规范地址
部署形态
pgAdmin 9.18 Server Deployment
