Restic backup
We back up the server to a local Mac using restic. The backup is triggered automatically by the Mac via a launchd agent that runs every 30 minutes. If the last snapshot is less than 7 days old the script exits immediately, so a full backup runs at most once a week.
What is backed up: - /home/julian
(excludes ~/.cache) - /var/www -
/srv/git - /usr/local/bin -
/etc/nginx, /etc/postfix,
/etc/dovecot, /etc/letsencrypt,
/etc/systemd/system
The restic repository lives on the Mac at
~/backups/server.
Communication
The Mac and the server each need an SSH connection to the other:
Mac ──SSH──▶ Server (Mac triggers the backup by running backup.sh on the server)
Server ──SFTP──▶ Mac (restic on the server stores snapshots on the Mac)
The Mac already has an SSH key trusted by the server (used for normal login). We additionally generate a dedicated key on the server so it can reach the Mac via SFTP.
Prerequisites
We assume restic is not yet installed on the server and that
Remote Login (SSH) is enabled on the Mac under
System Settings → General → Sharing.
Install restic on the server
# pacman -S restic
Password file
We create a password file that restic uses to encrypt all snapshots. Store this password somewhere safe — without it the repository cannot be decrypted.
$ echo 'your-strong-password' > ~/.restic-password
$ chmod 600 ~/.restic-password
SSH key from server to Mac
We generate a dedicated key pair on the server and copy the public key to the Mac:
$ ssh-keygen -t ed25519 -f ~/.ssh/id_backup -N ""
$ ssh-copy-id -i ~/.ssh/id_backup.pub julian@mac.local
Test the connection:
$ ssh -i ~/.ssh/id_backup julian@mac.local echo ok
Backup directory on the Mac
$ mkdir -p ~/backups/server
Initialise the repository
We initialise the restic repository on the Mac from the server:
$ restic -r sftp:julian@mac.local:/Users/julian/backups/server \
--password-file ~/.restic-password init
Backup script
We create /home/julian/backup.sh on the server:
#!/bin/bash
REPO="sftp:julian@mac.local:/Users/julian/backups/server"
PASSWORD_FILE=~/.restic-password
# Skip if last snapshot is less than 7 days old
LAST_JSON=$(restic -r "$REPO" --password-file "$PASSWORD_FILE" snapshots --last --json 2>/dev/null)
LAST_TIME=$(echo "$LAST_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[-1]['time'] if d else '')" 2>/dev/null)
if [ -n "$LAST_TIME" ]; then
LAST_EPOCH=$(date -d "$LAST_TIME" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
[ $(( (NOW_EPOCH - LAST_EPOCH) / 86400 )) -lt 7 ] && exit 0
fi
restic -r "$REPO" --password-file "$PASSWORD_FILE" \
--exclude ~/.cache \
backup \
/home/julian \
/var/www \
/srv/git \
/usr/local/bin \
/etc/nginx \
/etc/postfix \
/etc/dovecot \
/etc/letsencrypt \
/etc/systemd/system
$ chmod +x /home/julian/backup.sh
Run it once manually to verify everything works:
$ ~/backup.sh
Launchd agent on the Mac
We create
~/Library/LaunchAgents/com.backup.server.plist on the
Mac. The easiest way is to copy it from the server:
$ scp julian@server.local:/home/julian/com.backup.server.plist \
~/Library/LaunchAgents/com.backup.server.plist
The plist content (also at
/home/julian/com.backup.server.plist on the
server):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.backup.server</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>julian@server.local</string>
<string>/home/julian/backup.sh</string>
</array>
<key>StartInterval</key>
<integer>1800</integer>
<key>StandardOutPath</key>
<string>/tmp/server-backup.log</string>
<key>StandardErrorPath</key>
<string>/tmp/server-backup.log</string>
</dict>
</plist>
Load the agent:
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.backup.server.plist
Verify it is registered:
$ launchctl list com.backup.server
To reload after editing the plist:
$ launchctl bootout gui/$(id -u)/com.backup.server
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.backup.server.plist