Quick answer. BorgBackup is a command-line tool for incremental backups with deduplication and client-side encryption. Install it via apt/pip, repository initialization takes one command, automation runs through a systemd-timer. The first archive weighs as much as a full backup, every subsequent one stores only the delta. Restore works through borg extract or by mounting the archive as a regular folder.
1. Diagnosis: why rsync and tar don’t cut it anymore
You set up a production server. You configured cron with tar running once a night. Six months later the backup disk is full, because every night a full archive gets written from scratch. Sounds familiar? Most people run this way until the first time they actually need the backup and there’s no space left for it.
BorgBackup solves this with chunk-level deduplication: it splits files into variable-sized blocks, hashes them, and stores only the unique pieces. An incremental BorgBackup run, after the first full archive, takes up only as much space as actually changed, not a byte more.
What you’ll get by the end of this article:
- A working encrypted Borg repository
- Automatic execution via systemd-timer instead of cron
- A rotation policy that doesn’t eat your disk forever
- A tested restore procedure, not just blind faith
Setup time: 30-40 minutes, if you don’t get stuck in the docs. You’ll need SSH access with write permission to the target directory and a current python3 version.
System requirements
| Component | Minimum version | Recommended |
|---|---|---|
| OS | Debian 11 / Ubuntu 20.04 | Debian 12 / Ubuntu 24.04 |
| Python | 3.9 | 3.10+ |
| BorgBackup | 1.2.x | 1.4.x (stable branch) |
| OpenSSH (for remote repository) | 7.x | 9.x |
At the time of publication, the current stable version is 1.4.5 (1.4 branch). The 2.0 branch is still in beta, don’t put it into production, no matter how tempting the new features look. Before installing, check the latest releases on the official borgbackup.org site.
2. Reasons to move from tar/rsync to Borg
| Reason | Why it breaks your workflow without Borg |
|---|---|
| Full copies every night | The backup disk grows linearly, and space runs out suddenly, at the worst possible moment |
| No encryption out of the box | tar + remote storage means your data sits in plain text on someone else’s server |
| No integrity checking | A corrupted backup only shows up at restore time, that is, when it’s already too late |
| Manual rotation via find -mtime | One wrong flag and the script deletes the wrong thing |
| No deduplication across machines | Backing up 10 identical servers means paying for 10x the space instead of a reasonable amount |
| Slow single-file restore | You have to unpack the whole archive just to pull out one config file |
Small mistakes cause big damage. One line in crontab without a comment, and the whole team spends two hours figuring out which script is wiping week-old backups.
3. The recipe: installing and configuring Borg
Preparation
Check your Python version and your package manager. Borg doesn’t pull in many dependencies, but it’s worth checking in advance.
python3 --version
apt list --installed 2>/dev/null | grep -i openssh
Expected result: python3 version 3.9 or higher, ssh client installed.
Step 1. Installing BorgBackup
Two paths: the system package or pip. The system package is simpler, pip gives you a more current version.
sudo apt update
sudo apt install -y borgbackup
borg --version
If the repository version is old, install via pipx, don’t mix the system python with third-party packages:
sudo apt install -y pipx
pipx install borgbackup
pipx ensurepath
Expected result: borg --version shows 1.4.x.
Step 2. Creating a backup user
Don’t run Borg as root without a reason. A dedicated user limits the damage if something goes wrong.
sudo useradd -m -s /bin/bash backupuser
sudo mkdir -p /srv/borg-repo
sudo chown backupuser:backupuser /srv/borg-repo
Expected result: the backupuser account exists, the repository directory is ready and owned by it.
Step 3. Initializing the repository
This is where it matters not to mix up the encryption mode. For a local or network disk, use repokey-blake2, the key is stored inside the repository, and blake2 is faster on modern CPUs.
sudo -u backupuser borg init --encryption=repokey-blake2 /srv/borg-repo
Borg will ask for a passphrase for the key. Pick a strong one and save it in a password manager, without it you can’t restore the backup, even with full access to the repository.
borg key export /srv/borg-repo /root/borg-key-backup.txt
Expected result: a borg-key-backup.txt file with a backup copy of the key. Keep it separate from the repository, on a flash drive, in a safe, anywhere except next to the backups.
Step 4. First backup
export BORG_PASSPHRASE='your-key-passphrase'
sudo -u backupuser -E borg create --stats --progress \
/srv/borg-repo::backup-{now:%Y-%m-%d_%H-%M} \
/etc /home /var/www
Expected result: an archive named something like backup-2026-07-28_14-30 inside the repository. The –stats flag shows the data volume and deduplication ratio right after completion.
Next come the architectural details. Below is a diagram of how Borg works internally, so you don’t have to guess what happens between the command and the repository.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#f8fafc',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Files on the server"] --> B["Split into chunks"]
B --> C["Hash the chunks"]
C --> D{"Chunk already exists?"}
D -->|Yes| E["Skip, link to existing"]
D -->|No| F["Compress and encrypt chunk"]
F --> G["Write to repository"]
E --> H["Archive ready"]
G --> H
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style H fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
Step 5. Archive rotation via borg prune
Without rotation the repository grows forever. borg prune removes old archives according to a retention policy, keeping N most recent daily, M weekly, and so on.
sudo -u backupuser -E borg prune -v --list \
--keep-daily=7 --keep-weekly=4 --keep-monthly=6 \
/srv/borg-repo
Expected result: the repository contains no more than 7 daily, 4 weekly, and 6 monthly archives. Disk space is physically freed only after a separate compact command.
sudo -u backupuser -E borg compact /srv/borg-repo
Without compact, deleted chunks still take up disk space, Borg just stops referencing them. Forget about compact, and you’ll be puzzled why space didn’t free up after prune. This has happened more than once.
Step 6. Automation via systemd-timer
Cron works, but systemd-timer gives you logs through journalctl, dependencies on other units, and sane error handling. This is where the whole automation comes together.
sudo mkdir -p /etc/borg
sudo tee /etc/borg/backup.sh > /dev/null << 'EOF'
#!/bin/bash
set -euo pipefail
export BORG_REPO=/srv/borg-repo
export BORG_PASSPHRASE='your-key-passphrase'
borg create --stats --compression zstd,6 \
"$BORG_REPO"::backup-{now:%Y-%m-%d_%H-%M} \
/etc /home /var/www
borg prune -v --list \
--keep-daily=7 --keep-weekly=4 --keep-monthly=6 \
"$BORG_REPO"
borg compact "$BORG_REPO"
EOF
sudo chmod 750 /etc/borg/backup.sh
sudo chown backupuser:backupuser /etc/borg/backup.sh
The systemd service file:
[Unit]
Description=BorgBackup daily job
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=backupuser
ExecStart=/etc/borg/backup.sh
Nice=19
IOSchedulingClass=idle
Save as /etc/systemd/system/borg-backup.service.
The timer file:
[Unit]
Description=Run BorgBackup daily at 03:15
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Save as /etc/systemd/system/borg-backup.timer.
sudo systemctl daemon-reload
sudo systemctl enable --now borg-backup.timer
Expected result: systemctl list-timers shows borg-backup.timer with the next run time.
4. Verification: does the backup actually work
The command finishing without errors means nothing on its own. Verify the fact.
sudo systemctl status borg-backup.timer
sudo journalctl -u borg-backup.service --since "2 days ago"
sudo -u backupuser borg list /srv/borg-repo
Expected result: the list shows archives with the expected dates, journalctl shows no tracebacks or access errors.
Checking repository integrity is a separate command, don’t rely only on the archive list:
sudo -u backupuser -E borg check --verify-data /srv/borg-repo
–verify-data runs a check across all data, not just metadata. It takes longer, but it’s the one that catches corrupted chunks on disk.
Verifying restore: mounting an archive
This is the most important part. Borg can mount an archive as a regular FUSE filesystem, so you can browse the contents without a full extraction.
sudo apt install -y borgbackup-fuse
mkdir -p /tmp/borg-mount
sudo -u backupuser -E borg mount /srv/borg-repo::backup-2026-07-28_03-15 /tmp/borg-mount
ls /tmp/borg-mount/etc
sudo -u backupuser borg umount /tmp/borg-mount
Expected result: the contents of /etc from the archive are available as a regular folder. Forget to unmount, and the borg process will hang around in memory, don’t worry, that’s normal until umount.
5. Complications
| Error | Cause | Fix | Command |
|---|---|---|---|
| Repository has been created with encryption mode X | The passphrase in BORG_PASSPHRASE doesn’t match the one used when the repository was created | Check where the variable comes from, via export or from another script | borg info /srv/borg-repo |
| Failed to lock repository | A previous borg process hung or crashed without releasing the lock | Remove the stale lock only if you’re sure the process is dead | borg break-lock /srv/borg-repo |
| Insufficient free space to complete transaction | The repository disk is full, prune and compact haven’t run in a while | Run prune and compact manually, check free space | df -h /srv/borg-repo && borg compact /srv/borg-repo |
| Connection closed by remote host (SSH repository) | SSH session drops during a long backup, timeout on the server side | Add ServerAliveInterval to the client’s SSH config | ServerAliveInterval 60 |
| borg-backup.service failed with code 1, journalctl is empty | The script runs as the wrong user, no permissions on the repository | Check the owner of the repository directory and User= in the unit | stat /srv/borg-repo |
Some backups just refuse to cooperate. That’s exactly what a hung borg check looks like on a multi-terabyte repository without –verify-data. Don’t panic, just wait or run it in screen/tmux.
6. Alternatives
- Restic: a similar deduplication model, written in Go, builds more easily as a static binary, but has a less flexible key storage system. Pick Borg over Restic when fine-tuned compression and compatibility with borgmatic matter.
- Rsnapshot: hardlinks instead of chunk-level deduplication, simpler conceptually, but has no built-in encryption and handles binary changes inside large files worse.
- Duplicity: encryption via GPG, works with cloud backends out of the box, but incremental chains break if one link is lost, forcing a full backup redo.
- Kopia: a newer tool with an architecture similar to Borg, has a web UI, but its ecosystem and documentation are younger, with fewer proven production scenarios.
- Borgmatic: not an alternative, but a wrapper around Borg. Gives you a YAML config instead of a bash script and built-in database hooks. If the bash script from step 6 feels clunky, look into this.
The main case for Borg: project maturity, chunk-level deduplication, and client-side encryption without extra tooling.
7. Prevention
Monitoring
Without monitoring, you find out about a broken backup only at restore time. The simplest option is an HTTP healthcheck ping after every run:
curl -fsS -m 10 --retry 3 https://hc-ping.com/your-uuid-here
Add this line to the end of backup.sh after a successful prune, the healthcheck service will alert you if the ping doesn’t arrive on time.
Backing up the key itself
| What to back up | How often | Where to store it |
|---|---|---|
| Encryption key (borg key export) | Every time the passphrase changes | Separate from the repository: cloud storage, physical media |
| Repository config file | Once a month | Together with the key |
| The repository itself | Daily (via step 6) | Separate physical disk or another server |
Restoring the key if lost:
borg key import /srv/borg-repo /root/borg-key-backup.txt
Updating Borg
Before a major version upgrade, read the changelog. Between 1.2 and 1.4, some internal structure formats changed.
borg --version
pipx upgrade borgbackup
borg check --verify-data /srv/borg-repo
Rolling back: pipx lets you install a specific version if the new one misbehaves.
pipx install --force "borgbackup==1.2.9"
Security
- UFW: allow SSH only from trusted IPs if the repository sits on a remote server
- Fail2ban: on the SSH service accepting backup connections
- A dedicated SSH key for backupuser, restricted via authorized_keys command=
- Firewall IP restriction for the SSH port of the remote repository
- Append-only mode for borg serve on the server side: protects against client compromise
Append-only is enabled with a flag when starting the remote borg serve:
borg serve --restrict-to-path /srv/borg-repo --append-only
With append-only, even if an attacker gets access to the client, they can’t delete old archives, only add new ones.
8. FAQ
Why doesn’t borg create work after setting up a systemd-timer?
Most often it’s the BORG_PASSPHRASE variable, it doesn’t carry over into a systemd service the same way it does in an interactive shell. Set it directly inside backup.sh or via Environment= in the unit, not through ~/.bashrc.
How do you verify that a BorgBackup backup actually works?
Three levels of checking: borg list shows the archives, borg check –verify-data checks data integrity on disk, and an actual restore via borg mount or borg extract to a test machine is the only way to confirm the data is readable, not just present.
What if borg prune doesn’t free up disk space?
prune only marks data as deleted in the repository metadata. Physical space reclamation happens through the separate borg compact command, without it the disk won’t clear up no matter how many archives you delete.
How does BorgBackup differ from restic?
Both tools use a similar chunk-based deduplication model. Borg offers more flexible compression settings and key storage modes, restic is simpler to deploy as a single static binary and supports more cloud backends out of the box without extra tooling like rclone.
Can you restore a single file without extracting the whole archive?
Yes, borg mount lets you mount the archive as a regular directory and copy the needed file directly from there. Alternatively, borg extract with a specific path inside the archive extracts only that file.
9. Outlook
You’ve set up a repository with client-side encryption, automation via systemd-timer instead of cron, and rotation that keeps the disk from filling up. Now the backup runs every night on its own, without reminders, and disk space usage matches exactly what changed in the last 24 hours.
From here it’s a matter of discipline: once a month, log in and verify restore by hand, don’t trust a green systemctl status alone. If something doesn’t work after setup, leave a comment, we’ll figure it out.
Architecture: client-server over SSH
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#f8fafc',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'noteBkgColor': '#fefce8',
'noteTextColor': '#713f12',
'noteBorderColor': '#fbbf24',
'actorBkg': '#f8fafc',
'actorBorder': '#94a3b8',
'actorTextColor': '#1e293b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'sequence': {
'mirrorActors': false,
'messageAlign': 'center',
'actorMargin': 120,
'width': 160,
'noteMargin': 12
}
}}%%
sequenceDiagram
participant T as systemd-timer
participant C as borg client
participant S as borg serve
T->>C: Launch backup.sh
C->>S: SSH connection
S->>C: List of known chunks
C->>S: Send new chunks
S->>C: Write confirmation
C->>T: Exit code 0
Port table
| Port | Protocol | Purpose | Exposed externally? |
|---|---|---|---|
| 22 | TCP/SSH | Transport for borg serve on a remote repository | Only from trusted IPs |
| No dedicated port | – | A local repository works without a network | No |
Useful links:
Оставайтесь на связи
Рецепты от IT-боли. Без воды, без рекламы, без маркетинговой шелухи.
Подписаться на IT-Аптеку →




