English · Italiano
NDM Linux Backup is a system backup and disaster recovery tool for Debian/GNU Linux
servers and workstations. It saves the parts of a machine you need to rebuild or diagnose
it, the ones most backup tools leave out: hardware inventory, boot setup (UEFI/BIOS/MBR,
GRUB), partition tables, LUKS and LVM layout, installed packages, modified /etc files,
crontabs, MySQL/MariaDB and PostgreSQL schemas, Docker and Podman configuration. GPG
encryption and retention of old archives are built in.
It is one Bash script with no extra libraries: it relies on standard Debian tools that
apt can install, and runs as root. Every setting lives in the script itself, optionally
overridden by a settings file of simple KEY=value lines that is only read, never
executed (see External settings file).
Each piece of the backup can be switched on or off individually (see Configuration), so you can trim the script down to only what your machine actually needs.
The backup itself is split into up to three archives: settings (hardware, configs,
package lists, database schema, container metadata/config... everything you need to
understand or rebuild the setup) is always produced; data (actual database rows,
real Docker/Podman volume content, your own documents) and secrets (SSH host keys,
TLS private keys, /etc/shadow, network credentials, root's own credentials...) are
separate, opt-in archives (see Data backup and Secrets
backup). The settings archive never contains secrets-archive material,
regardless of whether the secrets archive itself is enabled.
Most backup tools save your files. When a disk dies or a machine has to be rebuilt, what
is often missing is everything around them: which disks and partitions there were, how
LUKS and LVM were laid out, how the machine booted, which packages were installed by
hand, which /etc files were changed, the database schemas, the container setup. NDM
Linux Backup collects exactly that, into archives you can read with standard tools.
It does not replace file-level or disk-image tools such as Back In Time, Timeshift or Clonezilla: it complements them, saving the parts of the system they leave out.
- What it backs up
- Safety
- Data backup
- Secrets backup
- Encryption
- Requirements
- Installation
- Usage
- Configuration
- Notifications
- Cron
- Analyze
- Restore
- Sample output
- External services contacted
- Exit codes
- Project layout
- Versioning
- License and disclaimer
System and hardware:
- Hardware info
- Hardware devices list
- Full system report (
inxi) - GNU/Linux release
- Linux Kernel release
Boot and disks:
- Block device list (
lsblk) - Block ID of installed devices (root is required)
- UEFI (root is required)
- BIOS MBR (root is required)
- LUKS configuration (root is required) -- reference-only text dump (UUID, cipher, keyslot layout; no key material), not a restorable header. A separate, more sensitive raw header backup that can restore a corrupted LUKS header byte-for-byte -- and that also defeats crypto-shredding for anyone who gets hold of it -- lives in the opt-in secrets archive, off by default even there; see Secrets backup
- LVM configuration (root is required)
- Partitions info (root is required)
- Grub configuration
Software and data:
- Installed packages
- Flatpak app list
- AppImage files found on the system (size + path list, not the files themselves; see Configuration to add search paths)
- Manually modified system files: any file whose content no longer matches what its
package installed (
dpkg -V), copied as-is; only catches files already tracked by a package, not brand new unpackaged files - Unpackaged
/etcfiles: the complement of the above, files under/etcowned by no installed package at all (sodpkg -Vnever sees them either) -- e.g./etc/postgresql/*/main/postgresql.conf, generated by a package's post-install script rather than shipped in it. A default ignore list filters out auto-generated state, tool/editor backup copies, and compiled caches that would otherwise be noise on every run (not files an admin plausibly edited by hand --/etc/motd,/etc/hosts,/etc/hostname,/etc/machine-id,/etc/network/interfacesand the like are deliberately left in, unlike earlier versions of this list), as well as private key material (SSH host keys, the ssl-cert snakeoil key,/etc/mysql/debian.cnf...) -- that now lives in the opt-in secrets archive instead, see Secrets backup./etc/fstab//etc/crypttabspecifically have their own dedicated copy elsewhere (partitions/fstab.txt,luks/crypttab.txt) instead of showing up here -- except crypttab, which still surfaces here too whenever the LUKS module didn't run, so it is never silently missing from the whole archive - Crontab (root is required)
- MySQL/MariaDB schema (structure only: tables, views, triggers, routines; no row data. Root is required)
- PostgreSQL schema (same as above)
- Docker images/containers/volumes/networks info (name/id lists only; images and containers themselves are never backed up, they are meant to be recreated from the Compose/Quadlet files above plus their registry)
- Compose files found on the system: Docker Compose / podman-compose (searches common locations, see Configuration to add search paths; copied as-is, not just listed)
- Podman images/containers/volumes/networks info (name/id lists only, same reasoning as
Docker above; reads the rootless state of
v_podman_user, see Configuration) plus its config and Quadlet (systemd) unit files, both system-wide (/etc/containers) and forv_podman_user(~/.config/containers) - More directories chosen by the user (root could be required)
All of this is compressed into a single tar.xz settings archive in a chosen directory.
Optionally, the archive directory can also be synchronized to a local or remote
destination using rsync, and you can run your own commands before and after the backup.
See Data backup for the separate, opt-in data archive.
- Writes are confined. The script only ever writes inside the backup destination and
its own working directory. Every other path on the system (disks,
/boot,/etc, LUKS headers, MySQL...) is only ever read. Everytar/chmod/chown/sha256sum/gpg/rmcall is additionally checked at runtime against that same whitelist right before it runs, hard-exiting instead of proceeding if a path ever resolved outside it. - Dry run.
--dry-runwalks through every step for real (reading disks, LUKS headers, MySQL databases, etc., with root privileges) without creating, modifying, or deleting anything, so you can validate your configuration safely before a real run. - Overlapping source paths. If
v_tempdir/v_tempdir_datais moved (viandm_linux-backup.conf) to somewhere under ana_source_extra/a_data_source_extraentry, the script warns (without blocking the run) that its content will end up duplicated in the resulting archive -- both end up as separatetarsource arguments on the same command line. Move the tempdir back outside every source-extra path to silence it; see CONFIGURE'sv_tempdircomment for why the default never overlaps. Av_tempdir/_data/_secretsmoved to the same place as, or under, its ownv_dest/_data/_secretsis a harder case -- the archive would come out empty -- so this one hard-exits instead of just warning. - Dependency check. At every run, the script lists every external tool it can use,
reports what's missing, and lets you choose what to do about it: install only what's
needed by the backup pieces you've enabled (default) or everything the script could
ever use -- either way, the exact
apt-get installcommand is printed before it runs -- install it yourself instead (prints the exact command for Debian, then exits so you can run it and re-launch NDM Linux Backup once done), or skip and continue with those steps failing/skipped. Skipped entirely in--quiet/--dry-run. - Database credentials check. If MySQL/MariaDB or PostgreSQL backup is enabled and
its client tool is present, the script checks that its credentials file
(
.my.cnf/.pgpass) exists before doing any work; if not, it first tries a local socket connection (root@localhostfor MySQL/MariaDB, peer/ident for PostgreSQL) and only prints what's missing and how to create it, then exits, if that also fails -- it never creates, copies, or edits the file itself (see MySQL/MariaDB backup and PostgreSQL backup). If the file exists but is readable by group/other, the script prints a warning (it contains a plaintext password) without blocking the run --chmod 600it to silence the warning. - Run log. Every run (except
--dry-run) writes a plain-text, world-readable (chmod 644).logfile into the backup destination alongside the settings archive -- the full step-by-step record of what happened, independent of--quiet(which only silences the terminal, not the log), so unattended/cron runs still leave a complete trail to check afterward. - Checksum. Every archive (settings, data, secrets) gets a
<filename>.tar.xz.sha256sidecar written right next to it, in standardsha256sumformat. Verify withsha256sum -c <filename>.tar.xz.sha256from inside the destination directory -- worth doing before trusting an old backup for a real restore, since atar.xzthat got silently corrupted (disk issue, an interrupted copy elsewhere) still looks like a normal file until you actually try to read it. A checksum failure is logged as a warning and does not fail the run: the archive itself already succeeded by that point. - Retention. Off by default (
v_retention_days=0): old archives are kept forever unless you opt in. When enabled, it deletes archives (settings/data/secrets counted separately) older thanv_retention_days, always keeping at leastv_retention_min_keepof each type regardless of age -- so a machine that sits unused past the retention window never wakes up to zero backups on the next run. Runs right after a new archive of that type is successfully created, before the rsync step (so a mirroredv_dest_rsyncreflects the pruned state too), and only ever matches this script's own exact filename pattern. - Encryption. Off by default (
v_enable_gpg_settings/_data/_secrets=false).chmod 600protects an archive only as long as it stays on this machine's filesystem; if it's ever copied somewhere less protected (a USB drive, cloud storage, an unencrypted external disk), that protection is gone. When a switch is on, its archive is GPG-encrypted right after it's created (usingv_gpg_recipient_file/_data/_secrets, default${HOME}/pubkey.asc) and the plaintext is deleted -- the private key is never needed on the backup machine, only later, on whatever machine you actually restore from (see Restore). A switch enabled with no recipient key at that path exits the run before any backup work starts (see Encryption); a failed encryption hard-exits the run too, instead of silently leaving a plaintext archive behind. - Settings file is never executed.
ndm_linux-backup.confis read line by line asKEY=value: only the settings of the CONFIGURE section are accepted, every value is checked against its type (true/false, number, absolute path...), and nothing in it is expanded or run --$HOME,$(...)and backticks are refused. An unknown setting or an invalid value stops the script with exit code 2 and names the line. The environment is not read either. The only values that end up being run are the hook commands (v_pre_command,v_post_command,v_on_failure_command), each only once its ownv_enable_*switch is on. - Script and settings file trust.
ndm_linux-backup.shruns as root the moment yousudoit, and the hook commands inndm_linux-backup.confrun as root too, with no ownership/permission check on either file. Neither needs to be owned byroot: they work fine owned by your normal user, like any other script yousudo. If several people share this machine and you want to be sure only you (or root) can edit them before the next run, lock both down the same way:Entirely optional, and your call to make -- NDM Linux Backup itself never requires or checks for this.sudo chown root:root ndm_linux-backup.sh ndm_linux-backup.conf sudo chmod 755 ndm_linux-backup.sh sudo chmod 644 ndm_linux-backup.conf - Backup destination folder.
v_dest(and_data/_secrets) is created at whatever mode your umask produces (typically755 root:root-- listable, not writable, by other users), and every archive inside it is already individually locked down (chmod 600, see Encryption above) regardless. If you also want the folder itself unlistable to other users on a shared machine:Optional, same as above -- the individual archive permissions are the real protection either way.sudo chmod 700 /path/to/backup
Warning
The settings archive still isn't a secret-free zone, just secret-material-free.
/etc and /root are swept wholesale by default (a_source_extra), and system
config can still legitimately contain sensitive details even after the known high-risk
material (private keys, /etc/shadow, network/cloud credentials -- see Secrets
backup) is excluded from it. The .my.cnf/.pgpass files are
likewise always excluded from the settings and data archives even if they end up under
an a_source_extra/a_data_source_extra path (they authenticate the script to an
external database, not machine configuration). Protect the resulting .tar.xz files
(file permissions, where you store or rsync them) accordingly; the secrets archive,
when enabled, needs that protection even more.
Off by default (v_enable_data_backup=false). When enabled, a second tar.xz archive
is produced alongside the settings one, with actual data. Deliberately not included here
either: Docker/Podman images and containers themselves, since they are meant to be
recreated from what the settings archive already has (Compose/Quadlet files, image
names/tags) plus the registry, not restored byte-for-byte. What the data archive does
contain:
- MySQL/MariaDB and PostgreSQL row data (their schema is always in the settings archive regardless of this switch, see What it backs up)
- Real Docker/Podman volume content (
v_enable_docker_volumes_data/v_enable_podman_volumes_data), read via each volume's Mountpoint; only works for the default "local" driver. Copying a volume actively written by a running container can produce an inconsistent copy, same caveat as copying a live database data directory directly, so for database volumes prefer the MySQL/PostgreSQL data dumps instead. - Any directories in
a_data_source_extra(e.g. personal documents, media)
This pass has its own working directory and destination (v_tempdir_data /
v_dest_data, default next to the settings ones) and its own archive filename
(..._DATA_...), so it can be routed, retained, or scheduled independently of the
settings archive. --dry-run/--quiet/--keep apply to it exactly like the settings
pass, no separate flags needed.
Off by default (v_enable_secrets_backup=false). Enable it permanently in CONFIGURE, or
pass --secrets on the command line to produce it for a single run without touching
CONFIGURE at all (useful for an occasional "grab the secrets too this time" without
changing what every future run does by default; it only ever turns the pass on for
that run, never off if CONFIGURE already enabled it). Either way, a third tar.xz
archive is produced, containing sensitive credential/key material that the settings and
data archives never include, regardless of this switch: SSH host private keys, TLS
private keys (/etc/ssl/private, Let's Encrypt), /etc/shadow/ /etc/gshadow (plus
their - backup copies), NetworkManager/wpa_supplicant/WireGuard network credentials,
mail relay and PPP secrets, root's own SSH/cloud-CLI/Docker-registry credentials and
bash history, and this script's own .my.cnf/.pgpass (unlike everywhere else, the
secrets archive is where those belong, since it exists specifically to hold restore-time
credentials). The exact list is a_secrets_source in CONFIGURE, freely overridable; a
missing path is skipped with a notice, not an error, since not every machine has all of
these.
This pass has its own working directory and destination (v_tempdir_secrets /
v_dest_secrets, default next to the settings ones) and its own archive filename
(..._SECRETS_...), so it can be routed, retained, or protected independently of the
settings/data archives -- e.g. a tighter destination permission, no rsync to a shared
location, a shorter retention. --dry-run/--quiet/--keep apply to it exactly like
the settings pass, no separate flags needed.
Warning
Protect this archive at least as well as you would protect /root and /etc/shadow
themselves: whoever has it can impersonate this machine, decrypt its TLS traffic, and
use whatever cloud/registry credentials root had configured.
Raw LUKS header backup (v_enable_luks_header_backup, off by default even when the
rest of this pass is on): a real cryptsetup luksHeaderBackup image per a_luks entry,
landing in luks_headers/ inside this same archive. This is different from -- and more
sensitive than -- the human-readable luksDump/luksUUID text already in the settings
archive: it contains the actual keyslots (the master key encrypted with each
passphrase's derived key), so it can restore a corrupted LUKS header byte-for-byte.
Caution
This also defeats crypto-shredding -- destroying only the on-disk header to make a volume permanently unrecoverable, a technique some admins rely on when decommissioning a disk -- for anyone who also gets hold of it. Turn it on only if you specifically want the header-recovery guarantee above and accept this trade-off; the script prints and logs an explicit warning whenever it's active, separate from the general secrets-backup notice.
Off by default for all three archives (v_enable_gpg_settings/_data/_secrets, see
Configuration and Safety) -- v_enable_gpg_data/_secrets
mirror v_enable_gpg_settings unless overridden independently, same pattern as
v_dest_secrets/v_tempdir_secrets: turn on just v_enable_gpg_settings to encrypt
all three alike, or leave it off and turn on only v_enable_gpg_secrets to encrypt just
the secrets archive. Each archive's actual recipient key is
v_gpg_recipient_file/_data/_secrets, defaulting to ${HOME}/pubkey.asc (root's,
same place .my.cnf/.pgpass already live) even while encryption itself is off -- the
path being set is harmless on its own, only the matching v_enable_gpg_* switch turns
encryption on.
One-time setup, before turning a switch on:
- Generate a GPG keypair. Doesn't need to happen on the backup machine -- in fact it's
better if it doesn't, see step 4:
gpg --full-generate-key - Export just the public key to a plain file:
gpg --armor --export <keyid-or-email> > pubkey.asc - Copy
pubkey.ascto the backup machine, at the pathv_gpg_recipient_file/_data/_secretsalready points to (${HOME}/pubkey.ascunless you changed it) -- or already have a public key from whoever will restore this backup? Copy that one there instead, no need to generate your own. Different keys per archive for tiered access, e.g. a wider-access key for settings and a restricted one for secrets? Point each variable at its own file instead of sharing the default -- see Configuration for why settings is not necessarily "secret-free" either before deciding to leave it plain. - Keep the private key off the backup machine entirely. It is never needed there --
only
pubkey.ascis, to encrypt. The private key is needed later, only on whatever machine you actually restore to (see Restore); store it like you would any other master credential -- a password manager, an offline USB drive, a hardware token -- not on the machine being backed up. - Turn on the switch(es) for the archives you want encrypted:
v_enable_gpg_settings/_data/_secrets=true.
If a switch is on but its recipient key is missing at the moment NDM Linux Backup runs, it exits before doing any work and prints exactly what's missing and how to fix it (generate a keypair, export the public key, or copy an existing one into place) -- it never creates or edits key material itself, same "check and report, never automate the fix" approach as the MySQL/PostgreSQL credentials check above. Whether each archive ends up encrypted or plain is also stated at the start of every run, right next to the secrets-backup notice.
From the next run onward, every archive whose switch is on is encrypted automatically and its plaintext deleted; nothing else changes about how the script runs.
- Debian GNU/Linux: tested on Debian 13 (Trixie). Derivatives may work but are not tested.
- Bash and root privileges (
sudoorsu): the script refuses to run otherwise. - The tools of the backup pieces you enable. At every run the script lists what is
missing and offers to install it with
apt-get(see Safety, "Dependency check"). A tool needed by an enabled piece must be there, otherwise the run stops before doing any work; the ones marked optional only skip their own piece when missing.
| Debian package | Commands | Needed for | |
|---|---|---|---|
tar, coreutils, fdisk |
tar, sha256sum, fdisk |
every run | required |
lshw |
lshw |
hardware info | if enabled |
pciutils, usbutils |
lspci, lsusb |
hardware devices list | if enabled |
inxi |
inxi |
full system report | if enabled |
util-linux |
lsblk, blkid |
block devices | if enabled |
coreutils |
dd, uname |
MBR, kernel release | if enabled |
lsb-release |
lsb_release |
GNU/Linux release | if enabled |
fdisk |
sfdisk |
partition tables | if enabled |
dpkg, apt |
dpkg, apt-mark |
installed packages, modified system files | if enabled |
cryptsetup |
cryptsetup |
LUKS, raw LUKS header | if enabled |
cron |
crontab |
crontab | if enabled |
rsync |
rsync |
copy to a second destination | if enabled |
gnupg |
gpg |
Encryption | if enabled |
efibootmgr |
efibootmgr |
UEFI boot entries | optional |
lvm2 |
lvmdump, pvdisplay, vgdisplay, lvdisplay |
LVM | optional |
default-mysql-client |
mysql, mysqldump |
MySQL/MariaDB | optional |
postgresql-client |
psql, pg_dump, pg_dumpall |
PostgreSQL | optional |
docker.io, podman, flatpak |
docker, podman, flatpak |
containers, Flatpak apps | optional |
sudo |
sudo |
Podman of a user other than root (v_podman_user) |
optional |
./ndm_linux-backup.sh --requirements (no root needed) lists the tools your settings
need, which are missing and the apt-get command to install them.
sudo ./ndm_linux-backup.sh --analyze shows, for your settings and this machine, which
pieces would run and which are blocked (see Analyze).
sudo apt install git
mkdir -p ~/bin && cd ~/bin
git clone https://github.com/nidamanx/NDM_Linux-Backup.git
cd NDM_Linux-Backup
chmod +x ndm_linux-backup.sh
sudo ./ndm_linux-backup.sh --analyze # what your current settings would do; nothing is writtenOptional: make it available everywhere through a symbolic link. The script still finds
ndm_linux-backup.conf and its backup/ folder next to its real location.
sudo ln -s "$PWD/ndm_linux-backup.sh" /usr/local/bin/ndm_linux-backupThe examples in this README assume the clone is in ~/bin/NDM_Linux-Backup.
Downloading only
ndm_linux-backup.sh
is enough for everything, including per-machine settings (see
Configuration: no extra file is needed for that). The only exception
is v_enable_mysql/v_enable_postgresql, which need my.cnf.example/pgpass.example
respectively: clone the full project instead if you plan to use either.
cd ~/bin/NDM_Linux-Backup
git pull --ff-only
./ndm_linux-backup.sh --versionUpdating never touches your ndm_linux-backup.conf, your credential files or the
archives in backup/: they are not part of the repository. Keep your settings in
ndm_linux-backup.conf rather than editing the CONFIGURE section of the script, so that
git pull never conflicts with them.
If you downloaded only the script, download the new one from the latest release and replace the old file.
Upgrading from 2.x (published as NDM Backup on GitLab):
- rename
ndm_backup.conftondm_linux-backup.conf; - in it, write full paths instead of
${HOME}and remove anything that is not aKEY=valuesetting: the file is now only read, never executed (see External settings file).--requirementsreports the first line to fix, if any; - if you set
v_datetime_suffix, usev_datetime_formatinstead (adateformat); archives are now namedndm_linux-backup_<host>_...and staged under/home/ndm_linux-backup/: setv_backup_name=NDM_BKto keep the old names (and the retention of the old archives), delete the old/home/ndm_backup/, and useNDM_ARCHIVE_PREFIXinstead ofNDM_BACKUP_NAMEin your hook commands; - update the path of the script in your symbolic link and crontab.
See CHANGELOG.md for every change.
./ndm_linux-backup.sh --requirements # the tools your settings need (no root)
sudo ./ndm_linux-backup.sh --analyze # what would be backed up; nothing is written
sudo ./ndm_linux-backup.sh --start --dry-run # simulate a full run; nothing is written
sudo ./ndm_linux-backup.sh --start # back up
sudo ./ndm_linux-backup.sh --start --secrets # also the secrets archive, for this run onlyThis script needs root privileges to back up most parts of your GNU/Linux machine:
sudo ndm_linux-backup --start or su -c 'ndm_linux-backup --start'
Before a real run, you can test your configuration safely with
sudo ndm_linux-backup --start --dry-run (see Safety), or get an instant
report of what your current settings would actually do with
sudo ndm_linux-backup --analyze (see Analyze).
| Flag | Long form | Meaning |
|---|---|---|
-s |
--start |
start the backup |
-a |
--analyze |
report what would be backed up, skipped, or blocked, then exit -- see Analyze |
-n |
--dry-run |
simulate every step; nothing is written anywhere |
-q |
--quiet |
don't ask questions and don't show any progress |
-k |
--keep |
don't delete the temporary directory after a quiet run |
--secrets |
also produce the secrets archive for this run, even if v_enable_secrets_backup=false in CONFIGURE (does not disable it if already true; see Secrets backup) |
|
-h |
--help |
show the full help |
--requirements |
show the tools your settings need, which are missing and how to install them (no root needed) | |
-u |
--usage |
show a short usage message |
-v |
--version |
show the software version |
Short and long forms are interchangeable and can be combined, e.g.
ndm_linux-backup -s -q -k or ndm_linux-backup -sqk.
Full help: ndm_linux-backup -h or ndm_linux-backup --help.
All parameters live in the CONFIGURE section at the top of ndm_linux-backup.sh. You
can edit them in place, or override any of them from ndm_linux-backup.conf (see
External settings file) so future script updates don't
clobber your per-machine settings. The most relevant ones:
v_dest/v_tempdir/v_dest_rsync: where the archive is written, the temporary working directory used while collecting data, and an optional rsync destination; setv_enable_rsync=falseto skip the rsync step without clearingv_dest_rsync. The sync copiesv_destitself (no trailing slash) intov_dest_rsync, so archives end up nested one level down, e.g.v_dest_rsync/backup/*.tar.xzunder the stock CONFIGURE, not flattened intov_dest_rsync/*.tar.xz-- standard rsync behavior, not a bug, but easy to assume the opposite.v_tempdirdefaults to/home/ndm_linux-backup/${v_backup_name}instead of/tmp: everything the archive will contain sits there unprotected until the finaltar.xzexists, so it stays off a predictable, world-writable shared path./homeover/root: both are root-owned/non-shared, but/rootcommonly sits on the (often small) root filesystem, while/homeis more often its own, larger partition -- relevant since this directory can hold a full uncompressed data-pass staging tree before it's even compressed. It can't default under/root,/etc, or the script's own directory either way: all three are already swept into the settings archive bya_source_extra, andv_tempdiris a directtarsource itself, not just an excluded path likev_dest-- nesting it inside anythinga_source_extrasweeps either duplicates its content in the archive or, if excluded to avoid that, drops it from its own direct inclusion too.v_tempdir_data/v_tempdir_secretsare siblings of it (${v_backup_name}_data/${v_backup_name}_secrets), so all three -- and only these -- live under one shared/home/ndm_linux-backupcontainer instead of scattering separate top-level directories. That container is validated and, the first time it's needed, created at700right along with each leaf (f_prepare_tempdirinmain()), so it never ends up at whatever looser permissions its first creator happened to leave it at:/home/ndm_linux-backup/ 700 root:root (container, created once, checked every run) ├── ndm_linux-backup/ 700 root:root (v_tempdir -- settings staging) │ ├── crontab.txt 600 root:root │ └── ... ├── ndm_linux-backup_data/ 700 root:root (v_tempdir_data -- only if the data pass is on) │ └── ... └── ndm_linux-backup_secrets/ 700 root:root (v_tempdir_secrets -- only if the secrets pass is on) └── files/ └── etc/shadow 600 root:roota_storage/a_luks: which disks (e.g.sda,nvme0n1) and LUKS partitions to inspect. Set either to( "auto" )to auto-detect every physical disk / LUKS partition on the system instead of listing them by hand.v_enable_*: one on/off switch per backup piece (hardware, EFI, MBR, LUKS, LVM, partitions, installed packages, flatpak, AppImages, modified system files, unpackaged/etcfiles, cron, MySQL, PostgreSQL, Docker, Compose files, Podman...). Set any tofalseto skip that piece entirely, without touching the rest of the script.a_source_extra: extra directories to include in the archive as-is (default:/root,/etc,/usr/local/bin,/opt,/srv,/usr/local/etc,/var/spool/cron/crontabs, and the script's own directory -- captures how this NDM Linux Backup deployment itself is set up;.gitand the MySQL/PostgreSQL credential files are excluded from the archive even if they fall under one of these paths, see Safety; missing paths are skipped with a warning instead of failing the whole archive); setv_enable_source_extra=falseto skip it without clearing the array.a_appimages_extra_paths: extra directories to search for AppImages, in addition to the built-in defaults (/home,/root,/opt,/usr/local/bin).a_docker_compose_extra_paths: extra directories to search for Compose files (Docker Compose / podman-compose), in addition to the built-in defaults (/home,/root,/opt,/srv).v_mysql_cnf_path: path to the.my.cnfcredentials file for MySQL/MariaDB dumps, default${HOME}/.my.cnf. If missing, NDM Linux Backup first triesroot@localhostsocket auth before asking for the file -- no separate switch, it's just a fallback (see MySQL/MariaDB backup).v_postgresql_user/v_postgresql_pgpass_path: user to connect as and path to the.pgpasscredentials file for PostgreSQL dumps, default${HOME}/.pgpass(see PostgreSQL backup). If.pgpassis missing, NDM Linux Backup first tries local peer/ident auth over the Unix socket before asking for the file -- no separate switch, it's just a fallback (see PostgreSQL backup).v_podman_user: which user's rootless Podman instance to inspect (defaults to the same auto-detected user asv_dest). Podman is normally rootless and per-user, so reading it as root only shows root's own state. For a non-root user, runloginctl enable-linger <user>once (as root) so their runtime directory exists even without an active login session.v_pre_command/v_post_command: your own commands to run before/after the backup (e.g. stop/start services, send a notification); off by default even once set -- also setv_enable_pre_command/v_enable_post_commandtotrueto actually run it. Both get${NDM_HOSTNAME}/${NDM_ARCHIVE_PREFIX}/${NDM_LOG_PATH}/${NDM_USER}(the human user behindsudo, same asv_username) exported for them to use;v_post_commandadditionally gets${NDM_EXIT_CODE}(always0here, seev_on_failure_commandbelow for failures) and, for each archive actually produced this run, its full path and human-readable size:${NDM_SETTINGS_ARCHIVE}/${NDM_SETTINGS_ARCHIVE_SIZE},${NDM_DATA_ARCHIVE}/${NDM_DATA_ARCHIVE_SIZE},${NDM_SECRETS_ARCHIVE}/${NDM_SECRETS_ARCHIVE_SIZE}-- empty for whichever pass did not run this session. See Notifications for a ready-to-usentfyexample covering both.v_on_failure_command: your own command to run if the run fails (send an email, hit a webhook...) -- the actual pain point for unattended/cron use, where a failed run otherwise only shows up in the log/exit code that nobody is watching live. Fires from a singleEXITtrap covering every hard-exit insidemain()(missing dependency, missing DB credentials, a failed tar/GPG step...) plus Ctrl-C/SIGTERM, but not failures beforemain()starts (a malformedndm_linux-backup.conf, an invalid CLI flag) -- those are setup-time problems you'd notice on the very first run.${NDM_EXIT_CODE}/${NDM_LOG_PATH}/${NDM_ARCHIVE_PREFIX}/${NDM_HOSTNAME}/${NDM_USER}are exported for it to use; off by default even once set -- also setv_enable_on_failure_command=trueto actually run it.v_enable_data_backup/v_dest_data/v_tempdir_data: enable the separate data archive and where it's written (see Data backup).v_enable_docker_volumes_data/v_enable_podman_volumes_data: back up the actual content of Docker/Podman volumes, not just the metadata list.a_data_source_extra: extra directories with real data to include in the data archive; off by default even once set -- also setv_enable_data_source_extra=trueto include it (also requiresv_enable_data_backup=true, like the other data-pass switches).v_enable_secrets_backup/v_dest_secrets/v_tempdir_secrets: enable the separate secrets archive and where it's written (see Secrets backup).a_secrets_source/a_secrets_source_extra: which paths are swept into the secrets archive; the former is a curated built-in list (SSH host keys,/etc/shadow, network credentials, root's own credentials...), freely overridable, the latter is for anything extra specific to your machine.v_enable_gpg_settings/_data/_secrets: GPG-encrypt the matching archive, off by default (see Encryption);_data/_secretsmirrorv_enable_gpg_settingsunless overridden independently.v_gpg_recipient_file/_data/_secrets: path to that archive's recipient public key, default${HOME}/pubkey.ascfor all three unless overridden.v_tar_owner:chownthe settings/data archives to the invoking user right after creation, defaulttrue(they hold no secret material by default -- see Safety).v_tar_owner_secrets: same, for the secrets archive specifically, defaultfalseand deliberately independent -- turning onv_tar_ownernever also hands the secrets archive to a non-root user; that needs its own explicittrue.v_datetime_format: the date and time at the end of every archive, checksum and log name, as adateformat, default%Y%m%d-%H%M%S(local time). Only letters, digits,.,_and-may come out of it;%Aalone gives one archive per weekday, overwritten every week.
ndm_linux-backup.conf, next to the script (its real location, also when run through a
symbolic link), overrides the CONFIGURE section of the script. It is optional: without
it the built-in defaults are used, so a single downloaded ndm_linux-backup.sh keeps
working on its own.
cd ~/bin/NDM_Linux-Backup
cp -n ndm_linux-backup.conf.example ndm_linux-backup.conf
editor ndm_linux-backup.confKeep only the lines you want to change: a single line like v_enable_mysql=true is a
valid file. The file is only read, never executed (see Safety):
v_enable_mysql=true # one KEY=value per line, # starts a comment
v_dest="/mnt/my backups" # quote values with spaces
v_gpg_recipient_file=/root/pubkey.asc # full paths: $HOME and $(...) are not expanded
a_luks=( "sda3" "nvme0n1p3" ) # lists
a_source_extra=( # lists over several lines replace the built-in one
"/etc"
"/srv/my data"
)
v_post_command='curl -fsS -d "Backup done on ${NDM_HOSTNAME}" https://ntfy.sh/REPLACE_WITH_YOUR_OWN_RANDOM_TOPIC'Only the three hook commands (v_pre_command, v_post_command, v_on_failure_command)
may contain $: they are kept as written and run later, as root, by the script (see
Notifications). An unknown setting or an invalid value stops the
script with exit code 2 and says which line is wrong; --help and --version still
work.
No ownership change is needed: ndm_linux-backup.conf stays owned by you, same as
ndm_linux-backup.sh itself. See Safety for optionally locking both files
down on a shared machine, and the CONFIGURE section of the script for every setting and
its default.
v_mysql_cnf_path defaults to ${HOME}/.my.cnf (root's, since the script requires
root) -- the same file the mysql/mysqldump client tools read automatically:
sudo cp -n ~/bin/NDM_Linux-Backup/my.cnf.example /root/.my.cnfsudo editor /root/.my.cnf(set your mysql user/pass under[client])sudo chmod 600 /root/.my.cnf
Note: you can point v_mysql_cnf_path at a custom location instead (e.g. a copy kept
alongside the script). If it's missing when v_enable_mysql=true and mysqldump is
installed, NDM Linux Backup first tries root@localhost socket auth (see below); only
if that also fails does it report the missing file and exit before doing any work -- it
never creates or edits this file itself. Unlike ndm_linux-backup.conf, there's no
separate ownership step: this file is only ever read as plain data (never
sourced/executed), so root reads it fine regardless of who owns it -- sudo cp above
already makes it root's from the start anyway. my.cnf.example itself documents all of
this inline, as # comments (MySQL option files support them).
Avoiding a password/file entirely -- Debian's default root@localhost account usually
already allows this:
- Verify:
sudo mysql --no-defaults -u root -e "SELECT 1"-- prints1with no password prompt? Already set, skip.my.cnfentirely. - Still asks for a password? Check the plugin:
sudo mysql -e "SELECT user, plugin FROM mysql.user WHERE user='root';"--unix_socket/auth_socketis the passwordless one; switch to it if it's something else.
.my.cnf, if present, always takes precedence. If it's missing, NDM Linux Backup runs
step 1 itself automatically (time-capped, so an unreachable server can't hang an
unattended run) and uses --no-defaults -u root instead.
v_postgresql_pgpass_path defaults to ${HOME}/.pgpass (root's) -- the same file the
PostgreSQL client tools read automatically:
sudo cp -n ~/bin/NDM_Linux-Backup/pgpass.example /root/.pgpasssudo editor /root/.pgpass(set host:port:database:username:password, e.g.localhost:5432:*:postgres:secret)sudo chmod 600 /root/.pgpass
Note: PostgreSQL client tools silently ignore .pgpass if it isn't chmod 600. You can
point v_postgresql_pgpass_path at a custom location instead; also set
v_postgresql_user in CONFIGURE if it doesn't match the username in .pgpass. Same as
above: NDM Linux Backup only checks that the file exists and tells you how to create it
if not, it never creates or edits it itself, and there's no separate ownership step
either, same reasoning as .my.cnf above. pgpass.example itself documents all of this
inline, as # comments (PostgreSQL skips lines starting with #, same as blank
lines).
Avoiding a password/file entirely -- unlike MySQL, this needs setup: PostgreSQL's role
name must match the connecting OS user exactly, and this script always runs as root,
not postgres:
pg_hba.conf(e.g./etc/postgresql/<version>/main/pg_hba.conf), thelocalline for the role inv_postgresql_user:local all postgres peer map=ndm_linux-backuppg_ident.conf:ndm_linux-backup root postgres(maps OSrootto PostgreSQL rolepostgres)sudo systemctl reload postgresql- Verify:
sudo psql -U postgres -tAc "SELECT 1"-- prints1with no password prompt? Done, skip.pgpassentirely.
.pgpass, if present, always takes precedence. If it's missing, NDM Linux Backup runs
step 4's check itself automatically (time-capped, so an unreachable server can't hang an
unattended run) and falls back to requiring .pgpass if it fails.
NDM Linux Backup has no notification feature of its own -- it reuses
v_pre_command/v_post_command/ v_on_failure_command (see
Configuration), each just an arbitrary command string, eval'd with a
handful of NDM_* variables exported for it to use. Any push service reachable with a
single curl call works the same way; ntfy (simple, self-hostable)
is used as the example below.
Warning
An unreserved ntfy.sh topic is public: anyone who knows or guesses its name can read
it. Pick a long, random topic name (or self-host) instead of the placeholder below.
Keep any access token the same way .my.cnf/.pgpass are kept -- in its own root-only
file, never inline inside v_pre_command/v_post_command/v_on_failure_command
themselves (those live in CONFIGURE, or in ndm_linux-backup.conf, and end up in this
machine's own settings archive via a_source_extra):
echo 'tk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' > ${HOME}/.ntfy_token
chmod 600 ${HOME}/.ntfy_token
v_enable_pre_command=true
v_pre_command='curl -fsS -H "Authorization: Bearer $(cat ${HOME}/.ntfy_token)" -H "Title: NDM Linux Backup" -d "$(date "+%F %T") - Starting backup ${NDM_USER}@${NDM_HOSTNAME}" https://ntfy.sh/REPLACE_WITH_YOUR_OWN_RANDOM_TOPIC'
v_enable_post_command=true
v_post_command='curl -fsS -H "Authorization: Bearer $(cat ${HOME}/.ntfy_token)" -H "Title: NDM Linux Backup" -d "$(date "+%F %T") - Backup done ${NDM_USER}@${NDM_HOSTNAME}: settings ${NDM_SETTINGS_ARCHIVE_SIZE}${NDM_DATA_ARCHIVE:+, data ${NDM_DATA_ARCHIVE_SIZE}}${NDM_SECRETS_ARCHIVE:+, secrets ${NDM_SECRETS_ARCHIVE_SIZE}}" https://ntfy.sh/REPLACE_WITH_YOUR_OWN_RANDOM_TOPIC'
v_enable_on_failure_command=true
v_on_failure_command='curl -fsS -H "Authorization: Bearer $(cat ${HOME}/.ntfy_token)" -H "Title: NDM Linux Backup FAILED" -H "Priority: urgent" -d "NDM Linux Backup failed on ${NDM_USER}@${NDM_HOSTNAME} (exit ${NDM_EXIT_CODE}) -- see ${NDM_LOG_PATH}" https://ntfy.sh/REPLACE_WITH_YOUR_OWN_RANDOM_TOPIC'All three use single quotes: in the CONFIGURE section of the script they keep ${NDM_*}
unexpanded until eval runs each command at its actual trigger point; in
ndm_linux-backup.conf nothing is expanded on reading anyway, so the same lines work
there as they are. v_post_command's example above skips the
, data .../, secrets ... clause entirely whenever that archive did not run this
session (${NDM_DATA_ARCHIVE:+...} -- empty unless NDM_DATA_ARCHIVE is set), so the
same one-liner works unmodified whether or not you have the data/secrets passes enabled.
--quiet (-q) is what makes an unattended run possible: it skips every confirmation
prompt (whether to run at all, whether to reuse an existing working directory, whether
to delete it afterward -- see Safety and Usage), so the whole run
needs no interaction at all, start to finish. Verified live: --start --quiet with
stdin closed (< /dev/null, exactly how cron itself runs a job) completes and exits 0
without ever touching stdin.
Example, run daily at 3:00 AM as root:
0 3 * * * /usr/local/bin/ndm_linux-backup --start --quiet
Edit root's crontab with sudo crontab -e (root's own, since the script requires root
-- see Usage). Points worth knowing:
- Without
--keep(-k), the working directory (v_tempdir) is deleted automatically after a quiet run, same as answering "yes" to the interactive prompt -- nothing left behind between runs. - cron mails a job's stdout/stderr to
MAILTO(or the crontab owner) by default, on top of the run's own.logfile already written into the backup destination (see Safety, "Run log" -- that one is complete and unconditional regardless of--quiet). If you don't want both, redirect cron's own copy instead:... --start --quiet >> /var/log/ndm_linux-backup_cron.log 2>&1. - A failed run only shows up in the log/exit code by default, which nobody is watching
live in an unattended setup -- this is exactly the case
v_on_failure_commandexists for (see Configuration and Notifications): set it once and get a real notification (email, push, webhook) instead of finding out days later. - cron's own
PATHis minimal (no shell profile is sourced), so use the script's full path in the crontab line, not justndm_linux-backup, even if you symlinked it into your normalPATH(see Installation).
sudo ndm_linux-backup --analyze reports, for your current CONFIGURE settings, which
modules will actually be backed up, which are enabled but blocked by something (a
missing tool, a missing .my.cnf/.pgpass, no LUKS partitions found, v_dest_rsync
not set...), which are simply disabled but would work if turned on, and which are
disabled and would still be blocked even if turned on because this machine lacks the
matching hardware/firmware (no UEFI, no MBR-partitioned disks, no LUKS partitions, no
LVM) -- then exits. Grouped into a section per archive (settings/data/secrets) plus a
cross-cutting one for rsync/hooks, so it's clear at a glance what each archive would
actually contain. Each archive's section also states its working directory
(v_tempdir/_data/ _secrets -- where the archive is staged before tar) and its
destination (v_dest/_data/_secrets -- where the final .tar.xz lands), so you can
confirm the paths a real run would use before it uses them. Unlike --dry-run, it never
runs a single real backup command (no lshw, no schema dumps, nothing): it only checks
applicability and prints the already-configured paths, without creating or touching any
of them -- no v_tempdir, no v_dest, no log file actually gets written or created, so
it's near-instant and reads/writes nothing beyond the check itself, regardless of
--quiet/--keep/--dry-run (all irrelevant to it) or whether it's combined with
--start (--analyze always wins and no backup runs). --secrets is honored, so it
reflects whether the secrets archive would actually be produced for that run, and it
also reports which of the archives that will actually be produced end up GPG-encrypted
or stay plain -- the same check a real run does, see Encryption.
Note
The MySQL/PostgreSQL credentials check is one exception to "no real command ever
runs": --analyze never attempts the live root@localhost socket/peer-auth
connection a real --start does (see MySQL/MariaDB backup and PostgreSQL
backup), so it can report .my.cnf/.pgpass as missing even on
a machine where a real run would still succeed via that fallback. The reported reason
always names the fallback so this doesn't read as a hard blocker.
There is deliberately no single "restore everything" command. NDM Linux Backup collects; putting things back on a real machine is a judgment call every time (matching disk, matching package versions, whether the target is even the same machine) that shouldn't be automated blindly. Restoring is: extract the archive you need, read what's in it, apply the specific piece you actually need by hand.
Before relying on an old archive, verify it against its checksum sidecar (see
Safety) -- against whatever file is actually on disk, .tar.xz or
.tar.xz.gpg if encryption is on, since the checksum covers the final artifact, not
necessarily the plaintext:
sha256sum -c ndm_linux-backup_myhost_20260815-120000.tar.xz.sha256
If the archive is encrypted (filename ends in .tar.xz.gpg, see
Encryption), decrypt it first, on whatever machine you're actually
restoring from -- the private key was deliberately never needed on the backup machine
itself:
gpg --output ndm_linux-backup_myhost_20260815-120000.tar.xz --decrypt ndm_linux-backup_myhost_20260815-120000.tar.xz.gpg
This works as-is if the matching private key is already in your own keyring
(gpg --list-secret-keys). If it isn't -- kept instead in a file, or on a removable USB
drive, specifically so it never sits on a regularly-connected machine -- import it into
an isolated, throwaway keyring for just this operation instead of merging it into your
everyday ~/.gnupg:
export GNUPGHOME="$(mktemp -d)"
gpg --import /media/usb/ndm-linux-backup-private-key.asc
gpg --output ndm_linux-backup_myhost_20260815-120000.tar.xz --decrypt ndm_linux-backup_myhost_20260815-120000.tar.xz.gpg
rm -rf "${GNUPGHOME}"
unset GNUPGHOME
The rm -rf "${GNUPGHOME}" at the end discards the temporary keyring (and the imported
private key with it) -- the USB drive or key file stays the only permanent copy, exactly
the point of keeping it offline in the first place.
Every archive extracts to a folder named after itself:
tar -xJf ndm_linux-backup_myhost_20260815-120000.tar.xz -C /path/to/extract
Every path below (software/, modified_files/, partitions/, mbr/, grub/...) is
relative to v_tempdir inside that folder -- by default
home/ndm_linux-backup/ndm_linux-backup/, since v_tempdir is an absolute path and tar
recreates it in full. The actual report lives there; etc/, root/, opt/... at the
top level is the raw a_source_extra sweep, not the report itself.
Per-item restore reference (click to expand)
- Packages (
software/apt_manual.txt,apt_auto.txt): reinstall withsudo apt-get install $(cat apt_manual.txt). This only restores package names, not pinned versions -- fine after a clean reinstall on the same Debian release, not a guarantee of byte-identical package versions. - Modified/unpackaged files (
modified_files/,unpackaged_files/, both mirroring the real/etclayout): review each file and copy back individually (diffagainst the live file first). Never extract this tree directly over a live/etc-- it was captured at one point in time and may conflict with changes made since.unpackaged_files/etc/machine-idspecifically: only restore this to the same machine you backed it up from (e.g. after a clean reinstall) --/etc/machine-idis meant to be globally unique per install, so copying it onto a different machine can confuse D-Bus, systemd-journal, and DHCP client identifiers derived from it. - Partition table (
partitions/sfdisk_<disk>.txt):sfdisk /dev/<disk> < sfdisk_<disk>.txtrecreates the partition layout -- destructive, and only meaningful on a disk of the same size/geometry as the original.partitions/fdisk_<disk>.txtis the human-readable counterpart, reference only. - MBR/boot sector (
mbr/<disk>_mbr.bin):dd if=<disk>_mbr.bin of=/dev/<disk> bs=1M count=1-- same caveat, only on the exact right disk, only if you understand this overwrites the current boot sector. - UEFI (
efi/efibootmgr.txt,efi/boot_efi/):efibootmgr.txtis reference only, recreate NVRAM boot entries by hand withefibootmgr --createif needed.boot_efi/is a real copy of the EFI System Partition content and can be copied back (cp -a boot_efi/. /boot/efi/) -- only onto the exact same or an equivalently-partitioned UEFI system, after mounting the real ESP yourself, never guessing the mountpoint. Getting this wrong on a live ESP can leave the system unable to boot. - LUKS (
luks/luksDump_<disk>.txt,luksUUID_<disk>.txt,crypttab.txt): reference only, not a restorable header backup -- this iscryptsetup luksDump's human-readable text output, not a rawluksHeaderBackupbinary dump. If a LUKS header actually gets corrupted, this tells you the cipher/key-slot layout to recreate an equivalent container by hand, but it cannot reconstruct the original header/keyslots byte-for-byte. For that, see the raw LUKS header backup in the secrets archive below. - LVM (
lvm/pvdisplay.txt,vgdisplay.txt,lvdisplay.txt,lvmdump/): reference only, not a restorable backup either -- human-readablepvdisplay/vgdisplay/lvdisplayoutput plus anlvmdumpdiagnostic bundle, not raw metadata. Use them to recreate the PV/VG/LV layout by hand (pvcreate/vgcreate/lvcreatematching the recorded names/sizes) on a fresh disk; unlike LUKS, there is no raw-metadata backup option for this. - Grub (
grub/boot_grub/,grub/default_grub.txt): copyboot_grub/back to/boot/grubanddefault_grub.txtto/etc/default/grub, then runupdate-grub-- prefer regenerating over blindly trusting the copied files if the kernel versions differ from the original.grub-install /dev/<disk>only if the boot sector itself needs reinstalling too, only on the same or an equivalent disk: a wrong disk/version combination can leave the system unable to boot. - MySQL/PostgreSQL schema (
mysql_schema/schema_<db>.sqlorschema_all.sql;postgresql_schema/schema_<db>.sql/schema_all.sql):mysql --defaults-file=.my.cnf < schema_db.sql/psql -f schema_db.sql dbnamerecreates structure (tables/views/triggers/routines), no rows. Restore schema before data (see data archive below) -- data restore assumes the tables already exist. - Docker/Podman (
docker/,podman/,docker_compose_files/): the metadata files are reference only; actual recovery isdocker compose up -d(orpodman-compose/Quadlet) from the copied compose/unit files, which pulls fresh images from the registry -- images/containers themselves were never backed up, see What it backs up. - Crontab (
crontab.txt):crontab crontab.txtrestores root's schedule. Low risk -- review the file first if it references paths that may no longer exist on the target. - Flatpak (
flatpak.txt): reference only, reinstall listed apps withflatpak install <app-id>. a_source_extradirectories: copied as-is under their own path inside the archive, restore by copying back to their original location. Same caution as Modified/unpackaged files above applies if any of them fall under/etcor/root: review before extracting over a live system.
Same as the settings archive above: every path below is relative to v_tempdir_data
inside the extracted folder -- by default
home/ndm_linux-backup/ndm_linux-backup_data/.
Per-item restore reference (click to expand)
- MySQL/PostgreSQL row data (
mysql/data_<db>.sqlordata_all.sql;postgresql/data_<db>.sql/data_all.sql): import after the matching schema exists (from the settings archive, or recreated by hand):mysql --defaults-file=.my.cnf dbname < data_db.sql/psql -f data_db.sql dbname. - Docker/Podman volume content (
docker_volumes/<name>/,podman_volumes/<name>/): create a fresh, empty volume first (docker volume create <name>), find its real Mountpoint (docker volume inspect <name>), then copy the backed-up content into it --cp -a docker_volumes/<name>/. <mountpoint>/. Check ownership/ permissions afterward: they need to match what the container expects, whichcp -aalone won't guarantee if UIDs differ between machines. a_data_source_extradirectories: copied as-is, restore by copying back to their original path.
files/ itself is relative to v_tempdir_secrets inside the extracted folder -- by
default home/ndm_linux-backup/ndm_linux-backup_secrets/files/. Every entry under it
keeps its original absolute path (e.g. files/etc/ssh/ssh_host_rsa_key,
files/root/.ssh/id_ed25519), so restoring one item is copying it back to that same
path and fixing ownership/permissions (SSH host keys: 600 root:root;
/etc/shadow//etc/gshadow: 640 root:shadow).
Per-item restore reference (click to expand)
- SSH host keys: restoring the original key (not regenerating a new one) means
clients that already trust this machine's fingerprint won't see a "REMOTE HOST
IDENTIFICATION HAS CHANGED" warning after a reinstall -- often the actual reason to
restore these specifically.
chmod 600,chown root:root, restartsshd. /root/.ssh(root's own keys, distinct from the SSH host keys above):600on private keys,644onauthorized_keys/known_hosts, owned byroot. Restoring an oldauthorized_keyscan silently reinstate access for a key you meant to revoke -- review its contents before restoring, don't copy it back blindly./etc/shadow//etc/gshadow: do not extract these over a live system's files. A mismatched or malformed/etc/shadowcan lock out every account or break PAM outright. If you need a specific account's password hash back, restore it surgically (e.g.usermod -p '<hash>' username) instead of replacing the whole file; only replace the file wholesale from an offline context (recovery media/chroot), never on a running system.- TLS private keys / Let's Encrypt / network credentials / cloud CLI configs: copy back to their original path, restart the service that uses them.
- Mail relay / PPP secrets (
exim4/passwd.client,postfix/sasl_passwd(.db),ppp/chap-secrets,ppp/pap-secrets): copy back to their original path, restart the matching service. For Postfix specifically, regeneratesasl_passwd.dbfrom the plaintext file withpostmapinstead of trusting the copied.dbbinary matches the target machine's Postfix version. /root/.gnupg: restoring this onto a keyring that already has other keys can produce duplicate or conflicting trust settings. Restore onto a fresh/root/.gnupg, orgpg --importjust the specific key you need, rather than overwriting an in-use keyring./root/.docker/config.json: copy back to restore saved registry logins. Contains registry credentials (plaintext or base64-encoded depending on the registry) -- handle the extracted file with the same care as the rest of this archive..my.cnf/.pgpass(this script's own database credentials): copy back tov_mysql_cnf_path/v_postgresql_pgpass_path,600 root:root. Only useful if the target machine's database host/user match what's inside -- otherwise NDM Linux Backup just fails to connect on the next run, same as a missing credentials file (see Safety)./etc/mysql/debian.cnf: MariaDB/MySQL's own internal maintenance credentials, copy back verbatim -- do not edit it, package maintenance scripts (logrotate,mysql_upgrade...) expect its exact original contents./root/.netrc: plaintext auto-login credentials for whatever tools read it (curl,ftp...), copy back with600 root:root-- same handling as any other plaintext-credential file in this archive.- Raw LUKS header (
luks_headers/<disk>_luksHeaderBackup.img, only present ifv_enable_luks_header_backup=true):cryptsetup luksHeaderRestore /dev/<disk> --header-backup-file <disk>_luksHeaderBackup.imgwrites it back -- only when you are certain the target really is the same volume (matching UUID, checked withcryptsetup luksUUID); restoring a header onto the wrong device makes it unreadable, it doesn't merely fail safely.
Warning
Regardless of which archive: never extract any of them directly onto / of a live
system. Extract to a scratch directory first, review what you actually need, then
apply it by hand.
sudo ./ndm_linux-backup.sh --analyze on a small Debian 13 virtual machine (BIOS, one
disk, no LUKS or LVM) with the default settings; host name and paths are generic:
Your system is based on: bios
Your partition type for /dev/vda is: dos
NDM Linux Backup 3.0.1 -- analysis for root@myhost (run --start to actually back up)
=== SETTINGS ARCHIVE (always produced) ===
Working directory (src): /home/ndm_linux-backup/ndm_linux-backup
Destination (dest): /home/user/bin/NDM_Linux-Backup/backup
Enabled: hardware, devices, lsblk, blkid, inxi, mbr, os_release, kernel, partitions, grub, packages, modified_files, unpackaged_files, cron, source_extra
Enabled but blocked:
efi -- not a UEFI system
Disabled (could enable in the settings): flatpak, appimages, mysql, postgresql, docker, docker_compose, podman
Not applicable to this machine:
luks -- no LUKS partitions found
lvm -- no LVM volumes detected
=== DATA ARCHIVE (opt-in) ===
Working directory (src): /home/ndm_linux-backup/ndm_linux-backup_data
Destination (dest): /home/user/bin/NDM_Linux-Backup/backup
Master switch (v_enable_data_backup): disabled
Enabled: (none)
Disabled (could enable in the settings): docker_volumes_data, podman_volumes_data, data_source_extra
=== SECRETS ARCHIVE (opt-in) ===
Working directory (src): /home/ndm_linux-backup/ndm_linux-backup_secrets
Destination (dest): /home/user/bin/NDM_Linux-Backup/backup
Master switch (v_enable_secrets_backup): disabled (pass --secrets to include it for this run)
Enabled: (none)
Not applicable to this machine:
luks_header_backup -- no LUKS partitions found
=== OTHER (cross-cutting) ===
Enabled: (none)
Disabled (could enable in the settings): rsync, pre_command, post_command, on_failure_command
Every archive has its own section: what will be backed up, what is switched on but cannot run on this machine (and why), what is switched off, and what this machine does not have. Nothing is written and no backup command runs (see Analyze).
None on its own: a backup run works offline and sends nothing anywhere. The network is used only by what you choose to enable:
| Service | When | What it receives |
|---|---|---|
Debian package mirrors (apt-get) |
Only if you accept installing missing tools | The usual package requests |
Your v_dest_rsync destination |
Only with v_enable_rsync=true and a remote destination |
The archives |
| Your database servers | Only with MySQL/MariaDB or PostgreSQL enabled (normally localhost) |
The credentials in .my.cnf/.pgpass |
| Whatever your hook commands call | Only with v_pre_command/v_post_command/v_on_failure_command enabled (see Notifications) |
What you put in the command |
--analyze, --dry-run, --requirements, --help, --usage and --version contact
nothing.
| Code | Meaning |
|---|---|
0 |
The backup completed; also --analyze, --requirements, --help, --usage, --version, no arguments, and a prompt answered No or Cancel |
1 |
An error: not run as root, missing required tool, missing database credentials or GPG key, unsafe working directory, failed archive or encryption step |
2 |
Invalid command-line usage (unknown option, unexpected argument, -n/-q/-k/--secrets without --start or --analyze), or an invalid setting in ndm_linux-backup.conf |
130 |
Interrupted (Ctrl-C): files already collected are kept for the next run |
143 |
Terminated (SIGTERM): the same |
For unattended runs, v_on_failure_command reports failures as they happen (see
Configuration and Notifications).
NDM_Linux-Backup/
├── ndm_linux-backup.sh the script
├── ndm_linux-backup.conf.example settings file template (versioned)
├── ndm_linux-backup.conf your settings (personal, ignored by Git)
├── my.cnf.example MySQL/MariaDB credentials template
├── pgpass.example PostgreSQL credentials template
├── backup/ default destination of the archives (contents ignored by Git)
│ └── .gitkeep keeps the empty folder in the repository
├── .github/workflows/ci.yml automatic checks (ShellCheck, checkbashisms, version, smoke tests)
├── .github/dependabot.yml monthly updates of the GitHub Actions used by CI
├── .gitattributes keeps Linux line endings
├── .gitignore keeps your settings, credentials and archives out of Git
├── LICENSE GNU AGPL v3
├── CHANGELOG.md changes in each version
├── CITATION.cff how to cite this software (used by GitHub)
├── SECURITY.md how to report a vulnerability
├── README.md documentation in English
└── README.it.md documentation in Italian
The project follows Semantic Versioning. Version and release date
are in the APP INFO block at the very top of ndm_linux-backup.sh, with name,
description and author; --version shows them. Every release has a Git tag vX.Y.Z,
and the changes of each version are listed in CHANGELOG.md. Versions up
to 2.0.10 were published as NDM Backup on
GitLab.
Copyright (C) 2020-2026 Nicola Davide Mannarelli (@nidamanx)
This program is free software: you can redistribute it and/or modify it under the terms
of the GNU Affero General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version
(SPDX-License-Identifier: AGPL-3.0-or-later). See the LICENSE file.
Credit. If you use this tool, or parts of its code, in your work or in another project, please credit NDM Linux Backup by Nicola Davide Mannarelli and link to this repository. The Cite this repository button on GitHub, generated from CITATION.cff, gives a ready-made citation. The license already requires redistributed and modified copies to keep the copyright notice.
Disclaimer. This software is provided "as is", without warranty of any kind, express or implied. In no event shall the author be liable for any claim, damages or other liability, including loss of data, backups that are incomplete or cannot be restored, and systems that no longer boot after a restore, arising from the use or misuse of this software. You alone are responsible for how you use it, for keeping and protecting the archives it creates, and for checking that they can be restored.