Skip to content
Rowsafe
Docs

Adopt an existing database

Put a production PostgreSQL server under Rowsafe safely, step by step.

This guide puts a production PostgreSQL server under Rowsafe. At the end, every change is archived to your bucket, backups and the weekly Proof run on their own, and you get alerts. Coming from a nightly pg_dump, the most you can lose drops from 24 hours to about a minute.

The examples use a database app on a server db-1. Use your own names.

Safety principles

  • Read-only until you say yes. The installer shows its plan first.
  • No surprise restarts. Rowsafe restarts PostgreSQL only when you say yes in the installer, or when someone clicks Restart and confirms, on servers where you allowed it.
  • Never replaces another backup tool silently. If something else archives WAL, the plan refuses without --force.
  • Small, known changes. At most four settings, all with ALTER SYSTEM, all reversible. See the settings.
  • Keep your old backups until Rowsafe has proven itself.

Time: about an hour, plus one restart of a few seconds.

Check the server

On the server:

hostname                         # the host name Rowsafe will show
pg_lsclusters                    # e.g. 18  main  5432  online
sudo -u postgres psql -Xc "select name, setting from pg_settings where name in
  ('wal_level','archive_mode','archive_command','archive_library','archive_timeout')"
apt-cache policy pgbackrest | head -3
df -h / && free -h

Check that:

  • the OS is Debian 12/13 or Ubuntu 22.04/24.04 (amd64 or arm64), with PostgreSQL 13 to 18 on the primary;
  • archive_command is empty or (disabled), and archive_library is empty. If something else archives WAL, find out what first;
  • for PostgreSQL 18, pgBackRest is 2.55.0 or newer. Prefer apt.postgresql.org, which Rowsafe is tested with;
  • there's room for Proof: about 1.3 × the data directory + 1 GiB free.

Create the bucket

Create a private S3-compatible bucket, for example app-rowsafe. With Cloudflare R2:

  • R2, then Create bucket. For the EU, choose the EU jurisdiction. Leave public access off.
  • Don't add a lifecycle rule that deletes objects. Rowsafe expires backups itself; a rule would delete WAL that backups need and silently break restores.
  • In the bucket's Settings, copy the S3 API URL (https://<account-id>.eu.r2.cloudflarestorage.com/app-rowsafe).

Create an API token with Object Read & Write on this bucket only, limited to the server's IPs if you can. Copy the key ID and secret: they're shown once. Other providers: Set up backup storage.

Create the passphrase and store it first

Without the passphrase, no backup can be restored

Every backup is encrypted with it. Lose it and every backup is unreadable forever. Nobody can recover it, Rowsafe included. Store it in two places before the first backup.

openssl rand -base64 48

Store it with the bucket keys in your secret manager, apart from the app's secrets: the app shouldn't be able to read or delete its own backups. Put a second copy in your company password manager, and check that someone else can open it.

Run the installer

On the server:

curl -fsSL https://rowsafe.sh | sudo sh -s -- --account

It prints a link to the approval page. Check the code matches your terminal and approve (admins and owners only). The server gets a single-use enrollment token, never an API key.

Or from your own computer (macOS or Linux, CLI logged in), over SSH, with nothing to copy or approve:

rowsafe hosts add [email protected]

It uses your SSH login (any sudo user; -p PORT, -i KEY or a ~/.ssh/config name), makes a one-time token and runs the same installer, with its questions in your terminal. The token never appears in a command line on the server. Installer options go after --, for example rowsafe hosts add [email protected] -- --storage rowsafe.

To skip the approval, use the command from Servers, then Add a server. It holds a one-time token, valid for an hour (CLI: rowsafe hosts enroll-token):

curl -fsSL https://rowsafe.sh | sudo sh -s rse_...

The installer verifies the agent's signature and installs pgBackRest if missing (check its summary: signature verified, pgBackRest 2.55.0+). Then it asks:

  1. The bucket URL, key ID and secret. It tests a write, read and delete, and asks again with the reason if that fails.
  2. The passphrase. Choose Use my own passphrase and paste yours.
  3. What Rowsafe may do on this server: restart PostgreSQL (Restart or stop PostgreSQL, when someone clicks Restart or Rewind?), install updates, manage PgBouncer for connection pooling (installs nothing now), and so on. Always only when an admin or owner clicks and confirms. Change it later with sudo rowsafe-allow (permissions).
  4. The database name, for example after Found PostgreSQL 18 on port 5432 (1.2 GB; databases: app). It covers the whole PostgreSQL server: lowercase letters, digits and dashes.
  5. The plan, then Nothing changes until you say yes. Turn on backups for app now? [Y/n].

A typical plan:

ChangeFromToRestart
write /etc/rowsafe/pgbackrest/app.conf (0600)
pgbackrest stanza-create
archive_modeoffonyes
archive_command(empty)/usr/bin/pgbackrest --config=/etc/rowsafe/pgbackrest/app.conf --stanza=app archive-push %pno
archive_timeout060no

If wal_level is minimal, it becomes replica (also a restart). Answer n if the plan shows anything else, especially a warning about an existing archive_command.

On yes, the agent writes the config and runs stanza-create (if that fails, PostgreSQL is untouched), then sets the settings and reloads. No downtime.

Last: Restart PostgreSQL now? [y/N]. For production, answer N and restart in your window (next step).

Check the server reports in: in the dashboard, or rowsafe hosts list (db-1, last seen seconds ago). sudo journalctl -u rowsafe-agent -n 20 should show enrolled and no errors. Then delete the used-up ROWSAFE_ENROLL_TOKEN line from /etc/rowsafe/agent.env.

Restart PostgreSQL in a maintenance window

The database is awaiting_restart. The restart is the only downtime: usually 2 to 10 seconds of refused connections. Run a checkpoint right before to keep it short, and make sure your app reconnects by itself.

sudo -u postgres psql -Xc "select name from pg_settings where pending_restart"   # archive_mode
sudo -u postgres psql -Xc "checkpoint"
time sudo systemctl restart postgresql@18-main                                   # your cluster's unit
sudo -u postgres psql -Xc "show archive_mode"                                    # on

Use the cluster's systemd unit, so its restart policy stays in charge.

Check the first backup and run Proof

Rowsafe notices the restart by itself. It checks that changes reach your bucket, marks the database active, and starts the first full backup. To check now, click Check now or run rowsafe verify app.

rowsafe tasks app      # backup running ... succeeded
rowsafe backups app    # type, duration, database size, stored size

A few GB take several minutes. Then click Run Proof now, ideally in a quiet hour, or:

rowsafe proof app
rowsafe proofs app     # PASS, recovered to <time>, duration

A passing Proof is your first evidence you can restore. rowsafe status app now says app is PROTECTED.

Keep your old backups for 30 days

Keep your old backup job running for 30 days. Meanwhile:

  • every day, backups succeed (alerts tell you if not);
  • every week, the Sunday Proof passes;
  • once, practise: restore a copy and compare it, and do one restore by hand to a scratch server and run your app's smoke tests. Note how long each took.

Then retire the old job. Set up alert channels if you haven't.

Automate it

For cloud-init, config management or scripts, run the installer without questions:

  • Put the storage settings in its environment. It writes them to agent.env without printing them, and tests the bucket.
  • Add --protect NAME to register, plan and apply without prompts. It never restarts PostgreSQL; Rowsafe notices when you do.

Send secrets over SSH's standard input, so they never land in a shell history, file or command line (printf is a shell builtin, so they stay out of the process list):

printf 'export ROWSAFE_ENROLL_TOKEN=%s ROWSAFE_REPO_S3_KEY=%s ROWSAFE_REPO_S3_KEY_SECRET=%s ROWSAFE_REPO_CIPHER_PASS=%s\n' \
  "$ENROLL_TOKEN" "$S3_KEY" "$S3_SECRET" "$CIPHER_PASS" |
ssh admin@db-1 'sudo sh -c ". /dev/stdin;
  export ROWSAFE_REPO_S3_ENDPOINT=<account-id>.eu.r2.cloudflarestorage.com ROWSAFE_REPO_S3_BUCKET=app-rowsafe;
  curl -fsSL https://rowsafe.sh | sh -s -- --protect app"'

If the plan needs anything unusual (another tool archives WAL), --protect stops and changes nothing. --no-allow-restart keeps Restart off on this server; --allow-restart turns it on. See the installer options.

Add the database from the dashboard or the CLI

Use this after --no-setup (agent and storage only), when a server runs more than one PostgreSQL, or for PostgreSQL in Docker.

In the dashboard: Databases, then Add database. Or:

rowsafe adopt app --host db-1    # register and print the read-only plan
rowsafe apply app                # apply it; you confirm with y

adopt only reads; rowsafe plan app shows the plan again. Options: --port (default 5432), --socket-dir (default /var/run/postgresql), --retention-full (default 2 full backups). apply never restarts PostgreSQL.

Rollback

Every step can be undone. Pick how far to go.

Keep the bucket and passphrase until you're sure you won't need any backup in it.

Edit on GitHub