PostgreSQL géré : sauvegarde, restauration, fork

Une base PostgreSQL gérée de 1 Go, 5 millions de lignes, une mise à jour sans WHERE. Sauvegarde en 30 secondes, restauration à la minute en six minutes, fork copié en 35 secondes, et des empreintes identiques à chaque fois.

Une base PostgreSQL reliée à trois copies, une sauvegarde, une restauration à une minute précise et un fork, chacune marquée d'une empreinte vérifiée

Une base gérée promet des sauvegardes, une restauration à la minute et des copies instantanées. Cet article met ces trois promesses à l'épreuve sur une base PostgreSQL 18 de la plus petite taille (1 Go, 0,020 CAD l'heure), remplie avec 5 millions de lignes, puis vérifie chaque copie par des comptes de lignes et des sommes de contrôle. Le test a été fait le 3 novembre 2026 ; les heures sont celles de Montréal et seuls les mots de passe sont masqués.

La création de la base, la connexion par le réseau privé et le passage à la taille supérieure sont déjà décrits dans Bases de données gérées à Montréal, en démonstration. Ici, on chronomètre ce qui protège les données.

1. Le banc d'essai

Une base PostgreSQL de 1 Go commandée par l'API à 12:06:09, active à 12:12:43. La première sauvegarde complète était faite à 12:12:49 et son essai de restauration réussi à 12:13:14 : la base est protégée avant même la première écriture.

Le client tournait sur une machine à Montréal, hors du réseau privé de la base. L'accès public a donc été ouvert, limité à ses deux adresses (IPv4 et IPv6). Il est en place en une trentaine de secondes :

bash
curl -s -X PUT https://api.ffxf.net/v1/databases/34/public-access \
     -H "Authorization: Bearer $FFXF_TOKEN" -H "Content-Type: application/json" \
     -d '{"enabled": true, "sources": ["203.0.113.10", "2001:db8::10"]}'

Pour une application hébergée chez FFxF, préférez le réseau privé : rien n'est exposé.

2. TLS obligatoire, vérifié

La chaîne de connexion de GET /v1/databases/34/credentials se termine par sslmode=verify-full&sslrootcert=system. Le client vérifie le certificat et le nom du serveur :

sql
$ psql "$DB" -c "SELECT ssl, version AS tls, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
 ssl |   tls   |         cipher
-----+---------+------------------------
 t   | TLSv1.3 | TLS_AES_256_GCM_SHA384

Sans chiffrement, le serveur refuse la connexion :

bash
$ psql "postgresql://ffxfadmin:••••@dsghpaczji.db.ffxf.cloud:28772/defaultdb?sslmode=disable"
psql: error: … FATAL:  no pg_hba.conf entry for host "2602:f3a4:c0:ffee::5", user "ffxfadmin", database "defaultdb", no encryption

Un piège rencontré en chemin : l'image Docker officielle postgres:18, utilisée ici comme client, ne contient pas les autorités de certification du système. Avec sslrootcert=system, la connexion échoue sur SSL error: certificate verify failed, alors que le certificat Let's Encrypt est valide (openssl s_client répond Verify return code: 0 (ok)). Montez le magasin de l'hôte dans le conteneur, ou installez ca-certificates :

docker
docker run --rm -it -e DB="$DB" -v /etc/ssl/certs:/etc/ssl/certs:ro postgres:18 psql "$DB"

3. Un jeu de données de taille connue

pgbench, livré avec PostgreSQL, crée quatre tables bancaires. Au facteur 50 : 5 000 000 de comptes, 500 guichets et 50 agences.

bash
$ pgbench -i -s 50 "$DB"
5000000 of 5000000 tuples (100%) of pgbench_accounts done (elapsed 7.03 s, remaining 0.00 s)
done in 12.78 s (drop tables 0.00 s, create tables 0.02 s, client-side generate 7.17 s, vacuum 0.90 s, primary keys 4.70 s).

La base pèse alors 755 Mo. Une minute de transactions à 4 clients ajoute de l'historique : 40 223 transactions, 671,8 par seconde, 5,95 ms de latence moyenne, réseau compris. Les dernières sont écrites à 12:16:02.

Pour comparer les copies, une requête calcule, par table, le nombre de lignes, la somme des soldes et une empreinte : la somme des hashtext() de chaque ligne entière. Une seule valeur modifiée change l'empreinte.

sql
SELECT 'accounts' t, count(*) n, sum(abalance) s, sum(hashtext(a::text)::bigint) h FROM pgbench_accounts a
UNION ALL SELECT 'branches', count(*), sum(bbalance), sum(hashtext(b::text)::bigint) FROM pgbench_branches b
UNION ALL SELECT 'tellers',  count(*), sum(tbalance), sum(hashtext(x::text)::bigint) FROM pgbench_tellers x
UNION ALL SELECT 'history',  count(*), sum(delta),    sum(hashtext(y::text)::bigint) FROM pgbench_history y;

État de référence, à 12:16:09 :

bash
    t     |    n    |    s    |       h
----------+---------+---------+---------------
 accounts | 5000000 | -383125 | 1430067750301
 branches |      50 | -383125 |  -12480793191
 tellers  |     500 | -383125 |  -86711664516
 history  |   40223 | -383125 |   80768475562

4. Une sauvegarde à la demande

Les sauvegardes sont automatiques. Avant une opération risquée, on peut en lancer une tout de suite :

bash
curl -s -X POST https://api.ffxf.net/v1/databases/34/backups \
     -H "Authorization: Bearer $FFXF_TOKEN" -H "Idempotency-Key: $(uuidgen)"

Demandée à 12:16:15, elle démarre à 12:16:22 et se termine à 12:16:52 : 30 secondes. C'est une différentielle, prise par rapport à la complète de 12:12:40, et elle pèse 40,5 Mo pour 755 Mo de données (les tables de pgbench se compressent bien). Plus tard, la première complète d'une base de même contenu a pris 34 secondes pour 43,2 Mo.

json
{ "label": "20261103-171240F_20261103-171622D", "kind": "diff",
  "started_at": "2026-11-03T17:16:22Z", "finished_at": "2026-11-03T17:16:52Z", "size_bytes": 40461008 }

5. L'erreur, puis la restauration à la minute

À 12:18:31, une mise à jour part sans WHERE :

sql
UPDATE pgbench_accounts SET abalance = 0;
UPDATE 5000000
Time: 49896.622 ms (00:49.897)

La restauration se fait à partir de la sauvegarde complète et du journal des transactions archivé en continu. N'importe quel instant de restore_window convient. À 12:19:39, la fenêtre allait jusqu'à 12:17:51 : le journal arrive hors site avec une ou deux minutes de retard (43 secondes lors d'une autre lecture). L'instant choisi est 12:17:30, après les dernières transactions et avant l'erreur, donné en UTC :

bash
curl -s -X POST https://api.ffxf.net/v1/databases/34/restore \
     -H "Authorization: Bearer $FFXF_TOKEN" \
     -H "Idempotency-Key: $(uuidgen)" \
     -H "Content-Type: application/json" \
     -d '{"at": "2026-11-03T17:17:30Z", "name": "banque-avant-erreur"}'

L'essentiel des six minutes sert à préparer la machine. Les empreintes de la base restaurée sont identiques, chiffre pour chiffre, à l'état de référence, et la dernière ligne d'historique date de 12:16:02.576 :

bash
 accounts | 5000000 | -383125 | 1430067750301
 branches |      50 | -383125 |  -12480793191
 tellers  |     500 | -383125 |  -86711664516
 history  |   40223 | -383125 |   80768475562

La base restaurée garde l'utilisateur et le mot de passe de la source ; seul l'hôte change. La source, elle, n'est jamais écrasée : ses comptes sont toujours à zéro, prêts à être examinés.

6. Le fork : une copie en 35 secondes

Un fork copie la base telle qu'elle est maintenant, ici avec l'erreur :

bash
curl -s -X POST https://api.ffxf.net/v1/databases/34/forks \
     -H "Authorization: Bearer $FFXF_TOKEN" \
     -H "Idempotency-Key: $(uuidgen)" \
     -H "Content-Type: application/json" \
     -d '{"name": "banque-preprod"}'

Demandé à 12:20:04, le fork était lié à ses données à 12:20:39, 35 secondes plus tard pour 755 Mo. Il est devenu actif à 12:29:30 : 9 min 26 s, demandé 19 secondes après la restauration et préparé en même temps qu'elle. Ses empreintes sont celles de la source après l'erreur (accounts | 5000000 | 0 | 1464243819646, les trois autres tables inchangées).

Une écriture sur le fork ne touche pas la source :

sql
-- sur le fork
UPDATE pgbench_accounts SET abalance = 1 WHERE aid <= 1000;
UPDATE 1000
-- sur la source
SELECT count(*) FILTER (WHERE abalance <> 0), sum(abalance) FROM pgbench_accounts;
0|0

Tant qu'il est lié, le fork n'a pas de sauvegardes (GET …/backups renvoie une liste vide) et la source ne peut pas être supprimée :

json
{"error":{"code":"action_conflict","message":"Forks of this database still depend on it: make them independent or delete them first."}}

POST /v1/databases/36/detach lui donne sa propre copie. Une sonde interrogeait le fork chaque seconde : dernière réponse à 12:34:08, connexions refusées ensuite, première réponse revenue à 12:34:35. Environ 27 secondes de coupure ; les 1 000 lignes modifiées sont toujours là. D'après la documentation, le fork a dès lors ses propres sauvegardes.

7. pgvector et PostGIS

Les extensions de la liste de GET /v1/databases/options s'activent par l'API, base logique par base logique. La réponse est immédiate :

bash
curl -s -X POST https://api.ffxf.net/v1/databases/35/databases/defaultdb/extensions \
     -H "Authorization: Bearer $FFXF_TOKEN" -H "Content-Type: application/json" \
     -d '{"extension": "vector"}'
{"data":{"name":"defaultdb","extensions":["vector"]}}

Avec postgis ajoutée de la même façon, la base contenait vector 0.8.7, postgis 3.7.0 et pg_stat_statements 1.12, présente d'office. Un index HNSW et un calcul de distance fonctionnent tels quels :

sql
CREATE TABLE docs (id int PRIMARY KEY, embedding vector(3));
INSERT INTO docs VALUES (1, '[1,0,0]'), (2, '[0.9,0.1,0]'), (3, '[0,0,1]');
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
SELECT id, round((embedding <=> '[1,0,0]')::numeric, 4) AS distance
  FROM docs ORDER BY embedding <=> '[1,0,0]' LIMIT 2;
 id | distance
----+----------
  1 |   0.0000
  2 |   0.0061

SELECT round(ST_Distance('SRID=4326;POINT(-73.5674 45.5019)'::geography,
                         'SRID=4326;POINT(-71.2080 46.8139)'::geography)::numeric / 1000, 1) AS montreal_quebec_km;
 montreal_quebec_km
--------------------
              233.4

L'administrateur ffxfadmin n'est pas superutilisateur. Une extension dite de confiance comme hstore se crée aussi en SQL ; dblink et file_fdw sont refusées (Must be superuser to create this extension).

8. Les limites, telles que mesurées

  • Rétention de 7 jours. Au-delà, plus de restauration possible. Pour garder une copie plus longtemps, faites votre propre export : un pg_dump -Fc de cette base de 755 Mo a pris 7,7 secondes et produit un fichier de 15 Mo.
  • Pas de téléchargement des sauvegardes. L'API les liste et les restaure dans une nouvelle base ; elle ne les livre pas en fichier.
  • Une restauration rend toute la base. Pour récupérer une seule table, restaurez, puis copiez la table avec pg_dump -t, et supprimez la base restaurée.
  • Les dernières minutes. La fenêtre de restauration suit le présent avec une à deux minutes de retard.
  • L'historique ne suit pas. La base restaurée commence sa propre fenêtre à sa première sauvegarde (12:26:23) ; celle de la source reste accessible tant que la source existe, puis 7 jours après sa suppression.
  • Chaque copie est une base facturée. Restauration et fork coûtent le tarif de leur taille, à l'heure, jusqu'à leur suppression.
  • 1 Go, c'est petit. 25 connexions au plus et shared_buffers de 241 Mo : prévoyez un pool de connexions côté application.

9. Bilan

Les trois bases ont vécu entre 15 et 29 minutes. À l'heure, le test entier coûte environ 0,06 CAD. Les appels utilisés sont tous décrits dans la documentation des bases de données.

Une base de données reliée par un lien chiffré à une machine du réseau privé, ses sauvegardes hors site et une flèche de retour dans le temps

Bases de données gérées à Montréal, en démonstration

Les bases gérées sont ouvertes à tous. Une base PostgreSQL suivie de bout en bout : créée en cinq minutes, jointe en TLS vérifié depuis le réseau privé, une suppression rattrapée à la minute près, un fork et un passage à 2 Go, avec les sorties réelles.

Lire l'article 8 min de lecture

Une machine, ses points de restauration empilés sur un stockage séparé, et une troisième copie qui part ailleurs

Sauvegarder un VPS : restaurer, puis copier hors site

Ce que FFxF sauvegarde pour vous, ce que la restauration remet en place et ce qu'elle efface, puis une copie hors site avec restic testée sur deux VM : un dossier supprimé, restauré, et chaque fichier vérifié.

Lire l'article 7 min de lecture

Support & discussions

Questions techniques, retours d'incidents ou discussions d'infrastructure, l'équipe est présente sur Discord, Telegram, X, Instagram, Reddit et IRC.