PostgreSQL Kit¶
id: postgresql · sections: Monitoring, Targets, Schedules,
Copies, Verification, Found · state: works — backup,
encryption, restore with verification.
Taking a
pg_dumpis 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_dumpare 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 isrequire: 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_dumpolder 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/healthand 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-14…postgres-verify-18, under theverifyprofile — 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:
- it restored without errors;
- the tables are there;
- 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"; - 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_dumpthat 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.