Shuttle 2.0 : des sauvegardes PostgreSQL vérifiées, envoyées par SSH

Shuttle 2.0 : des sauvegardes PostgreSQL vérifiées, envoyées par SSH


Npm package
PostgreSQL Node.js TypeScript Docker Backup SSH
Last updated on

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

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é

Comparaison entre Shuttle v1 et v2 : transferts vérifiés, clé d'hôte SSH épinglée, rétention distante corrigée, rapports par e-mail

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_hosts ou une empreinte SHA256.
  • La rétention distante ne supprimait rien. En v2, keepLast s’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

Les quatre étapes de Shuttle : configurer, dumper, envoyer et vérifier, rapporter
  1. 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.
  2. Dumper. À chaque déclenchement du cron, pg_dump exporte 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.
  3. 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.
  4. 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 : keepLast en 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 custom ou plain, 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 keepLast correspond 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


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

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

Shuttle v1 versus v2: verified transfers, pinned SSH host key, fixed remote retention, email reports

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_hosts file or a SHA256 fingerprint.
  • Remote retention deleted nothing. In v2, keepLast really 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 -c option 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

Shuttle's four stages: configure, dump, ship and verify, report
  1. Configure. One YAML file describes the database, the destination server, the schedule, retention and who gets the report. It is validated before anything runs.
  2. Dump. On each cron tick, pg_dump exports the whole database or a list of tables. The dump is gzip-compressed as a stream, so memory stays flat whatever the database size.
  3. 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.
  4. 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: keepLast locally 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 custom or plain format, 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 keepLast matches what you want to keep;
  • since -c is now honoured, check that the right file is loaded.

Adding known_hosts or host_fingerprint, and an email report, is also recommended.

© 2026 Mathieu Ponton | Co-Founder & ingénieur logiciel @ Apogée Consult | Lyon, France