Initial commit: restic backup configuration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 09:41:45 -04:00
commit 3af024579e
6 changed files with 224 additions and 0 deletions

4
.envrc Executable file
View File

@@ -0,0 +1,4 @@
if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM="
fi
use flake

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.password
.direnv

67
backup.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
#export RESTIC_REPOSITORY="sftp:glen@thalassa.local:/Users/glen/backups/restic-starbak"
export RESTIC_REPOSITORY="sftp:glen@192.168.0.247:/Users/glen/backups/restic-starbak"
export RESTIC_PASSWORD_FILE="${SCRIPT_DIR}/.password"
EXCLUDE_FILE="${SCRIPT_DIR}/excludes.txt"
# Force macOS system ssh — the nix restic wrapper prepends nix openssh to PATH,
# which lacks macOS network entitlements and fails inside tmux
SFTP_OPTS=(-o "sftp.command=/usr/bin/ssh glen@192.168.0.247 -s sftp")
if [ ! -f "$RESTIC_PASSWORD_FILE" ]; then
echo "Error: Password file not found at $RESTIC_PASSWORD_FILE"
echo "Create it with: head -c 32 /dev/urandom | base64 > ${RESTIC_PASSWORD_FILE}"
echo "Then: chmod 600 ${RESTIC_PASSWORD_FILE}"
exit 1
fi
case "${1:-backup}" in
init)
restic "${SFTP_OPTS[@]}" init
;;
backup)
restic "${SFTP_OPTS[@]}" backup \
/Users/glen \
--exclude-file="$EXCLUDE_FILE" \
--exclude-caches \
--one-file-system \
--verbose
;;
snapshots)
restic "${SFTP_OPTS[@]}" snapshots
;;
prune)
restic "${SFTP_OPTS[@]}" forget \
--keep-hourly 24 \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 12 \
--keep-yearly 3 \
--prune
;;
check)
restic "${SFTP_OPTS[@]}" check
;;
stats)
# per-snapshot sizes; pass snapshot ID for a specific one
restic "${SFTP_OPTS[@]}" stats "${2:-latest}"
;;
diff)
# diff two snapshots: ./backup.sh diff <id1> <id2>
restic "${SFTP_OPTS[@]}" diff "${2:?snapshot1 ID required}" "${3:?snapshot2 ID required}"
;;
mount)
MOUNT_POINT="${2:-/tmp/restic-mount}"
mkdir -p "$MOUNT_POINT"
restic "${SFTP_OPTS[@]}" mount "$MOUNT_POINT"
;;
*)
echo "Usage: $0 {init|backup|snapshots|prune|check|stats [id]|diff <id1> <id2>|mount [path]}"
exit 1
;;
esac

104
excludes.txt Normal file
View File

@@ -0,0 +1,104 @@
# ============================================
# Restic exclude list for macOS user data backup
# Mirrors Time Machine standard exclusions + extras
# ============================================
# --- macOS system/app caches ---
Library/Caches
Library/Logs
Library/Cookies
Library/HTTPStorages
Library/Saved Application State
Library/WebKit/MediaKeys
Library/Containers/*/Data/Library/Caches
Library/Containers/*/Data/Library/Logs
Library/Group Containers/*/Library/Caches
Library/Application Support
Library/Containers
# --- Spotlight & indexing ---
.Spotlight-V100
.fseventsd
# --- Trash ---
.Trash
# --- macOS metadata ---
.DS_Store
.TemporaryItems
.DocumentRevisions-V100
# --- Nix store symlinks (reconstructable) ---
.nix-defexpr
.nix-profile
# Skip files that are nix store symlinks
.zshrc
.zshenv
.manpath
# --- Build artifacts & dependency caches ---
node_modules
.gradle
.ivy2
.m2
.sbt
.npm
.cache
target
__pycache__
*.pyc
.tox
.venv
venv
.data
# --- Go ---
go/pkg
# --- IDE/editor state ---
.vscode/extensions
.vscode-server
# --- Git large objects (restic deduplicates, but no point backing up pack files) ---
.git/objects/pack
# --- Large/reconstructable/synced ---
big-files
Seafile
cloud
Downloads
# --- Application-specific transient data ---
Library/Application Support/Google/Chrome/Default/Service Worker
Library/Application Support/Google/Chrome/Default/Code Cache
Library/Application Support/Google/Chrome/Default/GPUCache
Library/Application Support/Google/Chrome/Default/IndexedDB
Library/Application Support/Google/Chrome/ShaderCache
Library/Application Support/Slack/Service Worker
Library/Application Support/Slack/Code Cache
Library/Application Support/Slack/GPUCache
Library/Application Support/Discord/Code Cache
Library/Application Support/Discord/GPUCache
# --- Photos library (synced from iCloud, very large) ---
Library/Photos
# --- Mail downloads (re-downloadable) ---
Library/Mail/V*/MailData/Envelope Index*
# --- Swap/sleep/VM ---
.SleepImage
.swap*
# --- Claude Code ---
#.claude.json.backup
# --- SSH sockets ---
.ssh/sockets
# --- atuin (shell history sync, reconstructable) ---
#.atuin
# --- zsh compiled/session state ---
.zcompdump*
.zsh_sessions

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1778794387,
"narHash": "sha256-BL04pOS9453Awkeb9f90XBJXBSkWxN+vB7HIgnL0iMM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8a1b0127302ea51e05bf4ea5a291743fac442406",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

20
flake.nix Normal file
View File

@@ -0,0 +1,20 @@
{
description = "Restic backup for starbak -> thalassa.local";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs = { self, nixpkgs }:
let
system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default = pkgs.mkShell {
packages = [
pkgs.restic
];
};
};
}