Aller au contenu
medium

Sauvegarder et restaurer PostgreSQL : pg_dump, pg_basebackup et PITR

27 min de lecture

Logo PostgreSQL

Un DROP TABLE en production, une corruption de disque, un ransomware — sans sauvegarde testée, vous perdez tout. PostgreSQL fournit deux familles d’outils complémentaires : les dumps logiques (pg_dump) pour exporter et restaurer des bases individuelles, et les sauvegardes physiques (pg_basebackup) combinées à l’archivage WAL pour restaurer le cluster entier à un instant précis (PITR — Point-In-Time Recovery).

Ce guide couvre les deux approches de bout en bout, avec chaque commande testée sur un lab réel (PostgreSQL 18.3, Debian 12). Vous saurez quelle méthode choisir, comment la mettre en place, et surtout comment tester que la restauration fonctionne — parce qu’une sauvegarde non testée n’est pas une sauvegarde.

  • Choisir entre sauvegarde logique et physique selon le contexte (volume, RPO, besoin de PITR)
  • Réaliser un pg_dump en formats custom, directory et plain, et restaurer avec pg_restore
  • Configurer l’archivage WAL continu (archive_mode, archive_command)
  • Réaliser une sauvegarde physique avec pg_basebackup et la vérifier avec pg_verifybackup
  • Restaurer à un instant précis (PITR) après un DROP TABLE accidentel
  • Définir une stratégie de sauvegarde adaptée à votre volume

Vous devez mettre en place des sauvegardes PostgreSQL dans ces situations :

  • Vous passez une base en production et avez besoin d’un plan de reprise d’activité
  • Vous devez pouvoir annuler une erreur humaine (DROP TABLE, UPDATE sans WHERE) en restaurant à l’instant qui précède
  • Vous migrez une base d’un serveur à un autre, ou d’une version de PostgreSQL à une autre
  • Vous devez dupliquer une base pour créer un environnement de test
  • Vous automatisez des sauvegardes quotidiennes dans un cron ou un pipeline CI/CD
  • PostgreSQL installé et le service actif (voir le guide Installation)
  • Accès au rôle superuser (postgres)
  • wal_level = replica (valeur par défaut — vérifiez avec SHOW wal_level;)
  • Connaître les bases de psql (voir le guide Prise en main de psql)

Logique vs physique : le bon outil pour le bon besoin

Section intitulée « Logique vs physique : le bon outil pour le bon besoin »
CritèreLogique (pg_dump)Physique (pg_basebackup)
GranularitéUne base, un schéma ou une tableLe cluster entier
PITR possibleNonOui (avec archivage WAL)
PortabilitéMigration vers des versions plus récentes, archives portables entre architectures (mais pg_dump ne peut pas dumper un serveur plus récent que sa propre version majeure)Même version majeure uniquement
Vitesse de dumpLente sur gros volumes (lecture SQL)Rapide (copie de fichiers)
Vitesse de restaurationLente (rejeu SQL)Rapide (copie de fichiers)
Inclut les rôlesNon (sauf pg_dumpall)Oui (cluster complet)

Règle pratique :

  • pg_dump quand vous devez sauvegarder une base précise, migrer entre versions ou créer un dump portable
  • pg_basebackup + archivage WAL quand vous avez besoin de PITR, de RPO faible ou de sauvegardes rapides sur de gros volumes

pg_dump propose trois formats principaux. Chacun a son usage :

FormatOptionExtensionCompressionRestauration parallèleUsage principal
Custom-Fc.dumpOui (gzip)OuiLe plus courant — flexible, sélectif
Directory-Fddossier/Oui (gzip)Oui (natif)Gros volumes, dump parallèle
Plain-Fp.sqlNon (sauf pipe)NonLisible, versionnable, migration cross-version

Le format recommandé pour les sauvegardes courantes :

Fenêtre de terminal
sudo -u postgres pg_dump -Fc lab_admin -f /tmp/lab_admin.dump
ls -lh /tmp/lab_admin.dump
-rw-r--r-- 1 postgres postgres 5.8K Apr 13 10:15 /tmp/lab_admin.dump

Le fichier est compressé (gzip intégré) et contient toute la structure + les données. Avec -Fc, vous pourrez ensuite restaurer sélectivement (une table, un schéma) grâce à pg_restore.

Utile pour les gros volumes grâce au dump parallèle :

Fenêtre de terminal
sudo -u postgres pg_dump -Fd lab_admin -f /tmp/lab_admin_dir -j 2
total 16K
-rw-r--r-- 1 postgres postgres 183 Apr 13 10:15 3389.dat.gz
-rw-r--r-- 1 postgres postgres 127 Apr 13 10:15 3391.dat.gz
-rw-r--r-- 1 postgres postgres 5.5K Apr 13 10:15 toc.dat

Chaque table produit un fichier .dat.gz séparé. L’option -j 2 utilise 2 jobs parallèles — adaptez au nombre de CPU disponibles.

Produit un script SQL lisible :

Fenêtre de terminal
sudo -u postgres pg_dump -Fp lab_admin | head -20
--
-- PostgreSQL database dump
--
-- Dumped from database version 18.3 (Debian 18.3-1.pgdg12+1)
-- Dumped by pg_dump version 18.3 (Debian 18.3-1.pgdg12+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET transaction_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

Le format plain est restauré avec psql, pas avec pg_restore. C’est le seul format directement lisible et versionnable dans Git.

Pour ne sauvegarder qu’une partie de la base :

Fenêtre de terminal
# Une seule table
sudo -u postgres pg_dump -Fc lab_admin --table=app.clients -f /tmp/clients_only.dump
# Un schéma entier
sudo -u postgres pg_dump -Fc lab_admin --schema=app -f /tmp/schema_app.dump
# Tout sauf une table
sudo -u postgres pg_dump -Fc lab_admin --exclude-table=app.commandes -f /tmp/sans_commandes.dump

pg_dump ne sauvegarde pas les rôles ni les tablespaces — ils sont globaux au cluster. Pour capturer ces objets :

Fenêtre de terminal
sudo -u postgres pg_dumpall --globals-only > /tmp/globals.sql
--
-- Roles
--
CREATE ROLE admin_lab;
ALTER ROLE admin_lab WITH NOSUPERUSER INHERIT NOCREATEROLE CREATEDB LOGIN
NOREPLICATION NOBYPASSRLS PASSWORD 'SCRAM-SHA-256$4096:...';
CREATE ROLE postgres;
ALTER ROLE postgres WITH SUPERUSER INHERIT CREATEROLE CREATEDB LOGIN
REPLICATION BYPASSRLS;

Créez une base cible vide, puis restaurez :

Fenêtre de terminal
sudo -u postgres psql -c "CREATE DATABASE lab_restore OWNER admin_lab;"
sudo -u postgres pg_restore -d lab_restore --verbose /tmp/lab_admin.dump
pg_restore: connecting to database for restore
pg_restore: creating SCHEMA "app"
pg_restore: creating TABLE "app.clients"
pg_restore: creating SEQUENCE "app.clients_id_seq"
pg_restore: creating TABLE "app.commandes"
pg_restore: creating SEQUENCE "app.commandes_id_seq"
pg_restore: processing data for table "app.clients"
pg_restore: processing data for table "app.commandes"
pg_restore: creating CONSTRAINT "app.clients clients_pkey"
pg_restore: creating CONSTRAINT "app.commandes commandes_pkey"
pg_restore: creating FK CONSTRAINT "app.commandes commandes_client_id_fkey"

Vérification :

SELECT count(*) AS nb_clients FROM app.clients;
nb_clients
------------
5

Les 5 clients sont bien restaurés.

pg_restore —list et —use-list : restauration sélective

Section intitulée « pg_restore —list et —use-list : restauration sélective »

Le format custom permet de lister le contenu du dump et de ne restaurer qu’une partie :

Fenêtre de terminal
sudo -u postgres pg_restore --list /tmp/lab_admin.dump
;
; Archive created at 2026-04-13 10:15:32 UTC
; dbname: lab_admin
; TOC Entries: 21
; Compression: gzip
; Format: CUSTOM
;
; Selected TOC Entries:
;
6; 2615 16390 SCHEMA - app postgres
221; 1259 16392 TABLE app clients postgres
220; 1259 16391 SEQUENCE app clients_id_seq postgres
223; 1259 16407 TABLE app commandes postgres
222; 1259 16406 SEQUENCE app commandes_id_seq postgres
3389; 0 16392 TABLE DATA app clients postgres
3391; 0 16407 TABLE DATA app commandes postgres
3235; 2606 16405 CONSTRAINT app clients clients_email_key postgres
3237; 2606 16403 CONSTRAINT app clients clients_pkey postgres
3239; 2606 16418 CONSTRAINT app commandes commandes_pkey postgres
3240; 2606 16419 FK CONSTRAINT app commandes commandes_client_id_fkey postgres

Pour ne restaurer que les données (pas la structure) :

Fenêtre de terminal
# Extraire la liste, ne garder que TABLE DATA
sudo -u postgres pg_restore --list /tmp/lab_admin.dump | grep 'TABLE DATA' > /tmp/data_only.list
# Restaurer uniquement ces entrées
sudo -u postgres pg_restore -d lab_restore --use-list=/tmp/data_only.list /tmp/lab_admin.dump

C’est indispensable quand vous devez recharger les données dans une base dont la structure existe déjà.

Avant de lancer pg_basebackup, vérifiez :

SHOW wal_level;
wal_level
-----------
replica

wal_level doit être au moins replica (c’est la valeur par défaut). Si c’est minimal, changez-le et redémarrez — c’est un paramètre postmaster.

La forme la plus courante pour un usage PITR :

Fenêtre de terminal
sudo -u postgres pg_basebackup \
-D /var/lib/postgresql/backups/base_backup \
-Fp -Xs -P -v
pg_basebackup: initiating base backup, waiting for checkpoint to complete
pg_basebackup: checkpoint completed
pg_basebackup: write-ahead log start point: 0/4000028 on timeline 1
pg_basebackup: starting background WAL receiver
pg_basebackup: created temporary replication slot "pg_basebackup_7042"
39450/39450 kB (100%), 1/1 tablespace
pg_basebackup: write-ahead log end point: 0/4000120
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: syncing data to disk ...
pg_basebackup: renaming backup_manifest.tmp to backup_manifest
pg_basebackup: base backup completed
OptionRôle
-DRépertoire de destination (doit être vide ou inexistant)
-FpFormat plain (copie directe des fichiers — prêt pour PITR)
-XsStream les WAL pendant la sauvegarde (pas besoin d’archivage actif pendant le backup)
-PAfficher la progression
-vMode verbeux

Résultat :

55M /var/lib/postgresql/backups/base_backup

Pour un stockage plus compact ou un transfert réseau :

Fenêtre de terminal
sudo -u postgres pg_basebackup \
-D /var/lib/postgresql/backups/base_backup_tar \
-Ft -z -Xs -P -v
total 5.3M
-rw------- 1 postgres postgres 224K Apr 13 10:19 backup_manifest
-rw------- 1 postgres postgres 5.1M Apr 13 10:19 base.tar.gz
-rw------- 1 postgres postgres 17K Apr 13 10:19 pg_wal.tar.gz

L’option -z active la compression gzip. Le résultat passe de 55 Mo (plain) à 5,3 Mo (tar+gzip).

PostgreSQL inclut un outil de vérification qui compare le manifeste du backup aux fichiers réels :

Fenêtre de terminal
/usr/lib/postgresql/18/bin/pg_verifybackup /var/lib/postgresql/backups/base_backup
backup successfully verified

pg_verifybackup détecte les fichiers manquants, corrompus ou modifiés depuis le backup. Intégrez-le dans vos scripts de sauvegarde automatisée.

Les fichiers WAL (Write-Ahead Log) contiennent toutes les modifications apportées aux données. L’archivage WAL copie chaque segment WAL terminé (16 Mo par défaut) vers un emplacement séparé.

Combiné à une sauvegarde physique (pg_basebackup), l’archivage WAL permet :

  • PITR : restaurer à n’importe quel instant entre deux sauvegardes
  • RPO quasi nul : ne perdre que les transactions en cours au moment du crash
  • Rétention longue : conserver des semaines de WAL sans refaire de backup complet

Sans archivage WAL, une sauvegarde physique ne permet de restaurer qu’à l’instant exact du backup — pas entre deux backups.

  1. Créer le répertoire d’archivage :

    Fenêtre de terminal
    sudo mkdir -p /var/lib/postgresql/wal_archive
    sudo chown postgres:postgres /var/lib/postgresql/wal_archive
  2. Activer l’archivage :

    ALTER SYSTEM SET archive_mode = 'on';
    ALTER SYSTEM SET archive_command = 'test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f';

    %p est le chemin complet du segment WAL source, %f est le nom du fichier. Le test ! -f empêche d’écraser un WAL déjà archivé — c’est la recommandation de la documentation officielle pour éviter les collisions.

  3. Redémarrer (archive_mode est un paramètre postmaster) :

    Fenêtre de terminal
    sudo systemctl restart postgresql@18-main
  4. Vérifier :

    SHOW archive_mode;
    SHOW archive_command;
    archive_mode
    --------------
    on
    archive_command
    ------------------------------------------------------------------
    test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f
  5. Forcer une rotation WAL pour vérifier que l’archivage fonctionne :

    SELECT pg_switch_wal();
    Fenêtre de terminal
    ls -lh /var/lib/postgresql/wal_archive/
    total 16M
    -rw------- 1 postgres postgres 16M Apr 13 10:18 000000010000000000000002

Si le fichier WAL apparaît dans le répertoire d’archivage, tout fonctionne.

  1. Vérifier l’état de l’archivage avec pg_stat_archiver :

    SELECT archived_count, failed_count,
    last_archived_wal,
    last_failed_wal,
    last_failed_time
    FROM pg_stat_archiver;

    Si failed_count > 0, inspectez last_failed_wal et last_failed_time pour diagnostiquer le problème.

Les WAL archivés s’accumulent indéfiniment. Définissez une politique de purge :

  • RPO cible : si votre dernière sauvegarde physique date de 24h, conservez au minimum 24h de WAL
  • Règle pratique : conservez les WAL jusqu’à la deuxième sauvegarde physique la plus récente (pour pouvoir restaurer même si la dernière est corrompue)
  • Purge : un cron qui supprime les WAL antérieurs au backup_label de l’avant-dernière sauvegarde

Le PITR est la capacité de restaurer le cluster à un instant précis passé. C’est le filet de sécurité ultime contre les erreurs humaines.

  1. Base backup : une sauvegarde physique complète (le point de départ)
  2. WAL archivés : toutes les modifications entre le backup et l’instant cible
  3. recovery_target_time : l’instant exact où arrêter le rejeu des WAL

PostgreSQL rejoue les WAL depuis le backup jusqu’à l’instant cible, puis s’arrête — avant la transaction destructrice.

Règle importante : le point cible (recovery_target_time) doit être postérieur à la fin du base backup utilisé pour la restauration. On ne peut pas utiliser un backup pour revenir à un instant situé pendant ou avant ce backup.

Démonstration : simuler un DROP TABLE et restaurer

Section intitulée « Démonstration : simuler un DROP TABLE et restaurer »

Voici un scénario complet testé sur le lab. La base lab_admin contient 5 clients et 5 commandes. Un backup physique et l’archivage WAL sont déjà en place.

  1. Insérer une ligne (qui doit survivre à la restauration) :

    INSERT INTO app.clients (nom, email, ville)
    VALUES ('PITR Test', 'pitr@example.com', 'Nantes');
    SELECT count(*) FROM app.clients;
    count
    -------
    6
  2. Forcer l’archivage WAL et noter l’instant de référence :

    SELECT pg_switch_wal();
    SELECT now() AS safe_timestamp;
    safe_timestamp
    -------------------------------
    2026-04-13 10:19:49.368402+00
  3. Simuler la catastrophe (3 secondes après, pour être sûr que le timestamp est passé) :

    DROP TABLE app.commandes;
    DROP TABLE app.clients;

    Les tables ont disparu. Sans PITR, les données sont perdues.

  4. Forcer l’archivage du WAL contenant le DROP :

    SELECT pg_switch_wal();
  5. Arrêter PostgreSQL :

    Fenêtre de terminal
    sudo systemctl stop postgresql@18-main
  6. Remplacer le data directory par la sauvegarde physique :

    Fenêtre de terminal
    sudo mv /var/lib/postgresql/18/main /var/lib/postgresql/18/main_damaged
    sudo cp -a /var/lib/postgresql/backups/base_backup /var/lib/postgresql/18/main
    sudo chown -R postgres:postgres /var/lib/postgresql/18/main
  7. Configurer la restauration dans postgresql.conf :

    restore_command = 'cp /var/lib/postgresql/wal_archive/%f %p'
    recovery_target_time = '2026-04-13 10:19:49+00'
  8. Créer le fichier signal qui indique à PostgreSQL d’entrer en mode recovery :

    Fenêtre de terminal
    sudo -u postgres touch /var/lib/postgresql/18/main/recovery.signal
  9. Démarrer PostgreSQL :

    Fenêtre de terminal
    sudo systemctl start postgresql@18-main

    Le serveur entre en mode recovery et rejoue les WAL :

    LOG: completed backup recovery with redo LSN 0/4000028 and end LSN 0/4000120
    LOG: consistent recovery state reached at 0/4000120
    LOG: recovery stopping before commit of transaction 798, time 2026-04-13 10:20:03.6119+00
    LOG: pausing at the end of recovery

    Le serveur s’est arrêté avant la transaction 798 (le DROP TABLE).

  10. Reprendre l’activité — le serveur est en pause (recovery_target_action = 'pause' par défaut). Reprenez le rejeu puis laissez la recovery se terminer :

    SELECT pg_wal_replay_resume();
    LOG: archive recovery complete
  11. Vérifier les données :

    SELECT pg_is_in_recovery();
    pg_is_in_recovery
    -------------------
    f
    SELECT nom, email FROM app.clients ORDER BY id;
    nom | email
    ---------------+--------------------
    Alice Martin | alice@example.com
    Bob Dupont | bob@example.com
    Claire Durand | claire@example.com
    David Moreau | david@example.com
    Eva Bernard | eva@example.com
    PITR Test | pitr@example.com

    6 clients : les 5 d’origine + la ligne insérée avant le DROP. Les tables commandes et clients sont intactes. Le PITR a fonctionné.

Une sauvegarde non testée n’est pas une sauvegarde

Section intitulée « Une sauvegarde non testée n’est pas une sauvegarde »

La vérification doit être automatisée et régulière. Trois niveaux :

NiveauMéthodeCe que ça prouve
1. Intégritépg_verifybackup (physique)Les fichiers ne sont pas corrompus
2. Restaurationpg_restore dans une base jetable (logique)Le dump est lisible et complet
3. CohérenceRequêtes de contrôle sur la base restauréeLes données sont exploitables
#!/bin/bash
set -euo pipefail
DUMP="/var/lib/postgresql/backups/lab_admin_$(date +%Y%m%d).dump"
TEST_DB="verify_$(date +%Y%m%d)"
# Dump
sudo -u postgres pg_dump -Fc lab_admin -f "$DUMP"
# Restaurer dans une base temporaire
sudo -u postgres psql -c "CREATE DATABASE $TEST_DB;"
sudo -u postgres pg_restore -d "$TEST_DB" "$DUMP"
# Vérifier le contenu
COUNT=$(sudo -u postgres psql -tA -d "$TEST_DB" \
-c "SELECT count(*) FROM app.clients;")
if [ "$COUNT" -gt 0 ]; then
echo "OK: $COUNT clients restaurés"
else
echo "ERREUR: base vide après restauration" >&2
exit 1
fi
# Nettoyage
sudo -u postgres psql -c "DROP DATABASE $TEST_DB;"
  • pg_dump -Fc quotidien via cron
  • Test de restauration hebdomadaire automatisé
  • Rétention : 7 dumps glissants + 1 mensuel
  • Suffisant pour la plupart des applications métier
  • pg_basebackup hebdomadaire + archivage WAL continu
  • pg_dump pour les bases critiques en complément
  • Test de PITR mensuel
  • RPO : quelques minutes (WAL archivés)
  • RTO : temps de copie + rejeu WAL
  • Réplication streaming pour la haute disponibilité
  • pgBackRest ou Barman pour la gestion des sauvegardes (parallélisme, compression, chiffrement, rétention automatique, vérification)
  • Archivage WAL continu vers stockage distant (S3, NFS)
  • Test de restauration automatisé dans la CI
SymptômeCause probableSolution
pg_dump extrêmement lentTable volumineuse sans index, ou serveur sous chargeUtiliser -Fd -j N (parallèle) ou planifier en heures creuses
pg_restore échoue avec “relation already exists”La base cible contient déjà des objetsAjouter --clean (supprime avant de recréer) ou restaurer dans une base vide
pg_basebackup: could not connect to serverRôle sans attribut REPLICATION ou pg_hba.conf manquantAjouter REPLICATION au rôle et une ligne replication dans pg_hba.conf
WAL archivés s’accumulent dans pg_wal/archive_command échoue silencieusementVérifier pg_stat_archiver : last_failed_wal, last_failed_time
PITR : “could not find WAL file”Segment WAL manquant dans le répertoire d’archivageLe segment a été purgé trop tôt — revoir la politique de rétention
PITR : serveur reste en recoveryrecovery_target_action vaut pause par défautExécuter SELECT pg_wal_replay_resume(); ou configurer recovery_target_action = 'promote'
pg_verifybackup : “file not found”Fichier supprimé ou déplacé après le backupRefaire la sauvegarde — le manifeste ne correspond plus aux fichiers
  • Deux familles : pg_dump (logique, portable, sélectif) et pg_basebackup (physique, rapide, PITR possible).
  • Utilisez -Fc (custom format) pour vos dumps — c’est le format le plus flexible pour la restauration.
  • pg_dumpall --globals-only sauvegarde les rôles et tablespaces que pg_dump ignore.
  • pg_basebackup seul ne suffit pas pour le PITR — il faut l’archivage WAL continu.
  • archive_command doit retourner 0 uniquement quand l’archivage a réellement réussi — PostgreSQL retente indéfiniment sinon.
  • Depuis PostgreSQL 12, le PITR se configure dans postgresql.conf + recovery.signal (plus de recovery.conf).
  • Après un PITR, le serveur est en pause — exécutez pg_wal_replay_resume() pour terminer la recovery (ou configurez recovery_target_action = 'promote' pour automatiser).
  • Le PITR restaure les données, pas vos fichiers de configuration. Pensez à sauvegarder séparément postgresql.conf, pg_hba.conf et pg_ident.conf.
  • Testez vos restaurations régulièrement — un backup non testé est un faux sentiment de sécurité.
  • Pour les gros volumes, passez à pgBackRest ou Barman plutôt que des scripts maison.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn