
I Tried Setting Up a Daily Linux Backup Environment with borgbackup + a USB SSD
This page has been translated by machine translation. View original
Introduction
As I delegate more development work to AI agents, the risk of unintended file deletion or overwriting has also grown. I manage the main deliverables with Git, but putting everything under the home directory under Git management is not realistic. On macOS I handled this with Time Machine and Google Drive, but since I started using a Linux development environment, I needed an equivalent backup setup.
borgbackup is a tool that provides variable-size chunk-based deduplication, compression, encryption, and generation management, and can store incremental backups efficiently.
In this article, I built the whole setup: preparing a USB SSD, initializing a borg repository, creating a backup script, and running it daily with a systemd user timer. I also checked the measured results of the first backup and restore patterns.
What I Tested
Test Environment
| Item | Details |
|---|---|
| OS | Fedora Linux 43 (Asahi Remix / aarch64) |
| borg | 1.4.4 (dnf) |
| USB SSD | 500GB |
| Backup target | User's home directory (about 7.6GB, about 200,000 files) |
Preparing the USB SSD
First, check the device name of the connected USB SSD. Look at the capacity and the connection bus (the TRAN column) to identify that the target is the intended USB SSD.
$ lsblk -o NAME,SIZE,TYPE,MODEL,TRAN
NAME SIZE TYPE MODEL TRAN
sda 465.8G disk SSD-PSTA/D usb
SSD-PSTA/D appears as a 465.8GB disk connected via the usb bus. Create a GPT label and an ext4 partition on this device, and format it.
$ sudo parted /dev/sda --script mklabel gpt
$ sudo parted /dev/sda --script mkpart primary ext4 0% 100%
$ sudo mkfs.ext4 -L borgbackup /dev/sda1
After formatting, register it in fstab so that it is mounted at /mnt/backup.
$ sudo mkdir -p /mnt/backup
$ sudo blkid /dev/sda1
Add an entry to fstab using the obtained UUID.
# /etc/fstab
UUID=<your-uuid> /mnt/backup ext4 defaults,nofail,x-systemd.device-timeout=5s 0 2
With nofail, boot continues without treating a mount failure as an error even if the device does not exist. x-systemd.device-timeout=5s shortens the timeout for waiting for the device, preventing boot delays when it is not connected.
$ sudo mount /mnt/backup
$ sudo chown $USER:$USER /mnt/backup
Installing borgbackup
borgbackup can be installed with dnf from Fedora's official repositories.
$ sudo dnf install -y borgbackup
$ borg --version
borg 1.4.4
Initializing the borg Repository
Generate a passphrase to use for encryption. Here I used Python's secrets module.
$ python3 -c "import secrets; print(secrets.token_urlsafe(24))"
Pass the generated passphrase through an environment variable and initialize the repository. I specified repokey-blake2 as the encryption mode. It combines AES-CTR-256 encryption with BLAKE2b-256 for authentication and chunk ID generation. For local backups, the repokey mode, which makes key management simple, is suitable.
$ export BORG_PASSPHRASE='Abc123xYz-your-passphrase-here_'
$ borg init --encryption=repokey-blake2 /mnt/backup/borg
Once initialization is complete, remove the environment variable with unset BORG_PASSPHRASE. In regular operation, the passphrase is passed via the systemd service's EnvironmentFile.
With the repokey mode, the encryption key is stored inside the repository. Export the key in case the SSD is lost or fails.
$ borg key export /mnt/backup/borg /path/to/borg-key-backup.txt
Backup Script
I adopted zstd,3 as the compression method for its good balance of speed and compression ratio. Directories that can be regenerated in the development environment (caches, dependency packages, build artifacts, etc.) are excluded to keep the backup size and duration down. For generation management, I keep 7 daily, 4 weekly, and 3 monthly backups, so that the last week can be restored on a daily basis, while older ones are thinned out gradually into weekly and monthly ones. The actual retention period varies depending on the backup frequency and missing days.
#!/bin/bash
# ~/tool/backup/backup.sh
set -euo pipefail
REPO="/mnt/backup/borg"
SOURCE="$HOME"
export BORG_PASSPHRASE="${BORG_PASSPHRASE:?Set BORG_PASSPHRASE before running}"
export BORG_REPO="$REPO"
ARCHIVE_NAME="ws-{now:%Y-%m-%dT%H:%M:%S}"
LOG_FILE="$HOME/tool/backup/backup.log"
log() {
echo "[$(date -Iseconds)] $*" | tee -a "$LOG_FILE"
}
# 前提チェック
if ! mountpoint -q /mnt/backup; then
log "SKIP: /mnt/backup is not mounted."
exit 0
fi
# バックアップ実行
log "Starting backup: $SOURCE -> $REPO"
# すべてのリポジトリがリモートにpush済みなら下記を有効化し .git/objects/pack を除外できます
# --exclude "sh:$SOURCE/**/.git/objects/pack"
borg create \
--verbose \
--filter AME \
--stats \
--show-rc \
--compression zstd,3 \
--exclude-caches \
--exclude "sh:$SOURCE/.cache" \
--exclude "sh:$SOURCE/**/node_modules" \
--exclude "sh:$SOURCE/**/__pycache__" \
--exclude "sh:$SOURCE/**/.venv" \
--exclude "sh:$SOURCE/**/.tox" \
--exclude "sh:$SOURCE/**/target" \
--exclude "sh:$SOURCE/**/.terraform" \
--exclude "sh:$SOURCE/**/dist" \
::${ARCHIVE_NAME} \
"$SOURCE" \
2>&1 | tee -a "$LOG_FILE"
# Prune(世代管理)
log "Pruning old archives..."
borg prune \
--list \
--show-rc \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 3 \
2>&1 | tee -a "$LOG_FILE"
# コンパクション(回収対象セグメントがあれば領域を回収)
borg compact 2>&1 | tee -a "$LOG_FILE"
log "Backup finished."
borg info 2>&1 | tee -a "$LOG_FILE"
Thanks to the mountpoint -q check at the beginning of the script, if the USB SSD is not connected, the script exits normally without running the backup. The passphrase is assumed to be set in advance as an environment variable with export BORG_PASSPHRASE='...' before running the script. Copy and paste it from the password manager.
Daily Execution with a systemd user timer
I use a systemd user timer for scheduled execution. First is the service definition. With ConditionPathIsMountPoint, the start condition is not met and the run is skipped if the SSD is not connected. Nice and IOSchedulingClass=idle keep the backup process at low priority to reduce the impact on other work.
# ~/.config/systemd/user/ws-backup.service
[Unit]
Description=Borg backup home directory to USB SSD
ConditionPathIsMountPoint=/mnt/backup
[Service]
Type=oneshot
ExecStart=%h/tool/backup/backup.sh
EnvironmentFile=%h/tool/backup/.env
Nice=10
IOSchedulingClass=idle
EnvironmentFile loads a file containing the passphrase. Set the value copied from the password manager in the file.
# ~/tool/backup/.env (chmod 600)
BORG_PASSPHRASE=Abc123xYz-your-passphrase-here_
Next is the timer definition. It starts every day at 03:00, and RandomizedDelaySec=300 adds a random delay of up to 5 minutes. With Persistent=true, if the machine was off at the scheduled start time, the run is executed late at the next startup.
# ~/.config/systemd/user/ws-backup.timer
[Unit]
Description=Daily borg backup timer
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
Enable the timer and check its status.
$ systemctl --user daemon-reload
$ systemctl --user enable --now ws-backup.timer
$ systemctl --user status ws-backup.timer
● ws-backup.timer - Daily borg backup timer
Active: active (waiting)
Trigger: Mon 2026-07-21 03:02:22 JST; 23h left
It became Active: active (waiting), and the next trigger was set for 03:02 the next day (including the random delay).
Note that this configuration assumes desktop use where the timer runs during a login session. In a server environment where a user session does not persist, such as with SSH connections only, run loginctl enable-linger <user>. This lets user services run even when the user is not logged in.
Running the First Backup and Results
I ran the script manually and took the first backup.
$ ~/tool/backup/backup.sh
This is the output of borg create --stats.
Archive name: ws-2026-07-20T00:22:52
Duration: 2 minutes 18.09 seconds
Number of files: 204,884
Original size Compressed size Deduplicated size
This archive: 7.58 GB 5.75 GB 3.42 GB
About 200,000 files with an original size of 7.58GB shrank to 5.75GB with zstd,3 compression, and further to 3.42GB with deduplication (about a 55% reduction), and it took 2 minutes 18 seconds.
Results of the Second (Automatic Daily) Run
The second backup was taken by the automatic run from the systemd timer.
Archive name: ws-2026-07-20T03:02:56
Duration: 40.07 seconds
Number of files: 204,922
Original size Compressed size Deduplicated size
This archive: 7.58 GB 5.75 GB 127.80 kB
All archives: 15.16 GB 11.51 GB 3.42 GB
The difference after deduplication was only 127.80 kB, and it took 40 seconds. If there are no major changes since the previous run, reusing existing chunks keeps the amount of data newly added to the repository small.
Checking and Using Archives
Check the repository information.
$ borg info /mnt/backup/borg
Repository ID: 2e7cb78e...
Encrypted: Yes (repokey BLAKE2b)
Original size Compressed size Deduplicated size
All archives: 15.16 GB 11.51 GB 3.42 GB
Unique chunks Total chunks
Chunk index: 75812 414144
Encrypted: Yes (repokey BLAKE2b) confirms that encryption is enabled. The list of archives is obtained with borg list.
$ borg list /mnt/backup/borg
ws-2026-07-20T00:22:52 Mon, 2026-07-20 00:22:52 [fa94ad80...]
ws-2026-07-20T03:02:56 Mon, 2026-07-20 03:02:56 [c3b17e22...]
By specifying an archive name and passing a path, you can check the list of files under a specific directory.
$ borg list /mnt/backup/borg::ws-2026-07-20T00:22:52 home/<user>/projects/my-project/ | head -15
File list output
drwxrwsr-x user user 0 Sun, 2026-07-12 04:35:14 home/<user>/projects/my-project
-rw-rw-r-- user user 139 Sun, 2026-06-07 18:01:59 home/<user>/projects/my-project/monitor.timer
-rwxrwxr-x user user 4543 Mon, 2026-06-15 00:49:30 home/<user>/projects/my-project/monitor.sh
-rw-rw-r-- user user 149 Sun, 2026-06-07 18:01:59 home/<user>/projects/my-project/monitor.service
drwxrwsr-x user user 0 Mon, 2026-07-20 00:04:47 home/<user>/projects/my-project/data
-rw-rw-r-- user user 21533 Sun, 2026-06-07 23:56:29 home/<user>/projects/my-project/data/2026-06-07.jsonl
-rw-rw-r-- user user 92911 Mon, 2026-06-08 23:57:46 home/<user>/projects/my-project/data/2026-06-08.jsonl
-rw-rw-r-- user user 92903 Tue, 2026-06-09 23:58:46 home/<user>/projects/my-project/data/2026-06-09.jsonl
-rw-rw-r-- user user 93272 Wed, 2026-06-10 23:57:46 home/<user>/projects/my-project/data/2026-06-10.jsonl
-rw-rw-r-- user user 93272 Thu, 2026-06-11 23:56:16 home/<user>/projects/my-project/data/2026-06-11.jsonl
-rw-rw-r-- user user 93629 Fri, 2026-06-12 23:59:16 home/<user>/projects/my-project/data/2026-06-12.jsonl
-rw-rw-r-- user user 93272 Sat, 2026-06-13 23:57:46 home/<user>/projects/my-project/data/2026-06-13.jsonl
-rw-rw-r-- user user 93272 Sun, 2026-06-14 23:58:16 home/<user>/projects/my-project/data/2026-06-14.jsonl
-rw-rw-r-- user user 139081 Mon, 2026-06-15 23:57:17 home/<user>/projects/my-project/data/2026-06-15.jsonl
-rw-rw-r-- user user 138860 Tue, 2026-06-16 23:55:17 home/<user>/projects/my-project/data/2026-06-16.jsonl
These operations require the passphrase. There are several ways to pass the passphrase to borg, and you can choose among them depending on the use case.
| Method | Use case | Notes |
|---|---|---|
BORG_PASSPHRASE (export) |
Interactive sessions, scripts | Remains in the process environment variables |
BORG_PASSCOMMAND |
Retrieve each time with an external command | Can integrate with password managers, etc. Security depends on the command implementation and how the secret is stored |
In this article, I adopted the approach of passing the value copied from the password manager to BORG_PASSPHRASE. For automatic execution from the systemd timer, it is passed to the script via the EnvironmentFile described earlier.
$ export BORG_PASSPHRASE='Abc123xYz-your-passphrase-here_'
$ borg list /mnt/backup/borg
With BORG_PASSCOMMAND, you can obtain the passphrase from the standard output of an external command. By combining it with a password manager's CLI, you can also achieve a setup that does not keep a plaintext file.
Single-File Restore Patterns
I checked three typical patterns for extracting just one file.
The first is to move to the root and restore by overwriting the original path as is. Use it when you want to immediately put an accidentally deleted file back in its original location.
$ cd /
$ borg extract /mnt/backup/borg::ws-2026-07-20T00:22:52 \
home/<user>/projects/my-project/SPEC.md
-rw-rw-r--. 1 user user 5575 6月 15 00:50 /home/<user>/projects/my-project/SPEC.md
The second is to extract into a working directory. Since it is extracted while keeping the path structure inside the archive, it suits workflows where you want to check the differences against the current file before applying it.
$ mkdir -p /tmp/borg-restore
$ cd /tmp/borg-restore
$ borg extract /mnt/backup/borg::ws-2026-07-20T00:22:52 \
home/<user>/projects/my-project/SPEC.md
The extraction destination is /tmp/borg-restore/home/<user>/projects/my-project/SPEC.md.
The third is to strip the leading path levels with --strip-components and extract the file flat. It is convenient when you do not need the directory structure and just want the single file.
$ mkdir -p /tmp/borg-restore-flat
$ cd /tmp/borg-restore-flat
$ borg extract --strip-components 4 /mnt/backup/borg::ws-2026-07-20T00:22:52 \
home/<user>/projects/my-project/SPEC.md
--strip-components 4 removes the four levels of home/<user>/projects/my-project/. The extraction destination is /tmp/borg-restore-flat/SPEC.md.
Here is how to choose among the three patterns.
| Pattern | Command | Destination | Use case |
|---|---|---|---|
| Overwrite original path | cd / && borg extract ... |
/home/<user>/.../file |
When you want to restore immediately |
| Separate directory | cd /tmp/restore && borg extract ... |
/tmp/restore/home/<user>/.../file |
Check differences before applying |
| Flat extraction | cd /tmp/flat && borg extract --strip-components N ... |
/tmp/flat/file |
When you only want the single file |
Summary
The initial 7.58GB fit into 3.42GB after deduplication, and the second daily run completed in 40 seconds with a difference of 127 kB. The additional capacity needed from here on depends on the amount of daily changes, but a 500GB SSD leaves ample room for the foreseeable future. With the combination of borgbackup, a USB SSD, and a systemd user timer, I was able to build a daily backup environment with just a handful of configuration files.
As a side note, Amazon Linux 2023 also provides a borgbackup package via the SPAL repository. I only confirmed that the package exists this time, but it seems like it could be an option when you want to complete path-specific backups and generation management on EC2 locally.