RUBY ON RAILS · 15 MIN READ ·

Rails Postgres-back-ups met pgBackRest: point-in-time recovery, S3-opslag en restore drills

Rails Postgres-back-ups met pgBackRest: volledige en differentiële back-ups naar S3, WAL-archivering, point-in-time recovery en maandelijkse restore drills.

Rails Postgres-back-ups met pgBackRest: point-in-time recovery, S3-opslag en restore drills

Het Slack-bericht kwam binnen op zondagochtend om 07:41. Een junior-developer had een rake-taak gedraaid in productie die een kolom moest bijwerken op 40.000 rijen. De taak had tegen de verkeerde tabel gedraaid en deleted_at = Time.current gezet op 812.000 klantorders. De teamlead zat in een vliegtuig. De CEO was wakker. De database draaide gewoon door — al die rijen werden netjes verstopt door de default scope, en noch PagerDuty noch de applicatie had door dat er iets mis was.

Om 07:52 hing ik met ze aan de telefoon. Om 08:07 hadden we een kopie van de database teruggezet naar 07:38 UTC op een verse instance. Om 08:34 hadden we een diff van de 812.000 getroffen rijen en een script dat in productie draaide en precies de schade van de rake-taak omkeerde. Niemand raakte data kwijt. De CEO ging weer slapen.

Dát is wat Rails Postgres-back-ups met pgBackRest je opleveren, en dat is precies wat een nachtelijke pg_dump naar S3 níét kan. Na negentien jaar Rails heb ik teams zien aankomen die back-ups behandelden als een vinkje en disaster recovery als “we hebben een snapshot op RDS.” Geen van beide overleeft dat telefoontje op zondagochtend. Deze post beschrijft de setup die ik op elk serieus Rails-productiesysteem installeer: pgBackRest, S3, WAL-archivering en een restore drill die je elke maand draait.

Waarom een nachtelijke pg_dump geen back-upstrategie is

pg_dump is een prima tool. Het is geen back-upstrategie. Drie redenen waarom het faalt op het moment dat je het nodig hebt:

  • RPO is 24 uur. Als je dump om 03:00 draait en het incident gebeurt om 20:00, ben je 17 uur data kwijt. Een moderne SaaS kan een klant niet met droge ogen beloven dat ze maar 17 uur werk verliezen.
  • Recovery zit vast op één tijdstip. Je krijgt wat de staat om 03:00 was. Je kunt niet terugspoelen naar 07:38 om te zien hoe de wereld eruitzag vóór de rake-taak losging. Wil je dat, dan heb je WAL-archivering (write-ahead log) nodig.
  • Het schaalt slecht. Een dump van 40 GB duurt vijftien minuten om te schrijven en vijfenveertig om terug te zetten. Een dump van 400 GB kost uren. Op die schaal is pg_dump niet iets waarvan je in een storing wilt lezen.

RDS-automatische back-ups lossen het eerste probleem voor je op — als, en alleen als, je het binnen het retentievenster opmerkte, de regio niet plat ligt, en je kunt herstellen naar een nieuwe instance en het verkeer binnen je RTO omzetten. Ze lossen niet op: “geef me een kopie van de database op mijn laptop om 07:38 UTC zodat ik een diff kan draaien.”

pgBackRest lost alle drie op. Het maakt volledige, differentiële en incrementele back-ups; archiveert elk WAL-segment naar S3; en laat je het cluster terugzetten op elke seconde tussen de oudste back-up en het laatst gearchiveerde segment. Het is wat Amazon RDS er onder de motorkap voor gebruikt. Het is wat elke Postgres-consultant met een telefoonnummer dat je moet bellen op dag één installeert.

pgBackRest installeren voor een Rails-Postgres-cluster

Op Ubuntu 24.04 met Postgres 16 is de installatie één apt-regel:

apt-get install -y pgbackrest

De configuratie leeft in /etc/pgbackrest/pgbackrest.conf. Een minimale productieconfig voor één Rails-app met S3-opslag ziet er zo uit:

[global]
repo1-type=s3
repo1-s3-bucket=ttb-postgres-backups
repo1-s3-region=eu-central-1
repo1-s3-endpoint=s3.eu-central-1.amazonaws.com
repo1-s3-key-type=auto
repo1-path=/pgbackrest
repo1-retention-full=4
repo1-retention-diff=6
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=REDACTED_LONG_RANDOM_PASSPHRASE

process-max=4
compress-type=zst
compress-level=3
log-level-console=info
log-level-file=detail
start-fast=y
archive-async=y
spool-path=/var/spool/pgbackrest

[app]
pg1-path=/var/lib/postgresql/16/main
pg1-port=5432
pg1-user=postgres

Twee dingen in die config zijn belangrijker dan de rest.

repo1-cipher-pass versleutelt elke back-up en elk WAL-segment vóórdat het de host verlaat. Het S3-bucketbeleid is niet je laatste verdedigingslinie — de client-side encryptie van pgBackRest is dat. Genereer een passphrase met openssl rand -base64 48 en bewaar hem in je secrets manager. Verlies je ‘m, dan zijn je back-ups onherstelbaar, en dat is precies de bedoeling.

repo1-s3-key-type=auto vertelt pgBackRest dat het de IAM-rol van de instance (of IMDS) moet gebruiken in plaats van een statische access key. Geef de EC2-instance een IAM-rol met alleen s3:GetObject, s3:PutObject, s3:DeleteObject en s3:ListBucket op de bucket. Nooit IAM-keys in /etc/pgbackrest/pgbackrest.conf.

WAL-archivering aanzetten in Postgres

Back-ups zonder WAL-archivering geven je nachtelijke snapshots. WAL-archivering is wat je point-in-time recovery oplevert.

In postgresql.conf:

archive_mode = on
archive_command = 'pgbackrest --stanza=app archive-push %p'
archive_timeout = 60
max_wal_senders = 5
wal_level = replica

archive_timeout = 60 dwingt Postgres om een WAL-segment minstens elke 60 seconden af te sluiten, ook als het niet vol is. Dat maximeert je worst-case RPO op 60 seconden verloren transacties — de prijs is een paar extra kleine WAL-bestanden per dag, en die verwerkt pgBackRest voor een dubbeltje.

Herstart Postgres, initialiseer daarna de stanza en maak de eerste back-up:

sudo -u postgres pgbackrest --stanza=app stanza-create
sudo -u postgres pgbackrest --stanza=app --type=full backup

Controleer de status:

sudo -u postgres pgbackrest --stanza=app info

Je wilt status: ok zien, minstens één volledige back-up en — cruciaal — een “archive”-sectie die laat zien dat WAL-segmenten worden gepusht. Is de archive-sectie leeg, dan roept Postgres het archive_command niet correct aan en heb je geen PITR-venster. Dat fix je vóórdat je gaat slapen.

Het back-upschema dat ik daadwerkelijk gebruik

Wekelijks volledig, dagelijks differentieel en continu WAL-archivering via archive_command. In /etc/cron.d/pgbackrest:

# wekelijkse full back-up, zondag 02:15 UTC
15 2 * * 0  postgres  pgbackrest --stanza=app --type=full backup

# dagelijkse differentiële back-up, ma-za 02:15 UTC
15 2 * * 1-6  postgres  pgbackrest --stanza=app --type=diff backup

# uurlijkse expire-check om retentie af te dwingen
0 * * * *  postgres  pgbackrest --stanza=app expire

Op een Postgres-cluster van 100 GB levert dat ruwweg 30 GB gecomprimeerd op zondag, 3-8 GB per differentiële back-up op werkdagen en zo’n 15 GB WAL per dag. Op S3 Standard-IA blijf je onder de $10 per maand voor vier weken retentie met 60 seconden RPO. Niemand heeft ooit spijt gehad van die uitgave.

Point-in-time recovery: het restore op zondagochtend

Dit is het scenario waarmee de post begon. Het is 07:52 UTC op zondag. Er draaide om 07:38 een rake-taak die we willen terugdraaien. Herstel de database naar 07:37:30 UTC op een verse instance:

# op een verse Postgres 16-host met pgBackRest geïnstalleerd en geconfigureerd
sudo -u postgres pgbackrest \
  --stanza=app \
  --type=time \
  --target="2026-09-09 07:37:30+00" \
  --delta \
  restore

Start daarna Postgres:

sudo systemctl start postgresql@16-main

pgBackRest downloadt de meest recente full en differentiële back-ups, past WAL-segmenten toe tot en met 07:37:30, en stopt. Postgres komt op in recovery mode, promoveert wanneer het target bereikt is, en je hebt een lees-schrijfkopie van de database van precies de seconde vóór het incident.

Vanaf daar exporteer je de getroffen rijen naar een CSV, stuur je de CSV naar productie en schrijf je een gerichte UPDATE die precies de schade terugdraait. Je zet géén verkeer over naar de herstelde kopie. Productie draait al die tijd gewoon door; je hebt alleen de historische view nodig om de diff te berekenen. Negenennegentig procent van alle “herstellen uit back-up”-incidenten is in werkelijkheid “geef me een read-only snapshot van het verleden zodat ik kan uitzoeken wat er veranderd is.”

De Rails rake-taken die ik met elke app meelever

Deze zet ik in lib/tasks/backups.rake in elke Rails-app die ik opzet. Ze wikkelen pgBackRest in een Rails-native interface zodat iedereen in het team de status kan controleren zonder in de databasehost te SSH-en.

namespace :backups do
  desc "Toon pgBackRest info voor de primaire database"
  task info: :environment do
    output = `ssh postgres@#{db_host} pgbackrest --stanza=app info`
    puts output
    abort "pgBackRest info faalde" unless $?.success?
  end

  desc "Controleer of de laatste back-up jonger is dan drempelwaarde uur (default 26)"
  task :verify, [:hours] => :environment do |_, args|
    hours = (args[:hours] || 26).to_i
    json = `ssh postgres@#{db_host} pgbackrest --stanza=app --output=json info`
    data = JSON.parse(json)
    stop = data.dig(0, "backup", -1, "timestamp", "stop")
    age_hours = (Time.now.to_i - stop) / 3600.0

    if age_hours > hours
      abort "Laatste back-up is #{age_hours.round(1)}u oud (drempel #{hours}u)"
    else
      puts "Laatste back-up #{age_hours.round(1)}u oud — OK"
    end
  end

  def db_host
    ActiveRecord::Base.connection_db_config.configuration_hash[:host]
  end
end

Koppel de backups:verify-taak aan een geplande job — Solid Queue recurring jobs is hier prima voor — en laat ‘m je pagen als de laatste back-up ouder is dan 26 uur. Back-upjobs falen standaard stilletjes. Het hele punt van alerting is dat je op dinsdag ontdekt dat de differentiële back-up van zondag mislukt is, en niet de zondagochtend erna op het moment dat je hem nodig hebt.

Restore drills: de oefening die echte recovery scheidt van wishful thinking

Elk team dat ik heb meegemaakt dat back-ups had geconfigureerd maar geen drill draaide, heeft minstens één keer tijdens een echt incident ontdekt dat de back-ups niet terug te zetten waren. WAL-archivering was vier maanden geleden gestopt. De S3-bucket zat in een andere regio dan de encryptiesleutel. De passphrase in de secrets manager bevatte een typfout. Niemand kwam er achter tot het ertoe deed.

Blok elke eerste maandag van de maand een slot van twee uur. Wijs ‘m toe aan een roulerende on-call engineer. De drill is:

  1. Zet een verse EC2-instance op in een niet-productie-account.
  2. Installeer Postgres 16 en pgBackRest en drop de versleutelde config uit je secrets manager erin.
  3. Herstel de database met --type=time naar een target van vier uur geleden.
  4. Start Postgres, draai SELECT count(*) FROM users, SELECT max(created_at) FROM orders en één representatieve productiequery.
  5. Meet de tijd van terraform apply tot psql-prompt. Schrijf het op in een Notion-pagina “Laatste Restore Drill.”
  6. Sloop de instance.

De metric die telt is time-to-first-query, en dat is je realistische RTO. Bij ons is dat rond de 22 minuten op 120 GB. Een team dat nooit heeft gedrild, ontdekt dat hun echte RTO 8 uur is, omdat de helft daarvan opgaat aan het achterhalen van de encryptiepassphrase.

Draai je ook Postgres logical replication of monitor je met PgHero, drill die dan op dezelfde manier. Een capaciteit die je drie maanden niet hebt geoefend, is een capaciteit die je niet hebt.

Wat ik níét doe

  • Geen pg_dump naar S3 als primaire back-up. Prima als secundaire “geef me een draagbare schema+data-snapshot”-tool, waardeloos als recovery-strategie op echte schaal.
  • Nooit alleen leunen op RDS-automatische back-ups. Ze zijn goed, maar ze zitten in één regio, één account en één AWS-factureringsrelatie. Cross-account, versleuteld, client-side pgBackRest naar S3 in een andere regio overleeft dingen die RDS-back-ups niet overleven.
  • Nooit WAL-archivering overslaan. Zonder klapt het hele systeem terug naar nachtelijke snapshots. Het is de belangrijkste regel in postgresql.conf voor elke productie-Rails-app.
  • Nooit ongeteste restore-procedures. Een back-up die nooit is teruggezet, is de kat van Schrödinger. Ga ervan uit dat ‘m kapot is tot de drill bewijst dat ‘t niet zo is.

Veelgestelde vragen

Hoeveel kost pgBackRest naar S3 nu écht per maand?

Voor een typische Series A Rails-app met een Postgres-cluster van 100 GB verwacht je rond de $8-15 per maand op S3 Standard-IA voor vier weken retentie met 60 seconden WAL-archivering. Kostenopbouw: één wekelijkse volledige back-up (~30 GB gecomprimeerd met zstd), zes dagelijkse differentiëlen (~3-8 GB per stuk), en zo’n 15 GB WAL per dag. Zet S3-lifecycleregels aan om back-ups ouder dan 30 dagen naar Glacier Deep Archive te verplaatsen als je langetermijnkopieën bewaart voor compliance. De S3-rekening voor back-ups is bijna altijd de goedkoopste regel op de hele infrastructuurfactuur — laat kosten nooit de reden zijn om WAL-archivering over te slaan.

Kan ik pgBackRest gebruiken met Amazon RDS of Aurora?

Nee, en het hoeft ook niet. RDS en Aurora nemen allebei hun eigen snapshots en archiveren WAL intern — je krijgt automatische back-ups en PITR out of the box, al is dat alleen binnen je retentievenster (tot 35 dagen) en alleen binnen AWS. Wil je cross-account of cross-region versleutelde back-ups buiten AWS, gebruik dan wekelijks een pg_dump naar een apart account als defense-in-depth, of draai een zelfbeheerde logische replica en back-up die met pgBackRest. Voor self-hosted Postgres op EC2, Hetzner of bare metal is pgBackRest het juiste antwoord.

Wat is een realistische RTO en RPO voor een Rails-Postgres-app met pgBackRest?

Met archive_timeout = 60 en asynchrone WAL-push is je worst-case RPO ruwweg 60-90 seconden verloren transacties na totale host-uitval — het laatste WAL-segment is misschien nog niet naar S3 gepusht. RTO hangt af van databasegrootte en hardware: op een cluster van 120 GB met 4 vCPU’s en gp3-storage verwacht je zo’n 20-25 minuten van een koude EC2-instance tot een psql-prompt op de herstelde database. Op 1 TB moet je rekenen op 60-90 minuten. Beide getallen gaan ervan uit dat je het proces gedrild hebt. Ongedrild kost het uren extra aan zoeken naar de passphrase en IAM-debuggen.

Heb ik pgBackRest nog nodig als ik al Postgres streaming replication draai?

Ja. Streaming replication beschermt tegen hardwarefalen van de primaire — het beschermt niet tegen logische corruptie. Als een developer UPDATE orders SET deleted_at = now() draait op 800.000 rijen, repliceert dat statement binnen milliseconden naar elke standby. Streaming replication en PITR-back-ups lossen andere problemen op, en elke serieuze productie-Rails-setup heeft ze allebei. Streaming replication is je high-availability-verhaal; pgBackRest met WAL-archivering is je disaster recovery en point-in-time undo.

Hulp nodig bij het opzetten van productiewaardige Postgres-back-ups, WAL-archivering en restore drills voor je Rails-app? TTB Software doet Rails-infrastructuurwerk en Postgres-reviews voor teams die zich geen dataverlies kunnen veroorloven. Negentien jaar Rails, en ik heb nog nooit een byte verloren voor een klant.

#rails-postgres-backups #pgbackrest #postgres-point-in-time-recovery #postgres-wal-archiving #rails-disaster-recovery #postgres-s3-backups #postgres-restore-drill

Related Articles

Laatste sectie. Bel dan alsjeblieft.

Het is een telefoongesprek. Erger dan dat kan het niet worden.

Geen discovery-deck. Geen 45-minuten "kwalificatiegesprek." 30 minuten, jouw probleem, mijn mening. Als we een fit zijn weet je dat in minuut 12.

Directe lijn — Roger neemt zelf op
+31 6 5123 6132
Ma–vr, 09:00–18:00 CET · Nu beschikbaar

OF
info@ttb.software