
PostgreSQL 主从复制从零搭建本地高可用数据库怎么远程安全验证标签PostgreSQL、Docker、数据库、cpolar、内网穿透本地服务最怕的不是代码报错而是数据库一挂接口、后台任务、管理页面一起停摆。真要给小团队做一套高可用数据库验证环境光把 PostgreSQL 跑起来不够还得确认主库写入后从库能不能稳定读到数据。这篇不讲“数据库挂了怎么办”的大叙事直接做一套能落地的本地实验用 Docker Compose 启动 PostgreSQL 主库、只读从库和 pgAdmin再用psql查复制状态。到验证阶段再临时用 cpolar 暴露 pgAdmin 或只读从库端口让远程同事帮忙看连接、复制状态和查询结果。划重点这套方案只用于开发、测试、演示和复制链路验证不要把生产主库写入口长期暴露出去。1 什么是 PostgreSQL 主从流复制PostgreSQL 的主从流复制说白了就是主库负责写入从库持续接收主库产生的 WAL 日志并回放。业务把数据写到主库后从库跟着同步适合做只读查询、备份验证、读写分离前的链路演练。这篇里我们只做一件事确认“主库写入 → WAL 传输 → 从库回放 → 远程只读验证”这条链路是通的。这里先不做自动故障切换。故障切换涉及 VIP、代理层、提升从库、应用连接重试一篇文章塞进去反而容易写乱。先把复制跑稳后面再扩展 Patroni、repmgr 或 HAProxy 会更清楚。本次实验的端口安排如下服务容器名容器端口宿主机访问PostgreSQL 主库pg-primary5432127.0.0.1:5432PostgreSQL 从库pg-replica5432127.0.0.1:5433pgAdminpgadmin80http://127.0.0.1:5050提醒一下主库和从库在 Compose 网络里都使用 5432只是映射到宿主机时从库改成了 5433避免端口冲突。2 环境准备目录、Docker 和账号先定好这一步先把目录和文件放整齐。后面排错时能一眼看出是主库初始化脚本、从库拉基线脚本还是 Compose 配置出了问题。2.1 创建项目目录在一台已经安装 Docker 和 Docker Compose v2 的机器上执行mkdir -p ~/pg-replication-lab/primary/init mkdir -p ~/pg-replication-lab/replica cd ~/pg-replication-lab检查 Docker Compose 是否可用docker compose version能看到版本号就继续。这里建议直接使用docker compose不要再用老的docker-compose命令后面的命令都按 Compose v2 写。2.2 准备主库初始化脚本主库启动时要做两件关键事创建复制账号并允许这个账号从 Docker 网络内发起 replication 连接。新建文件primary/init/01-primary.shcat primary/init/01-primary.sh EOF #!/bin/bash set -e psql -v ON_ERROR_STOP1 --username $POSTGRES_USER --dbname $POSTGRES_DB EOSQL CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD repl_pass_2026; CREATE ROLE app_readonly WITH LOGIN PASSWORD readonly_pass_2026; EOSQL cat $PGDATA/pg_hba.conf EOF_HBA host replication replicator 0.0.0.0/0 scram-sha-256 host all app_readonly 0.0.0.0/0 scram-sha-256 EOF_HBA EOF chmod x primary/init/01-primary.sh这里别把replicator当业务账号用它只负责从主库拉 WAL。后面远程验证查询时用app_readonly这个只读账号不把主库超级用户密码发给别人。2.3 准备从库初始化脚本从库第一次启动时要用pg_basebackup从主库拉一份基线数据。-R参数会写入复制配置让从库知道以后从哪个主库继续接收 WAL。新建文件replica/replica-entrypoint.shcat replica/replica-entrypoint.sh EOF #!/bin/bash set -euo pipefail mkdir -p $PGDATA chown -R postgres:postgres $PGDATA chmod 700 $PGDATA if [ ! -s $PGDATA/PG_VERSION ]; then echo waiting for primary... until gosu postgres pg_isready -h pg-primary -p 5432 -U postgres; do sleep 2 done echo running pg_basebackup... rm -rf $PGDATA/* export PGPASSWORD$REPLICATOR_PASSWORD gosu postgres pg_basebackup \ -h pg-primary \ -p 5432 \ -D $PGDATA \ -U replicator \ -v \ -P \ -R \ -X stream \ -C \ -S replica_slot unset PGPASSWORD fi exec gosu postgres postgres \ -c hot_standbyon \ -c listen_addresses* EOF chmod x replica/replica-entrypoint.sh这段脚本里有一个容易卡住的点pg_basebackup必须等主库真正 ready 后再执行所以前面用了pg_isready循环等待。否则从库容器启动得太快会直接连不上主库。3 使用 Docker Compose 搭建主库、从库和 pgAdmin现在把三个服务编排到一个文件里。主库负责写从库负责读pgAdmin 负责图形化验证。新建docker-compose.ymlcat docker-compose.yml EOF services: pg-primary: image: postgres:16 container_name: pg-primary environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: primary_pass_2026 POSTGRES_DB: appdb command: - postgres - -c - wal_levelreplica - -c - max_wal_senders10 - -c - max_replication_slots10 - -c - hot_standbyon - -c - listen_addresses* volumes: - pg_primary_data:/var/lib/postgresql/data - ./primary/init:/docker-entrypoint-initdb.d:ro ports: - 127.0.0.1:5432:5432 healthcheck: test: [CMD-SHELL, pg_isready -U postgres -d appdb] interval: 5s timeout: 5s retries: 20 networks: - pgnet pg-replica: image: postgres:16 container_name: pg-replica environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: replica_local_pass_2026 POSTGRES_DB: appdb REPLICATOR_PASSWORD: repl_pass_2026 PGDATA: /var/lib/postgresql/data entrypoint: [/replica-entrypoint.sh] volumes: - pg_replica_data:/var/lib/postgresql/data - ./replica/replica-entrypoint.sh:/replica-entrypoint.sh:ro ports: - 127.0.0.1:5433:5432 depends_on: pg-primary: condition: service_healthy networks: - pgnet pgadmin: image: dpage/pgadmin4:latest container_name: pgadmin environment: PGADMIN_DEFAULT_EMAIL: adminexample.com PGADMIN_DEFAULT_PASSWORD: pgadmin_pass_2026 ports: - 127.0.0.1:5050:80 depends_on: pg-primary: condition: service_healthy networks: - pgnet volumes: pg_primary_data: pg_replica_data: networks: pgnet: driver: bridge EOF启动服务docker compose up -d看容器状态docker compose ps如果从库一直重启先看日志不要急着删数据卷docker compose logs -f pg-replica正常情况下日志里能看到pg_basebackup的进度随后从库进入接收 WAL 的状态。这里建议第一次搭建时别开太多服务先让主从复制跑稳后面再接应用更轻松。4 验证主从复制主库写从库读复制链路不是看容器都在运行就算成功。我们要做三层验证主库能看到从库连接、从库确认自己处于恢复状态、主库写入的数据能在从库查到。4.1 在主库建表并写入数据进入主库执行 SQLdocker exec -it pg-primary psql -U postgres -d appdb在psql里执行CREATE TABLE IF NOT EXISTS demo_orders ( id BIGSERIAL PRIMARY KEY, order_no TEXT NOT NULL, amount NUMERIC(10, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); INSERT INTO demo_orders (order_no, amount) VALUES (ORD-20260607-001, 99.90), (ORD-20260607-002, 128.50); GRANT CONNECT ON DATABASE appdb TO app_readonly; GRANT USAGE ON SCHEMA public TO app_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_readonly;这里顺手给app_readonly授权是为了后面验证只读连接。真实项目里建议给业务表单独授权不要偷懒把写权限也给出去。4.2 在从库查询数据从宿主机连从库端口5433PGPASSWORDreadonly_pass_2026 psql \ -h 127.0.0.1 \ -p 5433 \ -U app_readonly \ -d appdb \ -c SELECT id, order_no, amount FROM demo_orders ORDER BY id;能看到两条订单记录就说明主库写入已经同步到从库。再确认从库是只读恢复状态docker exec -it pg-replica psql -U postgres -d appdb \ -c SELECT pg_is_in_recovery();返回t表示当前实例是从库。这个检查很重要别只看端口连通有些环境里连错端口会把主库当从库测。4.3 在主库查看复制连接回到主库查pg_stat_replicationdocker exec -it pg-primary psql -U postgres -d appdb \ -c SELECT application_name, client_addr, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;能看到一行从库连接state为streaming说明 WAL 正在传输。如果这里没有记录按这个顺序查从库日志里有没有pg_basebackup或连接认证错误主库pg_hba.conf是否允许replicator做 replication 连接REPLICATOR_PASSWORD是否和主库创建的repl_pass_2026一致两个容器是否在同一个 Compose 网络pgnet里。复制延迟也可以直接查 WAL 位置差异docker exec -it pg-primary psql -U postgres -d appdb \ -c SELECT pg_current_wal_lsn(); docker exec -it pg-replica psql -U postgres -d appdb \ -c SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();本地 Docker 环境里写入量很小时延迟通常很低。重点不是追求漂亮数字而是要能定位卡在“主库没发、网络没通、从库没回放”哪一段。5 用 pgAdmin 或 psql 做图形化验证命令行验证够直接但团队协作时pgAdmin 这种 Web 管理界面更适合让同事快速看结果。浏览器打开http://127.0.0.1:5050登录信息Email: adminexample.com Password: pgadmin_pass_2026在 pgAdmin 里添加主库连接时填写项主库配置Host name/addresspg-primaryPort5432Maintenance databaseappdbUsernamepostgresPasswordprimary_pass_2026添加从库连接时填写项从库配置Host name/addresspg-replicaPort5432Maintenance databaseappdbUsernameapp_readonlyPasswordreadonly_pass_2026注意pgAdmin 容器和 PostgreSQL 容器在同一个 Docker 网络里所以这里写pg-primary、pg-replica不是写127.0.0.1。如果你是在宿主机用本地 psql 连接才使用127.0.0.1:5432和127.0.0.1:5433。在 pgAdmin 的 Query Tool 里对从库执行SELECT pg_is_in_recovery(); SELECT id, order_no, amount FROM demo_orders ORDER BY id;再试着执行一条写入INSERT INTO demo_orders (order_no, amount) VALUES (ORD-READONLY-TEST, 1.00);从库处于恢复状态时这条写入会失败。这个失败是好事它证明你现在连的是只读从库不是误连了主库。6 用 cpolar 做远程临时验证只暴露验证入口本地验证通过后常见需求是让远程同事也看一眼从库能不能连、查询结果是不是最新、pgAdmin 里能不能看到状态。这里 cpolar 的作用很明确临时把本地验证入口映射出去。不要把它理解成“把数据库长期放到公网”。数据库端口直接暴露风险很高尤其是主库写入口。6.1 临时暴露 pgAdmin 页面如果只是让同事看 pgAdmin 页面优先开 HTTP 隧道映射本地5050cpolar http 5050cpolar 会输出一个公网 HTTP 地址。把这个地址发给同事后对方能打开 pgAdmin 登录页再用你提供的 pgAdmin 账号登录。安全提醒放前面pgAdmin 密码要设置强密码不要使用本文示例密码只在验证窗口内开启隧道用完关闭不要把生产库连接信息配置到这个临时 pgAdmin同事验证完成后修改临时密码或删除 pgAdmin 容器。6.2 临时暴露只读从库端口如果对方需要用本地数据库客户端连接从库可以临时开 TCP 隧道映射宿主机的从库端口5433cpolar tcp 5433cpolar 会给出一个公网 TCP 地址和端口。对方连接时使用 cpolar 输出的主机名和端口数据库账号使用只读账号Database: appdb Username: app_readonly Password: readonly_pass_2026这里别填主库账号也别映射5432主库端口。验证目标是“远程读取从库数据和复制状态”不是让远程写入主库。如果要长期固定地址cpolar 的免费随机公网地址 24 小时内会变化固定二级子域名需要基础套餐或以上固定 TCP 地址需要专业套餐或以上。本文这个场景更推荐短时验证用完关闭减少暴露面。6.3 用完关闭验证入口前台运行的 cpolar 命令在对应终端按CtrlC就能停止。停止后再让同事刷新页面或重连数据库确认外部入口已经不可用。这一步不是形式主义。数据库验证链路越短越好暴露时间也越短越好。尤其是主从复制这种基础设施实验安全边界要从一开始就立住。7 常见排查复制没起来先看这几处这套实验最容易卡的不是 SQL而是初始化顺序和连接权限。7.1 从库没有数据先看从库是否真的处于恢复状态docker exec -it pg-replica psql -U postgres -d appdb \ -c SELECT pg_is_in_recovery();如果不是t说明从库没有按 standby 模式启动。检查replica-entrypoint.sh是否挂载成功以及数据卷里是否残留了旧数据。需要从头重建实验环境时用下面命令清理容器和数据卷docker compose down -v docker compose up -d提醒down -v会删除主库和从库数据卷只适合实验环境。生产库不要这么干。7.2 主库看不到 pg_stat_replication主库没有复制连接时重点看认证和网络docker compose logs pg-replica日志里如果出现密码认证失败检查repl_pass_2026是否前后一致。日志里如果出现主机无法解析检查 Compose 服务名是不是pg-primary。再检查主库是否开启了复制参数docker exec -it pg-primary psql -U postgres -d appdb \ -c SHOW wal_level; SHOW max_wal_senders; SHOW max_replication_slots;wal_level应该是replicamax_wal_senders和max_replication_slots都要大于 0。7.3 只读账号查不到表从库能连但查表失败多半是主库没有给只读账号授权。回主库补一次docker exec -it pg-primary psql -U postgres -d appdb \ -c GRANT USAGE ON SCHEMA public TO app_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;以后新建表想自动给只读权限就保留前面写过的默认权限语句ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_readonly;权限这块别嫌麻烦。远程验证时只给查询权限能减少很多不必要的风险。8 和 CloudBeaver 那篇有什么区别之前如果你看过 CloudBeaver 相关内容可以把它理解成“数据库 Web 管理工具”方向重点是怎么通过浏览器连接、管理多种数据库。本文不做客户端选型核心是 PostgreSQL 主从架构与复制验证。pgAdmin 只是验证工具之一真正要确认的是主库是否正确产生并发送 WAL从库是否处于恢复状态并回放数据只读账号是否能完成远程查询验证cpolar 是否只在验证阶段短时打开入口。旧文内链位置先留在这里CloudBeaver 数据库 Web 管理工具教程。9 总结到这里我们已经用 Docker Compose 搭好了一套 PostgreSQL 主从复制实验环境主库负责写入从库持续接收 WAL 并提供只读查询pgAdmin 和 psql 都能验证复制状态。远程协作验证时cpolar 只负责短时打开 pgAdmin 或只读从库入口不碰生产库也不长期暴露主库写入口。这套流程里最关键的几步是主库提前配置wal_levelreplica、max_wal_senders、max_replication_slots并在pg_hba.conf里允许复制账号连接从库用pg_basebackup -R -X stream拉取基线数据再通过pg_is_in_recovery()和pg_stat_replication双向确认状态远程验证只开放 pgAdmin 或只读从库端口使用强密码、只读账号和临时隧道用完立刻关闭。如果只是家庭服务器、小团队测试环境这套做法已经能把“主库写、从库读、远程验证”的链路跑通。后续要继续升级可以再接入故障切换、连接代理、备份恢复演练把它从实验环境慢慢推到更接近生产的形态。你更想看 PostgreSQL 故障切换、只读账号权限细分还是 CloudBeaver/pgAdmin 管理界面对比评论区直接点一个方向我按实操路线继续写。