Periodic script to back up installed OS packages
Posted on 2025-12-08 in Software
A while ago I switched to yadm for managing my dotfiles. One of its features I have been enjoying is its bootstrap support. I have been adding idempotent setup operations
My latest addition is a script that creates backup files with all installed OS packages. In case of disaster if I have to reinstall the OS I can just feed the package manager the latest backup list for that host. I'm working on a different set up for my data backup, that will be the subject of a future post.
On to the script, it is simple and tailored for my systems. It only supports APT (I use Debian for my headless servers) and pacman (I use Arch Linux on my desktop), but adding DNF or other package managers should be easy enough. I wrote it in fish shell, because it is my preferred shell now.
You can see the commit in my dotfiles for the full changes, but I will be reproducing them below as of the time of writing this article.
The main script I put in ~/.local/bin/backup_installed_packages_list.fish:
#!/usr/bin/env fish
function get_hostname
if type -q hostname
hostname
else if type -q hostnamectl
hostnamectl --static
else if test -f /etc/hostname
read -l host_from_file < /etc/hostname
echo $host_from_file | string trim
else
echo "Error: cannot reliably get hostname" >&2
exit 2
end
end
set current_hostname (get_hostname)
set current_date (date +%Y-%m-%d)
set backup_dir "$HOME/Backups/$current_hostname/packages"
set temp_file (mktemp)
function save_if_changed -a input_file -a suffix
set -l target_file "$backup_dir/$current_date$suffix.txt"
# regex matches YYYY-MM-DD followed optionally by suffix, ending in .txt
set -l pattern
if test -z "$suffix"
set pattern '^\d{4}-\d{2}-\d{2}\.txt$'
else
set pattern "^\d{4}-\d{2}-\d{2}$suffix\.txt\$"
end
# find the most recent backup file that matches the specific pattern
set -l latest_backup_name (ls -1 $backup_dir 2>/dev/null | string match -r $pattern | sort | tail -n 1)
if test -n "$latest_backup_name"
set -l latest_backup_path "$backup_dir/$latest_backup_name"
if cmp -s $input_file $latest_backup_path
echo "No changes in package list compared to $latest_backup_path. Skipping."
rm $input_file
return
end
end
# if we reach here, either no backup exists or content is different
mv $input_file $target_file
echo "Installed packages backup saved: $target_file"
notify-send \
--urgency=normal \
--wait \
--icon=backup \
"New backup of installed OS packages: $target_file"
end
mkdir -p $backup_dir
if type -q apt
# https://www.debian.org/doc/manuals/debian-reference/ch10.en.html#_backup_and_recovery_policy
dpkg --get-selections > $temp_file
save_if_changed $temp_file ""
else if type -q pacman
# https://wiki.archlinux.org/title/Migrate_installation_to_new_hardware#List_of_installed_packages
pacman -Qqen > $temp_file
save_if_changed $temp_file ""
pacman -Qqem > $temp_file
save_if_changed $temp_file "_aur"
else
echo "Error: Neither apt nor pacman found." >&2
rm $temp_file
exit 1
end
I made the get_hostname helper because my Arch Linux setup didn't have the hostname command available, only hostnamectl.
notify-send is nice to get desktop notifications, I have been using it frequently in my new scripts, especially those running in the background like this one.
Note that it requires libnotify to be installed (libnotify-bin in Debian-based installations).
I install it as a common package in my yadm bootstrap configuration.
I then define systemd service and timer units, in ~/.config/systemd/user/backup-installed-packages.service and ~/.config/systemd/user/backup-installed-packages.timer:
[Unit]
Description=Backup list of installed OS packages
After=graphical-session.target
[Service]
Type=oneshot
ExecStart=%h/.local/bin/backup_installed_packages_list.fish
StandardOutput=journal
StandardError=journal
[Unit]
Description=Timer to backup list of installed OS packages
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
Then just enable the timer:
$ systemctl --user daemon-reload
$ systemctl --user enable --now backup-installed-packages.timer
I enable the systemd timer in my yadm bootstrap file.