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
@@ -89,6 +89,10 @@ Confirm the timezone guessed from your country.
*Guided, use entire disk* on the system drive, then *All files in one partition*, which gives you one big `/` plus a swap partition. Separate `/home` or `/var` partitions buy you very little here and mostly guarantee that one fills up while the others sit half empty. Pick LVM only if you already know you want snapshots or to grow volumes later. Your data disks are not touched at this stage, you'll mount them afterwards.
::warning{to="/serveex/advanced/btrfs-snapshots"}
__For advanced users:__ this is the moment to decide, not later. Picking *Manual* here instead of *Guided* lets you format the root partition as Btrfs instead of ext4, unlocking instant, near-free snapshots you can roll back to before a risky update or config change. Once this step is done and Debian is installed on ext4, switching to Btrfs isn't possible without wiping the disk and starting over. See **BTRFS snapshots** if you want to set it up, it's not a replacement for real backups either, a snapshot lives on the same disk.
::
Finish with *Finish partitioning and write changes to disk*, then confirm with *Yes*: this is the point of no return for that disk.
![Debian installer partitioning scheme, all files in one partition](/img/serveex/install/debian-install-partition.png)
@@ -97,10 +101,6 @@ Finish with *Finish partitioning and write changes to disk*, then confirm with *
__Tip:__ what actually lives on that one partition, and why `/srv/docker` is where this guide puts every stack, is covered in **folders and partitions**.
::
::tip{icon="" to="/serveex/advanced/btrfs-snapshots"}
__For advanced users:__ picking *Manual* here instead of *Guided* lets you format the root partition as Btrfs instead of ext4, unlocking instant, near-free snapshots you can roll back to before a risky update or config change. It's not a replacement for real backups, a snapshot lives on the same disk, see **BTRFS snapshots** if you want to set it up.
::
#### Mirror and surveys
Answer *No* to *Scan another installation medium?*, everything else comes from the network. For the mirror, pick any one in your country, or `deb.debian.org` which routes to a nearby one automatically, and leave the HTTP proxy field empty unless you actually have one. The popularity contest (anonymous package statistics) is yes or no, no consequence either way.
@@ -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.