2026-09-03

Backing Up Postgres on Coolify, Then Proving It

A backup you have never restored is not a backup, it is a file. Here is how to make it one anyway.

A backup you have never restored is not a backup. It is a file that looks like a backup, sitting next to a green checkmark that also looks reassuring. I run a shared VPS with Coolify on it, several client apps each with their own Postgres container, and I found out the hard way that the distance between 'a dump exists somewhere' and 'I can bring this database back' is where the actual risk lives. A hosting agreement that promises daily backups is a promise about the second thing, not the first, and nothing forces those two to match unless you go and check.

What Coolify gives you natively

How much Coolify does for you out of the box depends on whether Postgres is set up as a proper Coolify-managed Postgres resource, or whether it's just a Postgres service riding inside a docker-compose stack alongside your app. A managed resource gets Coolify's own backup tooling: point it at an S3-compatible bucket, set a schedule, and it runs pg_dump on a timer and ships the result off-box. That's the version worth using if you're standing up a new database today.

Where it gets dangerous is the compose-stack case, because that's easy to end up with without deciding to. If you write your own docker-compose.yml with a postgres service in it, rather than adding Postgres as its own Coolify resource, Coolify's backup tab for that database is unreliable. I've seen it, and it's a known pattern across other people's Coolify setups too: the tab reports Success on schedule while uploading nothing at all. A hosting agreement that names a backup cadence can be silently unmet for weeks, with nothing in the UI telling you.

A dump file existing is not the same claim as data coming back

Even when the scheduled job genuinely runs and genuinely uploads something, that only proves a file landed in a bucket. It doesn't prove the file is a complete, uncorrupted dump of a database in a state you'd actually want back. It doesn't prove you still have the credentials to reach that bucket six months from now. It doesn't prove the dump format is one you remember how to restore under pressure, at 2am, with a client asking why the site is down. Every one of those gaps is invisible from a dashboard that just shows you a timestamp and a filename. The only way to close any of them is to actually restore the file into a real database and look at what comes out.

Taking a manual pg_dump from a Coolify-managed Postgres

Whatever scheduled backup you've got running, it's worth knowing how to take a dump by hand, because that's also what you reach for right before a risky migration or before decommissioning something. A Coolify-managed Postgres runs as its own container on the Coolify docker network, reachable by other containers using its container name (the service's UUID), not by an IP that can change on restart. That matters less for a manual dump than it does for wiring an app's DATABASE_URL, but it's the same container you're about to exec into.

The simplest path is to run pg_dump from inside the Postgres container itself, over SSH to the VPS, rather than trying to reach the database from your own machine. Your workstation may not even have a pg_dump binary at all: mine doesn't, and pg_dump has to be the same version or newer than the server it's dumping, so reaching for whatever happens to be installed locally is asking for a version mismatch anyway.

ssh your-vps
docker exec <postgres-container-name> \
  pg_dump -U <db-user> -d <db-name> -F c -f /tmp/backup.dump
docker cp <postgres-container-name>:/tmp/backup.dump ./backup.dump

The -F c flag takes the custom format instead of plain SQL. It's compressed, and it restores with pg_restore instead of piping a .sql file into psql, which also gives you options like restoring a single table later instead of the whole database. If you'd rather have a plain-text dump you can grep or diff, drop -F c and pipe pg_dump's output straight to a .sql file instead. Either way, that dump now exists as a file inside the container's filesystem and then, after the docker cp, sitting on the VPS itself. Neither of those locations is a backup yet.

Getting the dump off the host

A backup sitting on the same VPS as the database it was taken from dies with the VPS. If the disk fails, if the box gets compromised, if you fat-finger a docker command against the wrong volume, the dump goes with everything else. It has to leave the machine before it counts as protection against anything that can happen to the machine. Two workable options: copy it to a machine you control, or push it to object storage that isn't Coolify's own host.

scp your-vps:/root/backup.dump ./backups/backup-$(date +%F).dump

For anything you actually want to rely on, automate that trip off-box rather than remembering to scp a file by hand each time, and keep more than one copy in more than one place. Tools like rclone can push straight from the VPS to an S3-compatible bucket on a schedule, which is the same shape of thing Coolify's own backup feature is meant to do for a managed Postgres resource, just under your own control and something you've actually watched succeed.

The restore drill everyone skips

This is the step that turns a dump file into a backup, and it's the one that's easy to defer indefinitely because nothing is currently on fire. Spin up a throwaway Postgres, container or otherwise, that isn't your production database, and restore into it:

docker run --rm -d --name pg-restore-test -e POSTGRES_PASSWORD=test \
  -p 15433:5432 postgres:17-alpine

docker cp ./backup.dump pg-restore-test:/tmp/backup.dump
docker exec pg-restore-test createdb -U postgres restore_check
docker exec pg-restore-test pg_restore -U postgres -d restore_check /tmp/backup.dump

Then actually look at what you restored, not just whether the command exited without an error:

None of this is complicated once it's written down, which is exactly why it's the kind of task that gets skipped. A scheduled dump is one cron entry or one Coolify toggle. A restore drill takes ten minutes and produces nothing you can point to afterwards except confidence, right up until the day it produces everything. Do the drill before you need it, not while you need it.

Topics

Still stuck? Send it to Emsden Studio and get a straight answer on what it is and what fixing it costs.

More guides

All guides