
I tried building a daily Linux backup environment with borgbackup + USB SSD
This page has been translated by machine translation. View original
Introduction
As AI agents take on more development tasks, the risk of unintended file deletion or overwriting has increased. While major deliverables are managed with Git, tracking everything under the home directory in Git is not practical. In macOS environments, Time Machine and Google Drive have served as workarounds, but after starting to use a Linux development environment, an equivalent backup solution became necessary.
borgbackup is a tool that offers variable-length chunk-based deduplication, compression, encryption, and generation management, enabling efficient storage of incremental backups.
This article covers everything from preparing a USB SSD, initializing a borg repository, creating a backup script, to setting up daily execution via a systemd user timer. Actual measurements from the first backup and restoration patterns are also reviewed.
Verification Details
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 (approx. 7.6GB, approx. 200,000 files) |
Preparing the USB SSD
First, check the device name of the connected USB SSD. Identify the target USB SSD by looking at the capacity and connection bus (TRAN column).
$ 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 ext4 partition on this device, then 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 to mount at /mnt/backup.
$ sudo mkdir -p /mnt/backup
$ sudo blkid /dev/sda1
Use the obtained UUID to append an entry to fstab.
# /etc/fstab
UUID=<your-uuid> /mnt/backup ext4 defaults,nofail,x-systemd.device-timeout=5s 0 2
nofail ensures that if the device is absent, a mount failure is not treated as an error and boot continues. x-systemd.device-timeout=5s shortens the device wait timeout and prevents boot delays when the device is not connected.
$ sudo mount /mnt/backup
$ sudo chown $USER:$USER /mnt/backup
Installing borgbackup
borgbackup can be installed from Fedora's official repository using dnf.
$ sudo dnf install -y borgbackup
$ borg --version
borg 1.4.4
Initializing the borg Repository
Generate a passphrase for encryption. Here, Python's secrets module was used.
$ python3 -c "import secrets; print(secrets.token_urlsafe(24))"
Pass the generated passphrase via an environment variable and initialize the repository. repokey-blake2 was specified as the encryption method. This method combines AES-CTR-256 encryption with BLAKE2b-256 for authentication and chunk ID generation. For local backups, the repokey method is suitable for its straightforward key management.
$ 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. For ongoing operation, the passphrase is passed via EnvironmentFile in the systemd service.
With the repokey method, the encryption key is stored inside the repository. Export the key to prepare for SSD loss or failure.
$ borg key export /mnt/backup/borg /path/to/borg-key-backup.txt
Backup Script
zstd,3 was chosen 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 reduce backup size and time. Generation management retains daily 7, weekly 4, and monthly 3, designed to allow daily recovery for the most recent week while gradually thinning out older backups on a weekly and monthly basis. The actual retention period varies depending on 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"
}
# Prerequisite check
if ! mountpoint -q /mnt/backup; then
log "SKIP: /mnt/backup is not mounted."
exit 0
fi
# Run backup
log "Starting backup: $SOURCE -> $REPO"
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" \
--exclude "sh:$SOURCE/**/.git/objects/pack" \
::${ARCHIVE_NAME} \
"$SOURCE" \
2>&1 | tee -a "$LOG_FILE"
# Prune (generation management)
log "Pruning old archives..."
borg prune \
--list \
--show-rc \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 3 \
2>&1 | tee -a "$LOG_FILE"
# Compaction (reclaim space if there are reclaimable segments)
borg compact 2>&1 | tee -a "$LOG_FILE"
log "Backup finished."
borg info 2>&1 | tee -a "$LOG_FILE"
The mountpoint -q check at the beginning of the script means that if the USB SSD is not connected, the backup will not run and will exit successfully. The passphrase is expected to be set in the environment variable with export BORG_PASSPHRASE='...' before running the script. It is passed by copy-pasting from a password manager.
Daily Execution with systemd User Timer
systemd user timer is used for scheduled execution. First, the service definition. ConditionPathIsMountPoint skips startup if the SSD is not connected, as the startup condition is not met. Nice and IOSchedulingClass=idle keep the backup process at low priority, reducing the impact on other tasks.
# ~/.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 the file containing the passphrase. Set the value copied from the password manager in this file.
# ~/tool/backup/.env (chmod 600)
BORG_PASSPHRASE=Abc123xYz-your-passphrase-here_
Next, the timer definition. It starts daily at 03:00 and adds a random delay of up to 5 minutes with RandomizedDelaySec=300. Persistent=true ensures that if the machine is off at the scheduled start time, it will run with a delay 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
The status shows Active: active (waiting), and the next trigger is set for 03:02 the following day (including the random delay).
Note that this configuration assumes the timer operates during a desktop login session. For server environments where only SSH connections are used and no user session is persistent, run loginctl enable-linger <user>. This allows user services to run even without a user login.
Running and Results of the First Backup
The script was run manually to take the first backup.
$ ~/tool/backup/backup.sh
Output from 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
Approximately 200,000 files totaling 7.58GB were compressed with zstd,3 to 5.75GB, further reduced to 3.42GB (approximately 55% reduction) through deduplication, in 2 minutes and 18 seconds.
Results of the Second Run (Daily Automatic Execution)
The second backup was taken via automatic execution by 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 deduplicated difference was only 127.80 kB, and it completed in 40 seconds. When there are no major changes since the last backup, the amount of new data added to the repository can be kept small by reusing existing chunks.
Checking and Using Archives
Check 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. Get a list of archives 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 view the file list 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 the method can be chosen based on the use case.
| Method | Use case | Notes |
|---|---|---|
BORG_PASSPHRASE (export) |
Interactive session, scripts | Remains in process environment variables |
BORG_PASSCOMMAND |
Retrieve on demand via external command | Can integrate with password managers. Security depends on the command implementation and how secrets are stored |
This article adopted the method of passing the value copied from a password manager to BORG_PASSPHRASE. For automatic execution via systemd timer, it is passed to the script via the EnvironmentFile mentioned earlier.
$ export BORG_PASSPHRASE='Abc123xYz-your-passphrase-here_'
$ borg list /mnt/backup/borg
Using BORG_PASSCOMMAND, the passphrase can be obtained from the standard output of an external command. By combining this with a password manager's CLI, a configuration that does not retain plaintext files can be achieved.
Single File Restoration Patterns
Three representative patterns for extracting a single file were confirmed.
The first is to move to the root and restore directly by overwriting the original path. This is useful when you want to immediately restore a accidentally deleted file to 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 to a working directory. Since it is extracted while preserving the path structure within the archive, this approach is suited for operations where you want to check the diff against the current file before applying changes.
$ 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 will be /tmp/borg-restore/home/<user>/projects/my-project/SPEC.md.
The third is to use --strip-components to remove the leading directory levels and extract the file flat. This is convenient when you only want the file itself without the directory structure.
$ 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 4 levels of home/<user>/projects/my-project/. The extraction destination becomes /tmp/borg-restore-flat/SPEC.md.
The usage of the three patterns is as follows.
| 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 |
When checking diff before applying |
| Flat extraction | cd /tmp/flat && borg extract --strip-components N ... |
/tmp/flat/file |
When you only need the file itself |
Summary
The initial 7.58GB fit into 3.42GB after deduplication, and the second daily run completed with a 127 kB diff in 40 seconds. Future additional capacity will depend on the daily amount of changes, but a 500GB SSD provides ample capacity for the foreseeable operation. By combining borgbackup, a USB SSD, and a systemd user timer, a daily backup environment was built with the simplicity of just a few configuration files.
borgbackup packages are also available via the SPAL repository on Amazon Linux 2023. While this article only confirms the existence of the package, it could be a viable option for scenarios where you want to handle backups of specific paths or generation management on EC2 entirely locally.