You need two things: sloop installed — the one-liner takes a few seconds — and a PostgreSQL, MySQL or MariaDB database it can reach, with a user, a password and a port. Nothing else has to be set up in advance.
The example below uses a PostgreSQL database called shop with two tables and 10,601 rows in it. Substitute your own connection string and every command is the same.
1 · Set sloop up on this machine
sloop keeps its own state — the registry of what you have told it about, the schedules, what the background service has seen — in a PostgreSQL 18 of its own. Run this once.
$ sloop setupNeeded: sloop keeps its own state in PostgreSQL 18, and this machine has none. sloop can ask apt to install them: sudo apt-get install -y postgresql-18? Run that apt command now? y …sloop does not download a server here. It offers to run your own package manager, shows you the exact command before it asks, and runs nothing until you answer — so what ends up on the machine is a package your package manager knows about and can update, not a directory sloop unpacked.
Afterwards it uses a PostgreSQL 18 serving on 5432 if there is one, and otherwise builds a cluster of its own on 5433, which disturbs nothing already running.
Only the package manager step needs sudo — that is the command in the block above, and it is shown to you before it is run. Everything sloop itself puts on the machine goes under /home/you/.sloop and needs no root at all, and no service is registered with the machine at this stage.
When it has finished it prints where your state now lives — the cluster it used or made, and the connection string for sloop's own database, which is sloop_database owned by sloop_db_admin, with that role's password in the Secret Service. sloop's own PostgreSQL covers all of it, including how to open that database yourself.
It is safe to run again. On a machine that is already set up it reports what it found rather than changing anything, and brings the schema forward if a newer sloop needs a newer one — so it is the right thing to reach for when you are not sure, and after an upgrade.
2 · Register your first database
Tell sloop about a database once and you refer to it by name from then on. The name is yours — the label you will type — and it does not have to match what the database is called on the server.
$ sloop db add shop --global --url postgres://app@127.0.0.1:5441/shopregistered shop in the global registry (--global), password from the OS keyring postgres://app@127.0.0.1:5441/shopIt asks for the password and does not echo it, then stores it in the Secret Service. The registry holds a route to the password, never the password. There is no spelling of a secret that a registry will accept, which is what makes it impossible to put one there by accident.
If that is not what you want, --encrypted-file keeps it in an Argon2id-encrypted file, --env ORDERS_PW reads it from an environment variable at run time, and --password-from "op read op://vault/db/pw" runs a command and takes it from the output. The password never appears in argv, so it never appears in a process listing.
Check it before going further:
$ sloop db test shop --global shop postgres://app@127.0.0.1:5441/shop global✓ postgres 17.9 not encrypted3 · Export the backup key
This is a step, not an aside, and the next command will refuse without it. Backups are encrypted with an age keypair, and the asymmetry is the point: the public key sits in the registry so a scheduled backup needs no secret to unlock, and the private key lives in the Secret Service so only a restore ever reads one.
$ sloop key export --globala new backup key for /home/you/.sloop public age1w4k7mz0qx9v2lr5htn3jd8pc6yfs0ueg7a2xk9msv4rzq3ltdn6sh8wcjp private the OS keyring, and on standard output below keep that line somewhere sloop cannot reach — a password manager, a safe. Every encrypted backup this registry takes needs it, and nothing else can replace it. AGE-SECRET-KEY-1QF8TZ3KXW9MV0RJ5H2LYD7NPS4UGC6AE8QT7XZ3KMW9VRJ5H2LYDQ7X4NPThat last line is the whole of it. Put it in a password manager, or on paper in a drawer — somewhere sloop itself cannot reach, because a backup and the key to it sitting on the same disk is one disk failure away from being neither.
4 · Take the first backup
One command, and it does not report success until it has counted what arrived.
$ sloop backup shop --globalshop postgres://app@127.0.0.1:5441/shop✓ Connected postgres 17.9, 2 tables, 10601 rows✓ Dumped 2 tables 90.5 kB, encrypted✓ Checked 1c90608b129c backed up to /home/you/.sloop/backups/postgres/shop/20260919T124114Z 88.4 KiB in 0.5s, sha256 1c90608b129c taken 2026-09-19 18:41:14 +06:00 (2026-09-19T12:41:14Z) Two timestamps, and both are deliberate: the directory is named in UTC so backups sort correctly and survive a daylight-saving change, and every display is in local time so a human reads the one they think in. The sha256 is over the dump as written, and backups list --check compares it again later.
Beside the dump sloop writes a manifest, which is what makes a backup something you can reason about a year later rather than a file of unknown provenance:
$ cat manifest.json{ "version": 1, "sloop": "0.1.5", "label": "shop", "engine": "postgres", "source": "postgres://app@127.0.0.1:5441/shop", "server": { "version": "17.9", "tls": false }, "taken": { "utc": "2026-09-19T12:41:14Z", "local": "2026-09-19T18:41:14+06:00", "offset": "+06:00", "offset_seconds": 21600, "unix": 1789821674 }, "took_seconds": 0.765, "dump_seconds": 0.487, "dump": { "file": "dump.age", "bytes": 90525, "sha256": "1c90608b129ccc8f39cc61395ad374f99ea...", "encryption": { "format": "age", "recipient": "age1w4k7mz0..." } }, "rows": 10601, "tables": [ { "schema": "public", "name": "customers", "rows": 1284 }, { "schema": "public", "name": "orders", "rows": 9317 } ]}5 · See what you have
Newest first, sizes totalled, and every time in local time. --check re-reads every byte of every dump and hashes it against what the manifest recorded — which is a thing you ask for rather than something a listing does on the way past.
$ sloop backups list --globalshop postgres · 1 backup, 88.4 KiB 2026-09-19 18:41:14 +06:00 88.4 KiB, encrypted · 10601 rows 1 backup, 88.4 KiB · sizes checked, --check hashes them $ sloop backups list --global --checkshop postgres · 1 backup, 88.4 KiB 2026-09-19 18:41:14 +06:00 88.4 KiB, encrypted · 10601 rows 1 backup, 88.4 KiB · every dump hashed6 · Put it back
A backup nobody has restored is a hope, not a backup. Do this once now, against something you do not mind replacing, so that the first time you run it is not the day you need it.
$ sloop restore shop --globalrestoring /home/you/.sloop/backups/postgres/shop/20260919T124114Z taken 2026-09-19 18:41:14 +06:00 (2026-09-19T12:41:14Z) 88.4 KiB of postgres 17.9, encrypted, 10601 rows, sha256 1c90608b129c into shop — postgres 17.9 replacing what is there now — 2 tables, 10601 rows? type shop to destroy it, or anything else to stop: shop cleared 2 tables loaded in 0.1s checking it against the counts the manifest recorded✓ Verified exact count(*) on both sides verifying — exact count(*) on both sides public.customers 1284 → 1284 public.orders 9317 → 9317 2 of 2 tables matchedThe name is typed, never clicked. A y is one keystroke away from a mistake, and this command replaces what is in a database; typing the name out means the thing being destroyed has been read. With no terminal to ask at it does not prompt at all — it exits 2 and names --confirm shop, which has to match exactly, so a scheduled line names what it replaces and cannot be repointed at something else by editing one flag.
And it verifies. Not an estimate: count(*) on both sides, per table, compared with what the manifest recorded. If the numbers disagree the run exits 6 — which is neither success nor a crash, and is the exit code worth wiring an alert to.
Where to go next
That is the whole loop. Three things are usually next:
- Stop doing it by hand.The background service registers sloop with your machine and takes the backup on a schedule with nobody logged in.
- Check the role before you trust the dumps.sloop doctor reports what each registered role may actually do. On MySQL and MariaDB, one missing grant produces a dump with every trigger and stored routine silently absent from it, and an exit code of
0. - Copy a database instead of backing it up.mirror and sync move one database into another — exactly, or merged. They are different commands and the difference matters.