AMR SHAMS / BLOG

Build a PostgreSQL Backup System That Costs Zero Extra

2026-08-01 · Amr Shams, Software engineer and pgmoneta contributor / postgresql backups / free backup storage / pgmoneta / self hosted backups / database recovery

Most weak PostgreSQL backup systems are not weak because storage is expensive. They are weak because nobody built the loop: backup, compress, copy, verify, retain, restore, and measure. A directory full of old tarballs is not a recovery plan. It is storage theater.

Zero extra cost does not mean zero cost. Physics still collects rent. Disks wear out, electricity is real, and someone has to test the restore. The useful claim is narrower and more honest: you can build a serious PostgreSQL backup system with zero extra cloud spend if you already have spare disk, a second machine, a NAS, or a self-hosted S3-compatible target.

pgmoneta is built for that kind of ugly, practical work. It supports full and incremental backups, point-in-time restore, WAL shipping, hot standby, compression, encryption, TLS, worker pools, and Prometheus metrics (pgmoneta README, retrieved 2026-08-01). That is not a backup folder. That is a system.

PostgreSQL S3 Cold Storage Backups with pgmoneta

Key Takeaways

  • pgmoneta supports local, SSH, S3, and Azure storage engines, so you can start with hardware you already control.
  • A cheap setup becomes serious only after retention, verification, WAL handling, restore drills, and metrics are in place.
  • Garage gives pgmoneta an S3-compatible target without renting object storage, but you own the storage failure model.
  • The cruel truth: an untested backup is just a rumor with a timestamp.

What Does "Zero Extra Cost" Actually Mean?

As of 2026, pgmoneta documents 4 storage engines: local, ssh, s3, and azure (pgmoneta configuration documentation, retrieved 2026-08-01). That means the cheapest useful setup can start on disk you already own, then grow into SSH or self-hosted S3 before a provider invoice appears.

Rows of server racks representing PostgreSQL backup infrastructure built from existing hardware

"Free" backup advice is usually garbage because it hides the bill. You still need disk space, a PostgreSQL host, a place for WAL, a restore target, and time to test. If every copy sits on the same failing box, you did not save money. You just postponed embarrassment.

The useful goal is zero extra cloud spend. Use what already exists first: an internal SSD, a used mini PC, a NAS share, a lab machine, or a Garage node. Then make the setup prove itself. Backups that cannot prove recovery do not deserve polite language.

The first budget mistake is asking "Where can I store backups cheaply?" The better question is meaner: "Which failure can I survive without typing my credit card into another platform?" pgmoneta helps because it turns backup storage into an operational loop, not a shopping exercise.

Citation capsule: pgmoneta's documented storage engines include local, SSH, S3, and Azure targets. That range lets PostgreSQL teams start with owned disk, copy to a second machine, or use S3-compatible storage without changing the whole backup workflow.

Step 1: Start with Local Backups and Compression

In 2026, pgmoneta's documented default compression is zstd, with configurable compression levels and alternatives including gzip, lz4, and bzip2 (pgmoneta configuration documentation, retrieved 2026-08-01). Local compressed backups are the fastest way to stop pretending and prove the first part of the recovery loop.

Local storage is not the final architecture. It is the first adult step. You learn how backups are created, where metadata lands, how large restores become, and whether your PostgreSQL host can finish a backup without drama.

[pgmoneta]
base_dir = /var/lib/pgmoneta
storage_engine = local
compression = zstd
compression_level = 3

Create the first backup and list the result:

pgmoneta-cli backup primary
pgmoneta-cli list-backup primary --sort desc

Do not romanticize this stage. A local backup on the same host will not save you from host loss, theft, fire, or a bad operator deleting the wrong tree. It does save you from having no process. That matters because no serious remote design survives if the local workflow is already sloppy.

The output matters. Check the backup label, size, restore size, compression, encryption, PostgreSQL major version, and validity state. If you cannot explain those fields calmly, you are not operating a backup system. You are collecting artifacts.

Citation capsule: pgmoneta's CLI can create and list backups, and its backup output includes fields such as backup label, backup size, restore size, compression, encryption, PostgreSQL version, and validity. Those fields turn a backup job into something operators can inspect.

Step 2: Stop Disk Growth with Retention

pgmoneta's default retention is documented as 7 days, and retention can be configured in days, weeks, months, and years (pgmoneta retention documentation, retrieved 2026-08-01). Retention is where cheap storage becomes sustainable instead of slowly filling until the machine begs for mercy.

A spare disk is not a plan if it grows forever. Unbounded backups are just delayed failure with nicer filenames. Retention decides which recovery points matter and which old backups should leave.

[pgmoneta]
retention = 7, 4, 6, -
retention_interval = 1800

This shape keeps recent daily recovery points, preserves weekly and monthly coverage, and avoids keeping everything because nobody wanted to make a decision. Cowardly retention is expensive. It burns disk while giving the team a false sense of safety.

Incremental chains make this more interesting. pgmoneta's retention docs note that if a backup has an incremental child depending on it, data is rolled up to the child before deletion. That is the kind of detail a real backup system must own. A delete operation should not casually destroy the recovery chain.

For the difference between integrity checks and full restore testing, see our backup verification vs restore testing guide.

Citation capsule: pgmoneta retention can be expressed across days, weeks, months, and years, with a configurable retention interval. Its documentation also covers incremental dependency behavior, which matters because deleting parent backups can affect downstream recovery paths.

Step 3: Add WAL Shipping for Recovery Depth

PostgreSQL documentation describes 3 backup approaches: SQL dump, file-system-level backup, and continuous archiving with point-in-time recovery (PostgreSQL Backup and Restore, retrieved 2026-08-01). WAL is what moves a recovery design beyond static snapshots.

Backups are photographs. WAL is the footage between photographs. Without WAL, you recover to the backup you have. With a usable WAL chain, you get a fighting chance at recovering closer to the failure point.

[primary]
wal_shipping = /var/lib/pgmoneta-wal

PostgreSQL's own documentation is blunt about the requirement: continuous archive recovery needs a sequence of archived WAL files reaching back at least as far as the start time of the backup (PostgreSQL Continuous Archiving and PITR, retrieved 2026-08-01). Miss that chain and your beautiful restore story gets shorter.

This is where the cheap setup gets serious. You must monitor WAL space. You must know how much write activity your database produces. You must know whether the WAL directory is growing normally or quietly turning into the next outage.

In practice, the backup file is rarely the only problem during drills. The shame usually lives around it: missing WAL, a forgotten directory, a wrong permission, a full disk, or a restore command nobody has run in months.

Citation capsule: PostgreSQL continuous archiving depends on keeping WAL files back to the relevant base backup. pgmoneta adds WAL shipping configuration and metrics, giving operators a way to preserve and observe the recovery material between full backup points.

Step 4: Copy Backups to a Second Machine with SSH

pgmoneta documents SSH as a storage engine and requires a remote server, remote backup directory, SSH hostname, username, and key configuration (pgmoneta SSH documentation, retrieved 2026-08-01). This is the cheapest meaningful escape from same-host delusion.

An old server under a desk is not glamorous. Good. Glamour is not a recovery objective. A second machine with boring disks and working SSH is often the first real step away from "my backup died with my database."

[pgmoneta]
storage_engine = ssh
ssh_hostname = backup-host
ssh_username = pgmoneta_backup
ssh_base_dir = /srv/postgresql-backups
ssh_public_key_file = /home/pgmoneta/.ssh/id_rsa.pub
ssh_private_key_file = /home/pgmoneta/.ssh/id_rsa

Use a dedicated user. Lock down the key. Put the backup directory somewhere obvious. Then test the connection before trusting it. If SSH fails at 03:00, it will not care that your architecture diagram looked mature.

There is one uncomfortable requirement: local staging still matters. pgmoneta's SSH docs state that the local computer needs storage space for one backup. Cheap designs fail when nobody budgets temporary space. Do not be that person.

Citation capsule: pgmoneta's SSH storage engine lets PostgreSQL backups move to a remote server using configured host, user, directory, and key paths. It is a practical low-cost resilience step, but the local system still needs space for one backup.

Step 5: Use Garage When You Want Your Own S3

Garage describes itself as an S3-compatible distributed object store for small self-hosted geo-distributed deployments and is released as free software under AGPLv3 (Garage GitHub mirror, retrieved 2026-08-01). That gives pgmoneta users an S3-style path without renting object storage.

This is where the successful S3 idea gets sharper. The question is not "Which provider has the prettiest pricing page?" The question is "Can I keep the S3 workflow while owning the storage?"

pgmoneta documents Garage as an S3-compatible target:

[pgmoneta]
storage_engine = s3
s3_access_key_id = <garage-access-key-id>
s3_secret_access_key = <garage-secret-access-key>
s3_bucket = <garage-bucket>
s3_base_dir = pgmoneta
s3_endpoint = <garage-endpoint-host>
s3_port = 3900
s3_region = garage
s3_use_tls = off

Then test that pgmoneta can see the remote path:

pgmoneta-cli s3 ls primary

Garage is not free cloud. That phrase is nonsense. Garage is self-hosted object storage. You own replication, disks, upgrades, monitoring, TLS, failure testing, and the ugly day when a node disappears. If you want control, this is attractive. If you want someone else to hold the pager, pay someone.

Citation capsule: Garage provides an S3-compatible self-hosted object storage target, while pgmoneta can use S3 endpoints through s3_endpoint, s3_port, s3_region, bucket, base directory, and credential settings. That combination keeps S3 workflows without requiring rented object storage.

Step 6: Verify Backups Automatically

pgmoneta creates a backup.sha512 file for each backup and supports CLI verification with pgmoneta-cli verify (pgmoneta backup documentation, retrieved 2026-08-01). A backup system without verification is just a confidence scam with log output.

Schedule verification:

[pgmoneta]
verification = 1H

Run an on-demand check:

pgmoneta-cli verify primary newest /tmp/verify

There are two layers here. The daemon-level check validates stored archive integrity using checksums. The CLI workflow goes further by exercising the restore path into a target directory. You need both forms of pressure because storage corruption and broken restore mechanics are different failures.

This is where the tone should get cruel because the failure is cruel. A backup that was never verified does not become noble during an outage. It becomes a maybe. Production systems should not depend on maybe.

Citation capsule: pgmoneta uses SHA-512 checksum metadata for backup integrity and exposes pgmoneta-cli verify for backup verification. Scheduled verification can be configured with time suffixes such as 1H, making integrity checks part of normal operations rather than a heroic manual task.

Step 7: Prove the System with a Restore Drill

PostgreSQL 17 documentation lists recovery targets by time, transaction ID, LSN, named restore point, timeline, and action (PostgreSQL Write Ahead Log configuration, retrieved 2026-08-01). pgmoneta exposes restore positions such as current, time=X, lsn=X, timeline=X, and action=X.

The restore drill is where backup theater dies. Good. Let it die early, in a planned test, while nobody is shouting.

pgmoneta-cli restore primary newest current /tmp
postgres -D /tmp/primary-<backup-label> -p 55432
psql -p 55432 -d postgres -c "SELECT pg_is_in_recovery(), now();"

For a Garage or S3-backed copy:

pgmoneta-cli s3 restore primary <label> current /tmp

pgmoneta's S3 restore flow downloads metadata first, verifies backup.info, downloads backup files, runs the normal restore workflow, and removes the staged local copy after success (pgmoneta S3 documentation, retrieved 2026-08-01). That is the difference between a bucket full of objects and a recovery workflow.

Your drill should answer boring questions:

Citation capsule: pgmoneta supports local restore and S3 restore workflows, while PostgreSQL supports recovery targets by time, LSN, transaction ID, named restore point, and timeline. A real drill proves those mechanisms against an actual restored cluster, not a diagram.

Step 8: Watch It with Prometheus

pgmoneta exposes Prometheus metrics when metrics is configured, and its documentation lists backup, restore, WAL, compression, remote storage, and hot standby metrics (pgmoneta Prometheus documentation, retrieved 2026-08-01). Invisible backups are not operations. They are superstition.

[pgmoneta]
metrics = 5001

Then scrape:

http://localhost:5001/metrics

Watch the signals that show whether the system is alive:

The pgmoneta backup loop A serious backup system is a loop, not a folder. Backup Compress Copy Verify Restore Measure Source: pgmoneta documentation, retrieved 2026-08-01.
Backups become a system only when creation, retention, verification, restore, and measurement happen repeatedly.

Metrics make neglect visible. They tell you when valid backups disappear, invalid backups appear, compression ratio changes, WAL space grows, remote transfers slow down, or restores are active. The dashboard is not decoration. It is where your backup lies go to get caught.

Citation capsule: pgmoneta's Prometheus endpoint exposes metrics for backup validity, backup size, compression ratio, WAL shipping space, active workflows, restore size, and remote transfer throughput. Those metrics turn backup health into an observable operational surface.

Optional Power Move: Hot Standby

pgmoneta supports hot standby directories and documents up to 8 comma-separated hot standby paths (pgmoneta restore documentation, retrieved 2026-08-01). This is where spare disk can buy time instead of just storing old fear.

[primary]
hot_standby = /srv/pgmoneta-hot-standby

Hot standby changes the conversation. Instead of waiting for decompression, decryption, extraction, and restore during the incident, pgmoneta can maintain pre-restored copies of the latest backup. That costs disk. It may not cost another cloud subscription.

There are details to respect. Tablespaces may need mapping. Override files may matter. Multiple hot standby directories make capacity planning less forgiving. But the idea is powerful: if restore time hurts, do some restore work before the outage.

This is not magic. It is engineering. Magic is what people call systems they did not instrument.

Citation capsule: pgmoneta hot standby can maintain pre-restored backup directories, with documentation covering multiple standby paths, override files, and tablespace mappings. For teams with spare disk, that can reduce recovery work during an incident without buying another storage service.

What This Setup Still Does Not Solve

PostgreSQL documentation warns that continuous archiving needs enough WAL storage and that recovery depends on available WAL files (PostgreSQL Continuous Archiving and PITR, retrieved 2026-08-01). A zero-extra-cloud-spend design is still vulnerable if all copies share the same room, power feed, and operator habits.

Stay honest or the design becomes a lie.

This setup does not save you from site loss if every machine sits beside the same coffee machine. It does not protect against theft unless one copy is elsewhere. It does not fix bad credentials. It does not make a restore drill optional. It does not turn an old disk into enterprise durability by force of optimism.

Use the cheap path intelligently:

The goal is not to worship free infrastructure. The goal is to stop using cost as an excuse for having no recovery discipline.

Citation capsule: A zero-extra-cloud-spend backup system still needs failure-domain thinking. PostgreSQL recovery depends on usable base backups and WAL, while pgmoneta can help automate backup, copy, retention, verification, restore, and metrics. Hardware ownership does not remove operational responsibility.

Frequently Asked Questions

Can PostgreSQL backups really cost zero extra?

They can cost zero extra cloud spend if you already have suitable disk, a second host, or a self-hosted S3 target. pgmoneta documents 4 storage engines, including local, SSH, S3, and Azure, but you still pay in hardware, electricity, maintenance, monitoring, and restore testing.

Is local backup enough for PostgreSQL?

Local backup is a starting point, not a disaster recovery plan. PostgreSQL documents 3 backup approaches, and local filesystem backup is only one part of the picture. Use local backups to prove the loop, then add SSH, Garage, or another remote target.

Is Garage a replacement for AWS S3?

Garage can replace the S3-compatible workflow for some self-hosted teams, but it does not replace managed durability. Garage is AGPLv3 free software for small-to-medium self-hosted deployments, which means you own disks, replication, monitoring, upgrades, and recovery testing.

What is the cheapest useful pgmoneta setup?

A practical low-cost setup has at least 6 parts: local compressed backups, retention, WAL shipping, scheduled verification, Prometheus metrics, and an SSH copy to a second machine. Add Garage when you want an S3-compatible interface without renting object storage.

When should I still pay for cloud storage?

Pay for cloud storage when you need geographic separation, managed durability, simpler off-site retention, or recovery from site-level failure. PostgreSQL recovery depends on both base backups and WAL availability. Zero extra spend is a starting constraint, not a religion.

Conclusion

The cheapest PostgreSQL backup system is not the one with the lowest storage bill. It is the one that restores when the database is broken and everyone has stopped being charming.

Start local. Compress. Retain. Ship WAL. Copy to another machine. Use Garage if you want your own S3-compatible target. Verify. Restore. Measure. Then repeat until the process is boring.

That is the power pgmoneta gives you: not a prettier backup directory, but a recovery loop you can run before disaster grades your homework.

[INTERNAL-LINK: PostgreSQL S3 cold storage backups with pgmoneta -> existing S3 backup article]

Source Notes