Your production database has no working automated backup
how do I know if my postgres database is actually backed up Probably not, or not the way you think. The common failure isn't a missing cron job, it's a backup job that silently points at the wrong container, fails for weeks without alerting anyone, or writes a dump format your pg_restore can't read. Confirm with a real restore into a scratch database, not by checking that a file exists.
Seen in 31 of 450 scanned projects (7%). Based on Deep Scan runs across 450 self-hosted projects with a Postgres database.
How to tell you have it
- No cron entry or scheduled job references pg_dump anywhere on the host
- A backup script exists but its dump files stopped growing weeks ago
- The backup targets a container name or port that no longer matches the running database
- Nobody can say when the last successful restore was tested
- Runtime JSON state files next to the app are never copied off the host
Why it matters
A self-hosted Postgres holding all production data is a single point of failure with no safety net if there is no backup job at all. Disk corruption, a bad migration, a `DROP TABLE` typo, or a full disk during a write can destroy hours or days of data with no way back. This is the most common gap on solo-developer infrastructure because the database ran fine for months and backups never made it onto anyone's list.
A backup cron that points at the wrong container is worse than having none, because it creates false confidence. If a project got renamed, the compose stack was rebuilt, or a second database was added later, an old `pg_dump -h old-container` line keeps running and keeps succeeding, dumping a database nobody uses while the real one is silently unprotected.
Dumps that have been failing for weeks with nobody alerted are the second most common failure. `pg_dump` writes an error to a log file or stderr that nobody reads, the cron job's exit code is never checked, and the backup directory keeps old files from before the failure started, so a casual `ls` still looks fine.
A dump in a format the installed `pg_restore` cannot read, or a runtime JSON state file with no backup at all, both produce the same outcome during an incident: you go looking for the thing that was supposed to save you and it does not work. Format mismatches happen after a Postgres major version upgrade on one side but not the other; JSON state files get treated as disposable cache and quietly become the only copy of something that matters.
How Heygents detects it
Deep Scan reads the project's crontab and any `docker-compose.yml` or systemd timer for a `pg_dump` invocation, checks whether the target host/container name matches the currently running Postgres via `pm2 jlist` and `docker ps`-equivalent config, and lists the backup output directory with `ls -la` to check file ages and sizes against the schedule. It also inspects `psql` connectivity using the project's own `DATABASE_URL` from `.env` to confirm which database is actually live, and looks for any alerting hook (Telegram, email, exit-code check) wired to the backup script.
How to fix it
- Add a real nightly pg_dump cron job Write a dump script that uses the custom format so `pg_restore` can do selective and parallel restores, and point it at the database name and container the app actually connects to, not a name copied from an old setup.
- Verify the cron actually points at the live database Compare the host and port in the backup script against the app's real DATABASE_URL before trusting any existing job.
- Prove the dump restores, don't just check it exists A dump file that exists on disk is not a backup until it has been restored successfully at least once. Do this on a schedule, not just the day you set it up.
- Alert on failure instead of hoping someone notices Check the exit code of pg_dump explicitly and send a notification on nonzero, rather than letting a failed run sit quietly in a log file.
- Copy backups off the host A dump that only lives on the same disk as the database doesn't protect against disk failure, a bad `rm -rf`, or the host itself going down. Push a copy somewhere else on the same schedule, whether that's object storage, another server, or a synced remote path.
- Back up runtime JSON state files too If the app keeps state in local JSON files instead of the database, treat that directory the same way: include it in the nightly backup and off-host copy, not just the Postgres dump. It is easy to assume these are disposable until one of them turns out to be load-bearing.
Add a real nightly pg_dump cron job
#!/bin/bash
set -euo pipefail
STAMP=$(date +%F)
pg_dump "postgres://user:pass@host:5432/db" -F c -f "/var/backups/<app>/db-$STAMP.dump"
find /var/backups/<app> -name '*.dump' -mtime +14 -delete
Verify the cron actually points at the live database
grep -m1 DATABASE_URL /var/www/<app>/.env
crontab -l | grep pg_dump
Prove the dump restores, don't just check it exists
createdb restore_check
pg_restore -d restore_check /var/backups/<app>/db-2026-07-31.dump
psql restore_check -c "select count(*) from <table>;"
dropdb restore_check
Alert on failure instead of hoping someone notices
if ! pg_dump "postgres://user:pass@host:5432/db" -F c -f "/var/backups/<app>/db-$(date +%F).dump"; then
curl -s -X POST "https://api.telegram.org/bot<token>/sendMessage" \
-d chat_id=<chat_id> -d text="backup failed for <app> on $(hostname)"
exit 1
fi
Copy backups off the host
rclone copy /var/backups/<app> remote:backups/<app> --min-age 1h
Find this in your own projects, automatically
Heygents runs a read-only Deep Scan across every project you own, finds issues like this one, and hands you a ready-to-run fix an AI agent can execute and verify. A solo developer gets the audit, the backlog and the fix loop in one place.
Open Heygents →Frequently asked questions
Is a pg_dump cron job enough on its own?
No. A cron job that runs is only half the story. You also need to confirm the dump targets the correct database, check the exit code and alert on failure, keep a copy off the host, and periodically restore the dump into a scratch database to prove it actually works. Any one of those missing turns the backup into a false sense of security.
Why would a backup job silently protect the wrong database?
This usually happens after infrastructure changes, like renaming a Docker container, rebuilding a compose stack, or standing up a second database for a new feature. The old backup command still runs and still exits successfully, because it is dumping a real database, just not the one that matters. Always diff the backup target against the app's live DATABASE_URL.
How do I know if my backup dumps are actually restorable?
Restore one into a throwaway database and query a table you recognize. `createdb restore_check`, then `pg_restore -d restore_check <file>`, then a simple `select count(*)`. If this fails because of a format mismatch or a corrupt file, you have effectively had no backup this whole time, even though files were piling up on disk.
Do I need to back up runtime JSON state files separately from Postgres?
Yes, if any part of your app writes state to local JSON files outside the database. A pg_dump schedule does nothing for those files. Add the directory to the same off-host backup routine so an incident that wipes the disk doesn't also erase state that never made it into Postgres.
What is the fastest way to check if backups are currently working?
List the backup directory and check the timestamp and size of the most recent file against your schedule, then diff the database host in the backup script against the app's real DATABASE_URL. If both look right, do a test restore. All three checks take under ten minutes and catch the majority of silent backup failures.