Skip to content
sloop

Start here

Getting started

From an empty machine to a backup you have actually restored, in six commands. Everything below is real output from a real run, in the order you would type it.

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.

Showing Linux. Change it in the strip above and the whole page follows.

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.

a shell, on Linux
$ 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.

registering it
$ 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/shop

It 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:

does it connect
$ sloop db test shop --global shop postgres://app@127.0.0.1:5441/shop global✓ postgres 17.9 not encrypted

3 · 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.

the one thing to keep
$ 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-1QF8TZ3KXW9MV0RJ5H2LYD7NPS4UGC6AE8QT7XZ3KMW9VRJ5H2LYDQ7X4NP

That 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.

What losing it costs

Lose the machine and you still have the backups, as long as you have this line — sloop key import puts it on the new one. Lose this line and every encrypted backup that registry ever took is unreadable, by you and by anybody. Nothing can replace it, which is why sloop refuses to take the first encrypted backup until it has been exported or you have explicitly declined.

4 · Take the first backup

One command, and it does not report success until it has counted what arrived.

the first one
$ 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:

manifest.json, beside the dump
$ 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.

what is stored
$ 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 hashed

6 · 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.

restoring 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 matched

The 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.

sloop — a database operations CLI for PostgreSQL, MySQL and MariaDB. MIT or Apache-2.0.