Use Snapper instead of a custom script for BTRFS snapshots

This commit is contained in:
Djeex
2026-09-09 13:18:59 +02:00
parent a59966006b
commit c90cd781d3
4 changed files with 146 additions and 178 deletions
@@ -1,6 +1,6 @@
---
title: BTRFS Snapshots
description: Format Debian's root filesystem with BTRFS and use snapshots to instantly roll back a risky update or config change, as a local complement to Backrest's off-site 3-2-1 backups.
description: Format Debian's root filesystem with BTRFS and use Snapper to instantly roll back a risky update or config change, as a local complement to Backrest's off-site 3-2-1 backups.
---
@@ -50,118 +50,102 @@ Give the partition the rest of the disk (minus a small EFI partition if you're o
Everything else in the [installer](/serveex/core/installation#install-debian) stays the same.
## Taking a snapshot
A snapshot is created with a single command, and completes instantly no matter how much data is on the disk, since Btrfs only starts copying blocks the moment something actually changes (copy-on-write):
## Installing Snapper
Rather than juggling raw `btrfs subvolume` commands by hand, [Snapper](https://github.com/openSUSE/snapper) is a small tool that manages the whole snapshot lifecycle: creating them, storing them tidily, pruning old ones, and automatically bracketing every `apt` operation with a snapshot pair.
```bash [Terminal]
sudo mkdir -p /.snapshots
sudo btrfs subvolume snapshot -r / /.snapshots/$(date +%F_%H-%M-%S)
sudo apt install snapper
sudo snapper -c root create-config /
```
- `-r` makes it **read-only**, which is what you want for a safety net: nothing, including you by accident, can modify a snapshot after the fact.
- Storing snapshots under `/.snapshots` keeps them out of the way, and Btrfs is smart enough not to recurse into a subvolume the next time you snapshot `/` itself, so snapshots never end up containing older snapshots.
`create-config` registers a config named `root` for the `/` filesystem, and creates a dedicated `.snapshots` subvolume under it to store every snapshot, locked down to root. No `@`/`@home` subvolume split needed, this works fine on the plain single-subvolume layout the manual partitioning above just created.
Take the habit of running this before anything that could go wrong: `sudo apt full-upgrade`, editing a systemd unit, or a Docker Compose change that touches something outside `/srv/docker`.
Turn the config on by editing `/etc/default/snapper`:
::tip
✨ Give the snapshot a name that means something instead of just a timestamp, for example `/.snapshots/before-upgrade-2026-09-09`, so you know why it's there when you find it three weeks later.
::
```properties [/etc/default/snapper]
SNAPPER_CONFIGS="root"
```
## Restoring files from a snapshot
A snapshot is just a folder: everything the filesystem looked like at that instant, browsable like any other directory.
Then enable the timers that run Snapper's periodic snapshots and cleanup:
```bash [Terminal]
ls /.snapshots/2026-09-09_18-30-00/etc/ssh/
sudo systemctl enable --now snapper-timeline.timer snapper-cleanup.timer
```
To undo a mistake, copy the file (or folder) back from the snapshot over the current one:
```bash [Terminal]
sudo cp -a /.snapshots/2026-09-09_18-30-00/etc/ssh/sshd_config /etc/ssh/sshd_config
```
`-a` preserves permissions and ownership, which matters for anything under `/etc`.
::note
This covers the vast majority of real homelab accidents: a bad config, a deleted file, a package upgrade that broke one thing. Rolling back the __entire__ root filesystem (for a system that no longer boots at all) is also possible, by renaming subvolumes from a live USB, but it's a more delicate, less common operation. The [Btrfs documentation](https://btrfs.readthedocs.io/en/latest/Subvolumes.html) covers it if you ever need it, and honestly, if it comes to that, this is exactly the scenario your off-site [Backrest](/serveex/advanced/backrest) backup is for anyway.
Debian's package also ships an APT hook (`/etc/apt/apt.conf.d/80snapper`) that kicks in the moment a config is listed in `SNAPPER_CONFIGS`: from now on, every `apt install`, `apt upgrade` or `apt full-upgrade` automatically gets a snapshot right before and right after it runs, with zero extra effort on your part.
::
## Managing snapshots
## Taking a snapshot
For anything outside apt, like editing a systemd unit or a Docker Compose file, take one yourself with a description that will actually mean something later:
```bash [Terminal]
sudo btrfs subvolume list /
sudo snapper create --description "before compose change on swag"
```
Lists every subvolume on the filesystem, snapshots included, each with its own ID. Delete one you no longer need with:
List every snapshot with:
```bash [Terminal]
sudo btrfs subvolume delete /.snapshots/2026-09-09_18-30-00
sudo snapper list
```
::caution
```console [Output]
# | Type | Pre # | Date | Description
---+--------+-------+--------------------------+---------------------------------
0 | single | | | current
1 | single | | Tue 09 Sep 2026 18:30:00 | before compose change on swag
2 | pre | | Tue 09 Sep 2026 19:00:01 | apt install unattended-upgrades
3 | post | 2 | Tue 09 Sep 2026 19:00:14 | apt install unattended-upgrades
```
Snapshots are cheap, not free: the moment the live filesystem diverges from a snapshot, the old blocks stick around for as long as the snapshot references them. A pile of old snapshots on a server that changes a lot (container images, logs) can quietly eat real disk space. Check with `df -h /` and prune what you don't need anymore.
## Restoring files from a snapshot
Snapper keeps the full state of the filesystem at snapshot time under `/.snapshots/<number>/snapshot`, browsable like any other folder:
```bash [Terminal]
ls /.snapshots/1/snapshot/etc/ssh/
```
To see exactly what changed between a snapshot and the live system (`0` always means "current") before touching anything:
```bash [Terminal]
sudo snapper status 1..0
```
That prints every file created, modified or deleted since. To undo those changes automatically:
```bash [Terminal]
sudo snapper undochange 1..0
```
Or restore a single file by hand, which is often the safer, more surgical choice:
```bash [Terminal]
sudo cp -a /.snapshots/1/snapshot/etc/ssh/sshd_config /etc/ssh/sshd_config
```
::note
This covers the vast majority of real homelab accidents: a bad config, a deleted file, a package upgrade that broke one thing. Snapper can also `rollback` the entire root filesystem to an earlier snapshot (it creates a new default subvolume and boots into it on the next restart), for a system in a genuinely bad state rather than just missing one file. It's a more delicate, less common operation, worth testing once on a machine you don't mind rebooting before you actually need it for real. And if the disk itself is the problem, that's exactly the scenario your off-site [Backrest](/serveex/advanced/backrest) backup is for anyway.
::
## Automating snapshots
Rather than remembering to run the command by hand, a small script plus a systemd timer takes one automatically and prunes old ones:
## Managing and pruning snapshots
Snapshots are cheap, not free: the moment the live filesystem diverges from one, the old blocks it still references stick around. Left unchecked, months of snapshots on a server that changes a lot (container images, logs) can quietly eat real disk space.
The `snapper-cleanup.timer` enabled above already prunes automatically, based on the config at `/etc/snapper/configs/root`:
| Setting | Default | What it does |
|---------|---------|---------------|
| `TIMELINE_LIMIT_HOURLY` / `_DAILY` / `_MONTHLY` / `_YEARLY` | `10` | How many timeline snapshots of each granularity to keep |
| `TIMELINE_LIMIT_WEEKLY` | `0` | Off by default |
| `NUMBER_LIMIT` | `50` | How many manual (`single`) snapshots to keep |
Adjust these to taste, then delete one immediately by hand if you need the space back right now:
```bash [Terminal]
sudo nano /usr/local/bin/btrfs-snapshot.sh
sudo snapper delete 1
```
```bash [btrfs-snapshot.sh]
#!/bin/bash
set -e
mkdir -p /.snapshots
btrfs subvolume snapshot -r / "/.snapshots/$(date +%F_%H-%M-%S)"
Check actual disk usage with `df -h /`: the numbers above only cap how many snapshots exist, not the space a very active server can still burn through between cleanups.
# Keep only the 7 most recent snapshots
cd /.snapshots
ls -1 | sort | head -n -7 | while read -r old; do
btrfs subvolume delete "$old"
done
```
```bash [Terminal]
sudo chmod +x /usr/local/bin/btrfs-snapshot.sh
```
```bash [Terminal]
sudo nano /etc/systemd/system/btrfs-snapshot.service
```
```ini [btrfs-snapshot.service]
[Unit]
Description=Take a Btrfs root snapshot
[Service]
Type=oneshot
ExecStart=/usr/local/bin/btrfs-snapshot.sh
```
```bash [Terminal]
sudo nano /etc/systemd/system/btrfs-snapshot.timer
```
```ini [btrfs-snapshot.timer]
[Unit]
Description=Daily Btrfs root snapshot
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
```
```bash [Terminal]
sudo systemctl daemon-reload
sudo systemctl enable --now btrfs-snapshot.timer
```
Check it's scheduled with `systemctl list-timers`, and trigger one right away to test it with `sudo systemctl start btrfs-snapshot.service`.
That's it: a rolling, automatic undo button for your server, running quietly alongside the real backups Backrest is already taking off-site.
That's it: a rolling, automatic undo button for your server, quietly bracketing every update and pruning itself, running alongside the real backups Backrest is already taking off-site.