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