From 28de86b4e5d505b3ca4d78355e831e513799119a Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Tue, 9 Jun 2026 20:57:14 -0700 Subject: [PATCH 1/3] Add automated encrypted daily backup for paperless-ngx to Backblaze B2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses paperless document_exporter for version-independent exports, restic for encrypted/deduplicated snapshots, and launchd for daily 03:00 scheduling. Secrets stored in macOS Keychain at runtime — no plaintext credentials on disk. Co-Authored-By: Claude Sonnet 4.6 --- ansible/dotfiles.yml | 6 +++ ansible/tasks/paperless_backup.yml | 40 +++++++++++++++++++ paperless-ngx/backup.sh | 36 +++++++++++++++++ paperless-ngx/docker-compose.yml | 1 + ...uk.me.michaelbarton.paperless-backup.plist | 26 ++++++++++++ 5 files changed, 109 insertions(+) create mode 100644 ansible/tasks/paperless_backup.yml create mode 100755 paperless-ngx/backup.sh create mode 100644 paperless-ngx/uk.me.michaelbarton.paperless-backup.plist diff --git a/ansible/dotfiles.yml b/ansible/dotfiles.yml index 1d4adc5..d9c6f0c 100644 --- a/ansible/dotfiles.yml +++ b/ansible/dotfiles.yml @@ -71,6 +71,12 @@ - paperless - never + - name: Include paperless backup setup tasks + include_tasks: tasks/paperless_backup.yml + tags: + - paperless-backup + - never + - name: Include dotfile linking tasks include_tasks: tasks/link_files.yml tags: diff --git a/ansible/tasks/paperless_backup.yml b/ansible/tasks/paperless_backup.yml new file mode 100644 index 0000000..46cc0ad --- /dev/null +++ b/ansible/tasks/paperless_backup.yml @@ -0,0 +1,40 @@ +--- +- name: Install restic via Homebrew + community.general.homebrew: + name: restic + state: present + +- name: Create paperless backup staging directory + ansible.builtin.file: + path: "{{ ansible_user_dir }}/.paperless-backup-staging" + state: directory + mode: "0700" + +- name: Copy paperless backup launchd plist + ansible.builtin.copy: + src: "{{ ansible_user_dir }}/.dotfiles/paperless-ngx/uk.me.michaelbarton.paperless-backup.plist" + dest: "{{ ansible_user_dir }}/Library/LaunchAgents/uk.me.michaelbarton.paperless-backup.plist" + mode: "0644" + +- name: Load paperless backup launch agent + ansible.builtin.command: + cmd: launchctl load "{{ ansible_user_dir }}/Library/LaunchAgents/uk.me.michaelbarton.paperless-backup.plist" + args: + creates: "{{ ansible_user_dir }}/Library/LaunchAgents/uk.me.michaelbarton.paperless-backup.plist" + +# One-time manual setup required (run these once before first backup): +# +# security add-generic-password -a $USER -s paperless-backup-b2-id -w '' +# security add-generic-password -a $USER -s paperless-backup-b2-key -w '' +# security add-generic-password -a $USER -s paperless-backup-restic-pw -w '' +# +# IMPORTANT: Also save the restic password in 1Password — lose it and backups are unrecoverable. +# +# Then initialise the restic repo once: +# set -x B2_ACCOUNT_ID (security find-generic-password -a $USER -s paperless-backup-b2-id -w) +# set -x B2_ACCOUNT_KEY (security find-generic-password -a $USER -s paperless-backup-b2-key -w) +# set -x RESTIC_PASSWORD (security find-generic-password -a $USER -s paperless-backup-restic-pw -w) +# restic -r b2:mb-paperless-backup:paperless init +# +# Also restart the paperless stack to pick up the new volume mount: +# docker compose -f ~/.dotfiles/paperless-ngx/docker-compose.yml up -d diff --git a/paperless-ngx/backup.sh b/paperless-ngx/backup.sh new file mode 100755 index 0000000..ba96907 --- /dev/null +++ b/paperless-ngx/backup.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" + +LOG_FILE="$HOME/Library/Logs/paperless-backup.log" +STAGING_DIR="$HOME/.paperless-backup-staging" +COMPOSE_FILE="$HOME/.dotfiles/paperless-ngx/docker-compose.yml" + +log() { + echo "[$(date '+%Y-%m-%dT%H:%M:%S')] $*" | tee -a "$LOG_FILE" +} + +log "Starting paperless backup" + +export B2_ACCOUNT_ID +B2_ACCOUNT_ID=$(security find-generic-password -a "$USER" -s paperless-backup-b2-id -w) +export B2_ACCOUNT_KEY +B2_ACCOUNT_KEY=$(security find-generic-password -a "$USER" -s paperless-backup-b2-key -w) +export RESTIC_PASSWORD +RESTIC_PASSWORD=$(security find-generic-password -a "$USER" -s paperless-backup-restic-pw -w) + +log "Clearing staging directory" +mkdir -p "$STAGING_DIR" +rm -rf "${STAGING_DIR:?}"/* + +log "Running paperless document_exporter" +/Applications/Docker.app/Contents/Resources/bin/docker compose -f "$COMPOSE_FILE" exec -T webserver document_exporter -na -nt -f /usr/src/paperless/backup + +log "Running restic backup" +restic -r b2:mb-paperless-backup:paperless backup "$STAGING_DIR" + +log "Pruning old snapshots" +restic -r b2:mb-paperless-backup:paperless forget --keep-daily 7 --keep-weekly 2 --keep-monthly 2 --prune + +log "Paperless backup complete" diff --git a/paperless-ngx/docker-compose.yml b/paperless-ngx/docker-compose.yml index 361f130..bcc9041 100644 --- a/paperless-ngx/docker-compose.yml +++ b/paperless-ngx/docker-compose.yml @@ -51,6 +51,7 @@ services: - /Users/michaelbarton/.paperless:/usr/src/paperless/media:rw - /Users/michaelbarton/Downloads:/usr/src/paperless/export:rw - /Users/michaelbarton/Documents/consume:/usr/src/paperless/consume:rw + - /Users/michaelbarton/.paperless-backup-staging:/usr/src/paperless/backup:rw env_file: docker-compose.env environment: PAPERLESS_REDIS: redis://broker:6379 diff --git a/paperless-ngx/uk.me.michaelbarton.paperless-backup.plist b/paperless-ngx/uk.me.michaelbarton.paperless-backup.plist new file mode 100644 index 0000000..1dcfd70 --- /dev/null +++ b/paperless-ngx/uk.me.michaelbarton.paperless-backup.plist @@ -0,0 +1,26 @@ + + + + + Label + uk.me.michaelbarton.paperless-backup + ProgramArguments + + /bin/bash + /Users/michaelbarton/.dotfiles/paperless-ngx/backup.sh + + RunAtLoad + + StartCalendarInterval + + Hour + 3 + Minute + 0 + + StandardOutPath + /Users/michaelbarton/Library/Logs/paperless-backup.log + StandardErrorPath + /Users/michaelbarton/Library/Logs/paperless-backup.log + + From f28281beac42306a21e63cfcb54ff9c0d94b1e89 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Sat, 13 Jun 2026 16:12:49 -0700 Subject: [PATCH 2/3] Tidy up config files --- ansible/tasks/tmux.yml | 9 +++++---- fish/config.fish | 8 ++------ ghostty/config | 5 ++++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ansible/tasks/tmux.yml b/ansible/tasks/tmux.yml index d40f8b0..7b5c0ae 100644 --- a/ansible/tasks/tmux.yml +++ b/ansible/tasks/tmux.yml @@ -6,7 +6,8 @@ depth: 1 - name: Install tmux plugins via TPM - ansible.builtin.command: - cmd: "{{ ansible_user_dir }}/.tmux/plugins/tpm/bin/install_plugins" - environment: - TMUX_PLUGIN_MANAGER_PATH: "{{ ansible_user_dir }}/.tmux/plugins" + ansible.builtin.shell: + cmd: > + tmux start-server\; + set-environment -g TMUX_PLUGIN_MANAGER_PATH "{{ ansible_user_dir }}/.tmux/plugins/" && + {{ ansible_user_dir }}/.tmux/plugins/tpm/bin/install_plugins diff --git a/fish/config.fish b/fish/config.fish index 2ae5c7a..2851a08 100644 --- a/fish/config.fish +++ b/fish/config.fish @@ -185,9 +185,7 @@ function pbcat end # Use ctrl+s to fzf search the current directory -if status is-interactive - fzf_configure_bindings --directory=\cs -end +fzf_configure_bindings --directory=\cs # Search for all files with matching name in wiki function wiki_file @@ -197,9 +195,7 @@ function wiki_file --preview-window="right:65%" \ --bind "enter:become(nvim $HOME/Dropbox/wiki/{})" end -if status is-interactive - bind \cg wiki_file -end +bind \cg wiki_file # Search for all files *containing* text function wt diff --git a/ghostty/config b/ghostty/config index 90b95fe..867f9d1 100644 --- a/ghostty/config +++ b/ghostty/config @@ -5,8 +5,11 @@ shell-integration = fish theme = Catppuccin Frappe command = /opt/homebrew/bin/fish +# Auto-copy Ghostty terminfo to SSH remote hosts (prevents "unknown terminal" errors) +auto-install-terminfo = true + # Desktop notification when a long-running command finishes -notify-on-command-finish = unfocused +notify-on-command-finish = true # Let tmux handle all window/tab management keybind = super+t=unbind From a515f322013749b80f05e573bae21a3e93eeb19f Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Sat, 13 Jun 2026 16:31:15 -0700 Subject: [PATCH 3/3] fix(tmux): restore env var approach for TPM plugin installation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit set-environment is a tmux command, not a shell command — running it bare in the shell produced "unknown variable" errors. Restore the ansible environment: key for TMUX_PLUGIN_MANAGER_PATH and keep tmux start-server as a prerequisite step. Co-Authored-By: Claude Sonnet 4.6 --- ansible/tasks/tmux.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ansible/tasks/tmux.yml b/ansible/tasks/tmux.yml index 7b5c0ae..1c09b2f 100644 --- a/ansible/tasks/tmux.yml +++ b/ansible/tasks/tmux.yml @@ -8,6 +8,7 @@ - name: Install tmux plugins via TPM ansible.builtin.shell: cmd: > - tmux start-server\; - set-environment -g TMUX_PLUGIN_MANAGER_PATH "{{ ansible_user_dir }}/.tmux/plugins/" && + tmux start-server && {{ ansible_user_dir }}/.tmux/plugins/tpm/bin/install_plugins + environment: + TMUX_PLUGIN_MANAGER_PATH: "{{ ansible_user_dir }}/.tmux/plugins"