Shuttle 2.0 : des sauvegardes PostgreSQL vérifiées, envoyées par SSH
Brief
Shuttle est un outil open source qui sauvegarde des bases PostgreSQL selon un planning, envoie les sauvegardes sur un autre serveur par SSH, vérifie chaque transfert, supprime les anciennes sauvegardes et envoie un rapport par e-mail après chaque exécution. Il s’installe en une commande avec npm, ou tourne dans une image Docker qui embarque déjà le client PostgreSQL.
La version 2.0 est une refonte en profondeur. Elle corrige des défauts de la v1 qui pouvaient faire croire qu’une sauvegarde était bonne alors qu’elle ne l’était pas.
Sommaire
- Pourquoi Shuttle
- Ce que la v2 a changé
- Comment ça marche
- Ce que Shuttle garantit
- Configuration
- Installation
- Tests et CI
- Migrer depuis la v1
- Liens
Pourquoi Shuttle
Une sauvegarde qui reste sur la même machine que la base ne protège pas de grand-chose : si le serveur tombe, elle tombe avec lui. Il faut donc l’envoyer ailleurs, régulièrement, et surtout être sûr qu’elle est arrivée entière.
Je voulais un outil simple pour faire ça sur mes projets en production : un fichier de configuration, un démon qui tourne, et un e-mail qui me dit si tout s’est bien passé. Shuttle fait exactement ça, sans dépendre d’un service cloud.
Ce que la v2 a changé
En reprenant le code pour la v2, je me suis rendu compte que la v1 avait plusieurs problèmes sérieux. Elle fonctionnait en apparence, mais certaines garanties n’étaient pas tenues :
- Les transferts n’étaient pas vérifiés. Un envoi coupé en cours de route pouvait être enregistré comme une sauvegarde réussie. En v2, chaque fichier est envoyé sous un nom temporaire, sa taille est comparée à celle du fichier local, et il n’est renommé qu’une fois la taille correcte.
- N’importe quel serveur était accepté. La v1 ne vérifiait pas la clé SSH du serveur de destination, donc quelqu’un capable d’intercepter la connexion pouvait recevoir le dump de production. En v2, on épingle le serveur avec un fichier
known_hostsou une empreinte SHA256. - La rétention distante ne supprimait rien. En v2,
keepLasts’applique vraiment, en local comme sur le serveur. Les fichiers que Shuttle ne sait pas lire ou dater sont laissés de côté : mieux vaut garder un fichier de trop que perdre une bonne sauvegarde. - L’option
-cétait ignorée. La v1 chargeait toujours./shuttle.yml, quelle que soit l’option passée. C’est corrigé, et la configuration est maintenant validée au démarrage avec un message d’erreur lisible. - Node 18 était en fin de vie et bloquait une version vulnérable de
node-cron. La v2 demande Node 22.12 ou plus, et l’image Docker tourne avec un utilisateur non-root.
La v2 ajoute aussi un rapport par e-mail après chaque exécution, via SendGrid ou Resend : la base source, le fichier envoyé, la durée, ou l’erreur si quelque chose a échoué.
Comment ça marche
- Configurer. Un seul fichier YAML décrit la base, le serveur de destination, le planning, la rétention et les destinataires du rapport. Il est validé avant que quoi que ce soit ne tourne.
- Dumper. À chaque déclenchement du cron,
pg_dumpexporte la base, entière ou une liste de tables. Le dump est compressé en gzip au fil de l’eau, donc la mémoire utilisée reste la même quelle que soit la taille de la base. - Envoyer et vérifier. Le fichier part en SFTP sous un nom temporaire, sa taille est vérifiée, puis il est renommé. Les anciennes sauvegardes ne sont supprimées qu’après cette vérification.
- Rapporter. Un e-mail part avec le résultat. On peut choisir de le recevoir à chaque fois, seulement en cas de succès, ou seulement en cas d’échec.
Ce que Shuttle garantit
- Vérification de la clé d’hôte : le serveur de destination est épinglé.
- Transferts atomiques et vérifiés : un transfert tronqué ne devient jamais une sauvegarde.
- Planification sans chevauchement : un dump plus lent que son intervalle n’est jamais lancé deux fois, et l’arrêt attend la fin de la sauvegarde en cours.
- Rétention prudente :
keepLasten local et à distance, sans toucher aux fichiers inconnus. - Mémoire constante : une base de 200 Go demande autant de RAM qu’une base de 200 Mo.
- Dumps complets ou par tables, au format
customouplain, compressés par défaut. - Secrets hors du fichier : les valeurs comme
${DATABASE_URL}sont lues dans l’environnement.
Configuration
version: 1
shuttle:
name: my-prod-shuttle
timezone: Europe/Paris
source:
url: ${DATABASE_URL}
target:
host: backup.example.com
user: backup
key_path: ./ssh_key
base_path: /backups/myapp
known_hosts: ./known_hosts
strict_host_key: true
notifications:
email:
provider: resend # resend | sendgrid
api_key: ${RESEND_API_KEY}
from: shuttle@example.com
to:
- ops@example.com
on: always # always | success | failure
jobs:
- name: full-nightly
type: full
cron: "0 3 * * *"
format: custom
compress: true
keepLast: 7
Chaque job a son propre planning, son format, sa compression et sa rétention. On peut par exemple faire une sauvegarde complète chaque nuit, et sauvegarder quelques tables critiques toutes les six heures.
Installation
En ligne de commande, avec Node 22.12 ou plus et un pg_dump au moins aussi récent que le serveur :
npm install -g @claquettes/shuttle
shuttle init
shuttle validate -c shuttle.yml
shuttle daemon -c shuttle.yml
Ou avec Docker, dont l’image embarque déjà un client PostgreSQL 18 :
services:
shuttle:
image: claquettes/shuttle:latest
volumes:
- ./shuttle.yml:/config/shuttle.yml:ro
- ./ssh_key:/config/ssh_key:ro
command: shuttle daemon -c /config/shuttle.yml
Tests et CI
Pour un outil de sauvegarde, il fallait que les garanties soient testées, pas seulement promises. Chaque push passe par GitHub Actions :
- lint, vérification du formatage et build TypeScript en mode strict ;
- tests unitaires sur Node 22 et 24 : CLI, e-mails, sécurité SSH, planificateur ;
- tests de bout en bout sur PostgreSQL 14, 15, 16 et 17, avec un vrai serveur SFTP : dump, transfert vérifié, rétention, puis restauration ;
- un test de mémoire sur environ 200 Mo de données, pour vérifier que la compression reste à mémoire constante ;
- un test de l’image Docker.
La publication est automatisée à partir d’un tag git. Le workflow vérifie que le tag correspond à la version du package.json, relance toute la suite de tests, publie sur npm avec la provenance npm (le paquet est relié au commit et au workflow qui l’ont produit), puis publie l’image sur Docker Hub et sur GitHub Container Registry.
Le dépôt contient aussi un fichier documentation-agent.md : une référence complète (schéma de configuration, déploiement, vérifications, dépannage) écrite pour être donnée directement à un agent de code qui doit intégrer Shuttle dans un projet.
Migrer depuis la v1
Les fichiers de configuration de la v1 fonctionnent tels quels. Trois points d’attention :
- la v2 demande Node 22.12 ou plus, ce qui ne change rien si vous utilisez l’image Docker ;
- la rétention distante fonctionnant enfin, le premier passage de chaque job fera le ménage : vérifiez que
keepLastcorrespond bien à ce que vous voulez garder ; - l’option
-cétant maintenant respectée, vérifiez que c’est bien le bon fichier qui est chargé.
Il est aussi conseillé d’ajouter known_hosts ou host_fingerprint, et un rapport par e-mail.
Liens
- Site : shuttle.apogee-consult.com
- Code source : github.com/Claquettes/shuttle
- Paquet npm : @claquettes/shuttle
- Image Docker : claquettes/shuttle
English version
Brief
Shuttle is an open source tool that backs up PostgreSQL databases on a schedule, ships the backups to another server over SSH, verifies every transfer, prunes old backups and emails a report after every run. It installs with one npm command, or runs in a Docker image that already bundles the PostgreSQL client.
Version 2.0 is a deep rework. It fixes flaws in v1 that could make a backup look fine when it was not.
Table of contents
- Why Shuttle
- What changed in v2
- How it works
- What Shuttle guarantees
- Installation
- Tests and CI
- Upgrading from v1
- Links
Why Shuttle
A backup that stays on the same machine as the database does not protect you from much: if the server goes down, the backup goes with it. It has to be sent somewhere else, regularly, and you need to know it arrived intact.
I wanted a simple tool to do that for my production projects: one config file, a daemon, and an email telling me whether everything went well. Shuttle does exactly that, without depending on a cloud service.
What changed in v2
While reworking the code for v2, I realized v1 had several serious problems. It looked like it worked, but some guarantees were not actually kept:
- Transfers were not verified. An upload cut off halfway could be recorded as a successful backup. In v2, every file is uploaded under a temporary name, its size is compared with the local file, and it is only renamed once the size matches.
- Any server was accepted. v1 did not check the destination server’s SSH key, so anyone able to intercept the connection could receive the production dump. In v2, the server is pinned with a
known_hostsfile or a SHA256 fingerprint. - Remote retention deleted nothing. In v2,
keepLastreally applies, locally and on the server. Files Shuttle cannot read or date are left alone: keeping one file too many beats losing a good backup. - The
-coption was ignored. v1 always loaded./shuttle.yml, whatever was passed. That is fixed, and the config is now validated at startup with a readable error. - Node 18 was end-of-life and pinned a vulnerable
node-cron. v2 requires Node 22.12 or newer, and the Docker image runs as a non-root user.
v2 also adds an email report after every run, through SendGrid or Resend: the source database, the file shipped, the duration, or the error when something failed.
How it works
- Configure. One YAML file describes the database, the destination server, the schedule, retention and who gets the report. It is validated before anything runs.
- Dump. On each cron tick,
pg_dumpexports the whole database or a list of tables. The dump is gzip-compressed as a stream, so memory stays flat whatever the database size. - Ship and verify. The file goes over SFTP under a temporary name, its size is checked, then it is renamed. Old backups are only pruned after that check.
- Report. An email goes out with the result, either always, only on success, or only on failure.
What Shuttle guarantees
- Host key verification: the destination server is pinned.
- Atomic, verified transfers: a truncated transfer never becomes a backup.
- Overlap-safe scheduling: a dump slower than its interval is never started twice, and shutdown waits for the backup in flight.
- Defensive retention:
keepLastlocally and remotely, without touching unknown files. - Bounded memory: a 200 GB database needs the same RAM as a 200 MB one.
- Full or table dumps, in
customorplainformat, compressed by default. - Secrets out of the file: values such as
${DATABASE_URL}are read from the environment.
Installation
As a CLI, with Node 22.12+ and a pg_dump at least as recent as the server:
npm install -g @claquettes/shuttle
shuttle init
shuttle validate -c shuttle.yml
shuttle daemon -c shuttle.yml
Or with Docker, whose image already bundles a PostgreSQL 18 client:
services:
shuttle:
image: claquettes/shuttle:latest
volumes:
- ./shuttle.yml:/config/shuttle.yml:ro
- ./ssh_key:/config/ssh_key:ro
command: shuttle daemon -c /config/shuttle.yml
Tests and CI
For a backup tool, the guarantees had to be tested, not just promised. Every push goes through GitHub Actions:
- lint, formatting check and strict TypeScript build;
- unit tests on Node 22 and 24: CLI, email, SSH security, scheduler;
- end-to-end tests on PostgreSQL 14, 15, 16 and 17 against a real SFTP server: dump, verified transfer, retention, then restore;
- a memory test on about 200 MB of data, to make sure compression stays memory-bounded;
- a Docker image test.
Releases are automated from a git tag. The workflow checks that the tag matches the package.json version, runs the whole suite again, publishes to npm with npm provenance (the package is linked to the commit and workflow that built it), then pushes the image to Docker Hub and GitHub Container Registry.
The repository also ships a documentation-agent.md file: a complete reference (config schema, deployment, checks, troubleshooting) written to be handed directly to a coding agent integrating Shuttle into a project.
Upgrading from v1
v1 config files work as they are. Three things to watch:
- v2 requires Node 22.12 or newer, which changes nothing if you use the Docker image;
- since remote retention finally works, the first run of each job will clean up: check that
keepLastmatches what you want to keep; - since
-cis now honoured, check that the right file is loaded.
Adding known_hosts or host_fingerprint, and an email report, is also recommended.
Links
- Website: shuttle.apogee-consult.com
- Source code: github.com/Claquettes/shuttle
- npm package: @claquettes/shuttle
- Docker image: claquettes/shuttle