Preface
Wouldn't it be very annoying if one were to lose data in a LXD instance, for some reason as trivial as an over-eager glob? Yes. Yes it was. I had a days worth of metadata that I had accumulated from a task I had automated and just as I was creating a tarball and removing the original files, an over-eager globe wiped out all of the original data and the tarball. It was not critical data but it would have been helpful to have retained it, and it's annoying that in my extremely tired state I failed to see the error in my command before committing it.
Let's look at how we can limit the damage next time.
Snapshots
Snapshots are a point in time backup of the state of an instance. If the filesystem supports snapshots (e.g. ZFS) it becomes an accounting issue. "Mark the extents as protected, jobs a good 'un." If the filesystem does not support snapshots (e.g. Ext4) then it's a case of copying the whole instance volume and storing it somewhere. The former is clearly advantageous in this case as it preserves data and storage requirements and that's what I use.
When snapshotting an instance we do not snapshot the filesystem directly (if the filesystem supports it). We ask LXD to generate the snapshot for us so that we may let it take care of all of it's internal book-keeping and administration. If we were to restore an instance by restoring the filesystem to an earlier state we may cause a conflict between the state and the metadata. So, we issue:
lxc snapshot <instance> [<snapshot name>] [flags]
to create an instance snapshot, and:
lxc restore <instance> <snapshot> [flags]
to restore it.
I name the snapshots by the current UTC time. Snapshots will appear in the output of:
lxc info <instance>
Automating Snapshots
My first course of action was to create a script which finds all of the running instances, and creates a snapshot:
#!/bin/bash
# File: /usr/local/bin/lxd-snapshot
# Date: 2025-12-17
mapfile -t < <(
lxc list status=running --format=json |
jq -r '.[].name'
)
for instance in "${MAPFILE[@]}"
do
if ! tp="$(date '+%s' --utc)"
then
echo 'Failed to establish tp' >&2
exit 1
fi
if ! now="$(date --date="@$tp" --utc "+%Y%m%d-%H%M%S")"
then
echo 'Failed to establish snapshot_name' >&2
exit 1
fi
snapshot_name="AutoLXD_${now}"
lxc snapshot "${instance}" "${snapshot_name}"
done
This was then added to a crontab which executes at quarter-past the hour, every hour.
# m h dom mon dow command
15 * * * * /usr/local/bin/lxd-snapshot
This works well, however, 24 snapshots a day, every day, will cause a maintance burden for removing old snapshots. I could create a script that parses the existing snapshots and then remove each snapshot that is older than some point in time. But, keen observer, we notice there is an "EXPIRES AT" field in the snapshots summary. Let's use that!
Automating Snapshots
In order to set a snapshot expiry date, we invoke the following commands:
lxc config set <instance>/<snapshot_name> \
--property expires_at="$(date --date="@$expires_at" --utc -Is)"
`-Is` forces date to output in iso8601 format with seconds resolution.
That's it. One straight-forward command.
So, all that is neccessary to automatically expire the instance(s) is to calculate the expiry time and then set it.
I'm keeping snapshots for 2 days. That's enough.
#!/bin/bash
# File: /usr/local/bin/lxd-snapshot
# Date: 2025-12-17
mapfile -t < <(
lxc list status=running --format=json |
jq -r '.[].name'
)
for instance in "${MAPFILE[@]}"
do
if ! tp="$(date '+%s' --utc)"
then
echo 'Failed to establish tp' >&2
exit 1
fi
if ! expires_at="$(date '+%s' --date='2 days' --utc)"
then
echo 'Failed to establish expires_at' >&2
exit 1
fi
if ! now="$(date --date="@$tp" --utc "+%Y%m%d-%H%M%S")"
then
echo 'Failed to establish snapshot_name' >&2
exit 1
fi
snapshot_name="AutoLXD_${now}"
lxc snapshot "${instance}" "${snapshot_name}" &&
lxc config set "${instance}/${snapshot_name}" \
--property \
expires_at="$(date --date="@$expires_at" --utc -Is)"
done
You'll notice that I'm prefixing these snapshots with `AutoLXD_`. I'm doing this so I can always identity which snapshots were made by this script. That's all.
This is the output of lxc info myinstance after some time having run the scheduled job.
| NAME | TAKEN AT | EXPIRES AT | STATEFUL |
|---|---|---|---|
| AutoLXD_20251217-101501 | 2025/12/17 10:15 GMT | 2025/12/19 10:15 GMT | NO |
| AutoLXD_20251217-111502 | 2025/12/17 11:15 GMT | 2025/12/19 11:15 GMT | NO |
| AutoLXD_20251217-121502 | 2025/12/17 12:15 GMT | 2025/12/19 12:15 GMT | NO |
| AutoLXD_20251217-131502 | 2025/12/17 13:15 GMT | 2025/12/19 13:15 GMT | NO |
| AutoLXD_20251217-141503 | 2025/12/17 14:15 GMT | 2025/12/19 14:15 GMT | NO |
| AutoLXD_20251217-151502 | 2025/12/17 15:15 GMT | 2025/12/19 15:15 GMT | NO |
| AutoLXD_20251217-161502 | 2025/12/17 16:15 GMT | 2025/12/19 16:15 GMT | NO |
| AutoLXD_20251217-171502 | 2025/12/17 17:15 GMT | 2025/12/19 17:15 GMT | NO |
| AutoLXD_20251217-181502 | 2025/12/17 18:15 GMT | 2025/12/19 18:15 GMT | NO |
| AutoLXD_20251217-191502 | 2025/12/17 19:15 GMT | 2025/12/19 19:15 GMT | NO |
| AutoLXD_20251217-201502 | 2025/12/17 20:15 GMT | 2025/12/19 20:15 GMT | NO |
| AutoLXD_20251217-211503 | 2025/12/17 21:15 GMT | 2025/12/19 21:15 GMT | NO |
| AutoLXD_20251217-221502 | 2025/12/17 22:15 GMT | 2025/12/19 22:15 GMT | NO |
| AutoLXD_20251217-231502 | 2025/12/17 23:15 GMT | 2025/12/19 23:15 GMT | NO |
| AutoLXD_20251218-001503 | 2025/12/18 00:15 GMT | 2025/12/20 00:15 GMT | NO |
| AutoLXD_20251218-011502 | 2025/12/18 01:15 GMT | 2025/12/20 01:15 GMT | NO |
| AutoLXD_20251218-021503 | 2025/12/18 02:15 GMT | 2025/12/20 02:15 GMT | NO |
| AutoLXD_20251218-031505 | 2025/12/18 03:15 GMT | 2025/12/20 03:15 GMT | NO |
| AutoLXD_20251218-041503 | 2025/12/18 04:15 GMT | 2025/12/20 04:15 GMT | NO |
| AutoLXD_20251218-051503 | 2025/12/18 05:15 GMT | 2025/12/20 05:15 GMT | NO |
| AutoLXD_20251218-061502 | 2025/12/18 06:15 GMT | 2025/12/20 06:15 GMT | NO |
| AutoLXD_20251218-071502 | 2025/12/18 07:15 GMT | 2025/12/20 07:15 GMT | NO |
| AutoLXD_20251218-081502 | 2025/12/18 08:15 GMT | 2025/12/20 08:15 GMT | NO |
| AutoLXD_20251218-091503 | 2025/12/18 09:15 GMT | 2025/12/20 09:15 GMT | NO |
| AutoLXD_20251218-101502 | 2025/12/18 10:15 GMT | 2025/12/20 10:15 GMT | NO |
| AutoLXD_20251218-111503 | 2025/12/18 11:15 GMT | 2025/12/20 11:15 GMT | NO |
| AutoLXD_20251218-121503 | 2025/12/18 12:15 GMT | 2025/12/20 12:15 GMT | NO |
| AutoLXD_20251218-131502 | 2025/12/18 13:15 GMT | 2025/12/20 13:15 GMT | NO |
| AutoLXD_20251218-141502 | 2025/12/18 14:15 GMT | 2025/12/20 14:15 GMT | NO |
| AutoLXD_20251218-151502 | 2025/12/18 15:15 GMT | 2025/12/20 15:15 GMT | NO |
| AutoLXD_20251218-161502 | 2025/12/18 16:15 GMT | 2025/12/20 16:15 GMT | NO |
| AutoLXD_20251218-171504 | 2025/12/18 17:15 GMT | 2025/12/20 17:15 GMT | NO |
| AutoLXD_20251218-181502 | 2025/12/18 18:15 GMT | 2025/12/20 18:15 GMT | NO |
| AutoLXD_20251218-191502 | 2025/12/18 19:15 GMT | 2025/12/20 19:15 GMT | NO |
The snapshots are indeed removed once the expiry time has been reached.
LXD Native Snapshot Automation
Why then, did I not use LXC native instance snapshot scheduling? The answer is that it does not appear to support naming of snapshots or automatic expiry. It is quicker and easier for me to write a script which makes the backups and sets the expiry, than it is to write a script which determines which snapshots lack expiry information and to then calculate and set the appropriate information.
Round-up
This "quick and dirty" solution will be effective for my needs, which are very basic at this time. It certainly doesn't facilitate a "proper" backup retention schedule with hourly, monthly, daily, weekly snapshots; but my problem doesn't require such a schedule.
Perhaps this will suit your needs as well.
Mind how you go!