Skip to content

PostgreSQL Kit

id: postgresql · sections: Monitoring, Targets, Schedules, Copies, Verification, Found · state: works — backup, encryption, restore with verification.

Taking a pg_dump is a five-line script. Everything this kit is about is what such a script does not have — and the reason no ready-made tool did the job.

What it does

Logical dumps of PostgreSQL, on a schedule, into a storage of the platform, with rotation, with verification by restoring, and with monitoring that shows the real state of protection rather than the exit code of the last command.

  • A stream, not a file. pg_dump → compression → encryption → storage, with the checksum computed on the way. No intermediate file on the executor: on a two-hundred-gigabyte database that is the difference between works and does not.
  • Encryption of the copy is a property, not an option. The dump goes into a storage that may not be yours.
  • Rotation is grandfather-father-son, with separate retention for hourly, daily, weekly, monthly and yearly copies. The tier of a copy is decided by when it was taken (first of the day, first of the week…), not by its position — a person should be able to predict what survives rotation without replaying the code in their head. The last verified-restorable copy is never deleted, whatever the policy says.
  • Retries. A failed copy is retried before it counts as a failure — three times by default, five seconds apart, per database rather than per job. Each attempt is its own row in the history: "four attempts" and "one failure" are different facts about a database.
  • Progress and volume are reported while the dump runs, so a multi-hour backup does not look hung; both channels of pg_dump are drained from the start, because a 64 KB pipe that nobody reads is exactly how "stuck at 0%" happens.
  • Everything has a deadline, and the deadline ends in a kill. A job must end in a verdict; "still waiting" is not one.

A 15.9 MB database becomes about 180 KB in the storage, and the copy is restored into a separate database with tables and row counts compared.

What it needs

  • Network access from the kit to the database. The kit connects with the client tools itself; no agent is required for the backup.
  • A credential, stored as a platform secret. The connection check looks at what the account can do: if it has write rights the kit says so and suggests a read-only role — a backup needs no right to change data. It is advice, not a refusal.
  • TLS by choice of the target: disable, require, verify-ca, verify-full, with a root certificate and a client pair if needed. The default is require: silently going in clear text is worse than one refusal that is then consciously lifted.
  • Client tools of the right version. The image ships PostgreSQL clients 16–18; the driver picks the exact match for the server or the nearest newer one, so a 14 or 15 server is copied by the 16 client. Newer works, older does not — a pg_dump older than the server breaks off mid-dump, and that looks like a corrupt copy rather than the wrong tool. What is installed is visible in /health and on the Targets screen, not discovered in the backup window.
  • A storage of the platform. If there are several, the target says which one: "there is more than one storage — say in the target which one to write to".
  • For verification: a check server. A pool of separate PostgreSQL servers, never the one the copy was taken from. The compose file ships one per major, postgres-verify-14postgres-verify-18, under the verify profile — none runs until you start it on the host, and the Verification screen shows a card per major with the exact command; a started server is noticed within a minute and joins the pool by itself, with no registration and no password. A copy restores forward and never backward, so one server of your newest major checks everything older — and the image carries a client for every major it ships a check server for, so a copy of a 14 server is written by the 14 client and checked on the 14 server.

A target is not created blind. Connection, a writable storage, the named policy and a client fit for the server version are all checked before the row exists, and the password is saved after the check, not before — a batch entered with a wrong password leaves nothing behind. A database that is down can still be added, but out loud: "save without checking".

Targets, found databases, templates

  • Found. The kit asks the Network Map Kit what looks like a DBMS (ports 5432, 6432) and offers to take the findings on as targets. This is the conversation that justifies the platform: the list of what looks like a database and has no schedule is the list of what will be lost. Found databases are taken on in a batch: tick the ones you want, enter the account, storage and policy once, and a row can still carry its own database name and its own password. Partial success is a state, not an error — fifteen of seventeen created, two named with their reason.
  • Targets show, on each row, the last seven attempts as strips — backups and verifications separately — and a shield: green when the last copy was taken and read back, yellow when copies are taken and nobody unpacks them, red when the last backup or the last verification failed. The yellow is the point: backups that run and are never restored look exactly like protection until the day a copy is needed. Sixteen columns, the person chooses which to show; fifty targets arrive through the clipboard rather than fifty forms.
  • Schedules are templates that carry both halves of one decision — how often to copy, and how many copies of each tier to keep — plus a third: whether to verify each copy, or on a cadence of its own. Four presets:
Template How often Hourly Daily Weekly Monthly Yearly
Hourly every hour 36 7 4 0 0
Daily every day 0 14 4 6 0
Weekly every week 0 0 8 12 0
Monthly every month 0 0 0 12 5

The cron expression is derived from the frequency and the start moment, never stored, so the two cannot drift apart. A monthly copy on the 31st is clamped to the end of shorter months — plain cron would silently skip five months out of twelve. Applying a template to many targets always asks for confirmation and names the numbers, because retention decides which copies are deleted. - A target follows the name of its machine. When the Network Map Kit sees a DNS rename, the target that still carries the old name is renamed too; the former name is kept in the card, with who renamed it — DNS or a person. Two different names reaching one address is reported as an incident: two targets that look like two machines are one, and one failure loses both copies. - Copies opens with one row per target — last copy, last verification, how many copies exist, template, dump size, size in the storage, where it went — and the per-database journal lives inside the target. "Not configured" is written in words where nothing verifies a target's copies, because an empty cell reads as fine.

Verification by restoring

"A copy nobody has ever unpacked is a hope, not a backup." Verification is a separate job: take the copy, restore it into a scratch database on a pool member, compare, write the verdict onto the copy. Four levels of strictness, each named in the interface:

  1. it restored without errors;
  2. the tables are there;
  3. the row counts add up — against statistics recorded at dump time; on estimates (reltuples) discrepancies are shown but do not fail the check, because an estimate cannot tell "legitimately shrank" from "half the rows did not come back";
  4. the check queries pass — queries the user sets on the target.

A failed verification weighs as much as a failed backup. The scratch database of a failed check is kept as evidence — for a week by default, because a Monday failure should still be there on Friday for whoever gets to it; zero means forever. Only what the kit remembers leaving is cleaned up.

A copy restores forward, never backward: the pool stores each server's version (read from the server, not typed), only fitting members are chosen, and if none fits it is a refusal before the restore starts. "Forward" counts from two things, and the larger wins — the source server's major, and the major of the pg_dump that wrote the copy, because a dump carries the dialect of its tool whatever it was taken from (17 began writing SET transaction_timeout, which a 16 server refuses). The tool's version is recorded on every copy, and a target added without a connection check has its version read at the first backup. Otherwise a copy from PostgreSQL 18 restored into a 17 server breaks on line 127 with function uuidv7() does not exist. The copy was fine; the check server was the wrong shape for it.

Restore into a named database is the one action of this kit that overwrites data. It is a job with a confirmation: the database name is typed in full — "a restore overwrites data, so the name is typed in full" — and a non-empty database is refused unless overwrite is passed: "Restoring over data is a decision made out loud."

What it measures

Declared in the manifest, collected by the core, shown on the kit's Monitoring screen and served on /metrics. The wording is the manifest's:

Metric Help
backup_age_seconds Age of the last successful copy — the RPO in practice
targets_unscheduled Targets with no schedule — that is, forgotten ones
found_unprotected Of those, the ones nobody has taken on — the list of what will be lost
backup_size_bytes, database_size_bytes Size of the last dump; size of the database when it was taken
backup_stored_bytes Size of the last copy as it lies in the storage
backup_duration_seconds How long the last backup took
backup_failures, verify_failures Failures over a day; verifications that failed over a day
runs_interrupted Runs nobody closed over a day — the kit was stopped while they were going. Not failures
backup_attempts Tries the last backup needed. One is normal; three every night is a target failing slowly
backup_overdue_seconds How long ago the next scheduled backup was due — the one alert an outside system cannot build for itself
verified_age_seconds Age of the last successful verification of a copy
copies_total, copies_bytes Copies of this database that exist right now, and what they take in the storage
verify_pool_total, verify_pool_failing Members of the check pool; of those, the ones whose last verdict did not come back
evidence_kept Databases that failed checks left behind for somebody to look at
db_connections_used Share of the connection ceiling in use — a full pool is a backup window that cannot start
db_oldest_transaction_seconds Age of the oldest open transaction — it holds rubbish and inflates both the database and the dump
db_replica_lag_seconds How far behind a replica is — a dump from one is yesterday
db_xid_headroom Transaction ids left before the server stops accepting writes — weeks of warning, if anybody is looking
db_dead_rows, db_wal_bytes Rows deleted and not yet reclaimed; write-ahead log on disk

Size matters twice. A dump that suddenly shrank is an incident, not a success — a script with exit code 0 does not see it. Two thresholds: a drift below 70 % of the usual size needs several comparable runs; a collapse below 10 % needs exactly one previous size. The two are separate because a single rule written for drift stays silent on a collapse: 2.1 GiB one day, 653 KiB the next, and no comparable runs to judge it by. Database size is read in the same run as dump size, so the alert can say which it is: the copy is short (data unchanged) or something happened to the data.

Always alerted, not configurable: backup failed, verification failed, storage unreachable, target unreachable, storage full.

What it refuses to do, and why

  • Restore a copy for verification into the instance it came from. A verification that goes into production is not a verification, it is an incident waiting for the one time somebody picks the wrong entry from a list.
  • Restore backward. "{name} runs {engine} {have} and the copy comes from {need}: a copy restores forward, never backward."
  • Restore over data without being told. "the database {name} is not empty. Restoring over data is a decision made out loud: pass overwrite if that is exactly what is wanted."
  • Register an empty copy as a success. pg_dump that failed to connect produced zero bytes and, for a while, failed: False; the fix lives in the shared library, in one place, because two places is exactly how the bug was written twice.
  • Guess a storage. "the core handed over no storage at all: configure one in the Storages section — there is nowhere to write a copy."
  • Retry more than ten times. "that is not a number of attempts, it is a way to keep a database busy all day."
  • Wait forever. A dump is killed after AG_PG_DUMP_TIMEOUT (6 h by default): a hung backup is worse than a failed one, because the failed one reports itself.
  • Rename a target a person named. A target called "billing, prod" is not what the registry knows the machine as, and nothing renames it.

Known limitations

  • Logical copying gives no point-in-time recovery.
  • On very large databases the window of a logical dump may be unacceptable — that is the case for physical copying (pg_basebackup, WAL), which this kit does not do yet. Physical backups and point-in-time recovery are planned for the next development iteration; filesystems and VMs stay outside the kit.
  • Verification needs space and time comparable to the database itself; on large targets it is realistic less often than the backup.
  • Level 3 (row counts) does not catch corruption inside rows.
  • Row statistics and the dump are different instants: a zero tolerance is meaningful only for a database nobody is writing to at the time.
  • Whether an application works on the restored database is not checked, and the verdict says so.
  • Load and large volumes have not been tested: the largest backup verified so far is a 16 MB database.

Free level

From the manifest: backup and verify are free features, and targets are not capped (targets: -1, the platform's "declared, no ceiling"). Object storage is free on the core's side too, so a home lab or a small shop can put every database it has into a bucket without a key. Verification by restoring was at first the paid half and the line was drawn in the wrong place: "a product that gives the hope away for free and charges for the proof sells the wrong half." What a licence will eventually be about is scale, not honesty.

Configuration

AG_PG_RATE_MIB (read-rate ceiling, MiB/s; 0 — unlimited), AG_PG_COMPRESS (6), AG_PG_DUMP_TIMEOUT (21600), AG_PG_CONNECT_TIMEOUT, AG_PG_SSLMODE (require), AG_PG_BINDIR. See Configuration for which of these the compose file passes.