macOS backups with Kopia and Backblaze

I use Kopia to back my Mac up to the cloud. It’s fast, open-source, flexible, and the best part is that it’s costing me a mere $0.26 per month for 20GB of data.

Kopia takes incremental snapshots of directories of my choosing, and I’ve configured it to store my backups in Backblaze’s B2 service. Content is intelligently deduplicated, which makes taking new snapshots fast and space-efficient. B2’s pricing makes this setup astonishingly cheap. You get the first 10GB of storage completely free, and after that it’s only $0.026 per GB/month, which is less than a fifth of the cost of S3. The bandwidth is also significantly cheaper than S3. I’ve been using this setup for nearly two years, and I’ve still spent so little that Backblaze hasn’t bothered to bill me.

Kopia ships with a command line tool and an Electron-based GUI. My poor little laptop is already running enough instances of Chromium, so I chose to stick with the command line tool. The main downside of this approach is that it’s up to you to schedule and run any automated backups. Getting this set up is a little fiddly, so I wrote this post mostly to remind future me how to do it.

Kopia’s documentation does a good job of explaining how to get started. First you need to set up a storage provider (like Backblaze B2, AWS S3, etc) and create a new backup respository. Then you configure things like retention, compression, and which paths to ignore (ahem, node_modules) by creating snapshot policies. Here’s an example of what my policy looks like:

{
  "retention": {
    "keepLatest": 10,
    "keepHourly": 48,
    "keepDaily": 7,
    "keepWeekly": 4,
    "keepMonthly": 24,
    "keepAnnual": 3
  },
  "files": {
    "ignore": [
      ".build/",
      ".next/",
      ".venv/",
      "__pycache__/",
      "node_modules/",
      "target/debug/",
      "target/release/",
      ".DS_Store"
    ],
    "ignoreDotFiles": [
      ".kopiaignore"
    ]
  },
  ...
}

Creating a snapshot is simply a matter of running kopia snapshot create PATH. But if you choose not to use the Electron-based GUI, it wasn’t immediately obvious how to run automated backups on macOS, so here’s a quick description of how I went about it.

I started out by wrapping Kopia in a script that snapshots each of the directories I care about:

#!/bin/bash -e

echo "[$(date '+%F %T')] starting backup"
backup_dirs=(
  "$HOME/src/github.com/hmarr"
  "$HOME/Library/Fonts"
  # etc...
)
kopia snapshot create "${backup_dirs[@]}"
echo "[$(date '+%F %T')] backup complete"

This script lives in ~/bin/create-kopia-snapshots. To run the backup daily, I added a launch agent configuration to ~/Library/LaunchAgents:

<?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.hmarr.kopia-backup</string>

    <key>ProgramArguments</key>
    <array>
      <string>/bin/zsh</string>
      <string>-l</string>
      <string>/Users/hmarr/bin/create-kopia-snapshots</string>
    </array>

    <key>StandardOutPath</key>
    <string>/Users/hmarr/Library/Logs/com.hmarr.kopia-backup.stdout</string>

    <key>StandardErrorPath</key>
    <string>/Users/hmarr/Library/Logs/com.hmarr.kopia-backup.stderr</string>

    <key>WorkingDirectory</key>
    <string>/Users/hmarr</string>

    <key>StartCalendarInterval</key>
    <dict>
      <key>Hour</key>
      <integer>18</integer>
    </dict>
  </dict>
</plist>

To use this LaunchAgent:

  1. Swap out all the references to my home directory with yours
  2. Add it to ~/Library/LaunchAgents/ with a name like com.<your-username>.kopia-backup.plist
  3. Register the file with launchd by running launchctl load ~/Library/LaunchAgents/com.<your-username>.kopia-backup.plist
  4. Tell the script to run right away with launchctl start com.<your-username>.kopia-backup

If that worked, you should see a new snapshot when you run kopia snapshot list. If not, to see what went wrong, have a look at the logs in ~/Library/Logs/com.<your-username>.kopia-backup.std{out,err}.

At this point, you should have a working backup solution. To make sure my backups are actually working, I ping healthchecks.io at the end of the backup script, and I’ve configured healthchecks.io to send me an email if it doesn’t hear from me for a few days.