CephFS backups¶
all the swarm's stack data lives on one cephFS, passed to the docker VMs with virtioFS. VM backups do not include it, so it gets its own backup to PBS.
ceph takes a snapshot of the cephFS every hour on the hour. a small container on the proxmox cluster backs up the newest snapshot to PBS at quarter past, and proxmox HA restarts the container on another node when its node goes down, so the backup doesn't depend on any one node.
| what | where |
|---|---|
| snapshots | ceph's snap_schedule, fs docker, path /, hourly, keeping 24 hourly and 7 daily |
| backup | a systemd timer in the container, hourly at :15 |
| source | the host's /mnt/pve/docker-cephFS, bind mounted read-only at /mnt/cephfs |
| destination | datastore mnt-pbs, namespace Files, group host/cephfs |
| credential | the API token cephFS@pbs!cephFS, with DatastoreBackup only |
a run takes about 15 seconds. the first one had nothing to compare against, so it read all 10 GiB and took two minutes.
1. the snapshots¶
on any node:
ceph mgr module enable snap_schedule
until ceph mgr module ls | grep -Eq '^snap_schedule +on'; do sleep 2; done
ceph fs snap-schedule add / 1h --fs docker
ceph fs snap-schedule retention add / 24h7d --fs docker
ceph fs snap-schedule status / --fs docker
- the ceph manager takes the snapshots, so no particular node has to be up
- enabling a module restarts the manager, and
snap-schedulefails withnot enabled/loadeduntil it has loaded the module. theuntilline waits --fs dockernames the filesystem. i have three, and without it the module uses the first in the fs map, which isn't this one- 31 snapshots at a time is under the module's limit of 50 per directory, and
mds_max_snaps_per_dir's default of 100 - the snapshots are a quick local undo too: copy a file back out of
.snap. they are on the same cluster, so they are not the backup
ceph documents its snapshots as asynchronous, with buffered data flushed lazily. that is fine for config files but not for a database, see databases.
2. the container¶
a privileged debian 13 container, on vDisks so that HA can start it on any
node:
pct create <ctid> ISOs-Templates:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
--hostname cephfs-backup \
--ostype debian \
--unprivileged 0 \
--features nesting=1 \
--cores 2 \
--memory 1024 \
--swap 512 \
--rootfs vDisks:8 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto \
--mp0 /mnt/pve/docker-cephFS,mp=/mnt/cephfs,ro=1,shared=1 \
--hookscript ISOs-Templates:snippets/check-donotdelete-hook.sh \
--timezone America/Los_Angeles \
--onboot 1
ha-manager add ct:<ctid> --state started
- privileged, because files on cephFS belong to lots of different uids. an
unprivileged container shifts uids, so through a bind mount files owned by
uids outside its mapping show up as
nobody, and anything not world-readable is off limits - privileged means root in the container is root on the host. it runs nothing but the backup, and that is still better contained than a cron job on the node itself
ro=1stops the container changing the data it backs upshared=1tells proxmox the path exists on every node, which lets the container movenesting=1lets debian 13's systemd set up its own mounts and service namespaces. without it the container boots with its network down- the template has to be the
amd64build.pveam availablelists anarm64one too, which won't start on these nodes - the container has no ceph client and no key of its own. ceph's own network is
the thunderbolt mesh, and a guest reaches that through
vmbr100, whose subnet is different on each node, so a container that moves would lose its route - a container doesn't live-migrate. HA stops it on one node and starts it on another
3. the start guard¶
a bind mount is set up when the container starts. if cephFS isn't mounted on the host at that moment, the container gets the empty directory under the mount point, and keeps it after cephFS mounts. the container has the docker VMs' hookscript, so it refuses to start instead.
the script checks again before each run. it stops if .snap is missing, if
the newest snapshot is more than two hours old or dated more than a minute in
the future, or if the snapshot has no .donotdelete marker.
4. the backup¶
proxmox-backup-client comes from Proxmox's client repository. the script, the
timer and its unit go in the container, and the token goes in
/root/.pbs-cephfs-token, mode 600:
pbs-client.sources: Proxmox's client-only repository, for debian 13, 5 lines
cephfs-backup.sh: the backup, run by the timer, 102 lines, 6 notes
each in the code opens a note on that line. download cephfs-backup.sh
- the newest snapshot may be at most two hours old. older means the schedule has stopped, and backing up the same snapshot every hour would hide that.
- nor may it be dated more than a minute ahead. a snapshot from a clock that jumped forward would otherwise stay the newest, and hide every later one while the same state was backed up.
- an exit that has not reported yet reports a failure to gatus, including one
set -ecauses, such asproxmox-backup-clientfailing. - snapshot names carry the ceph manager's local time and zone, such as
scheduled-2026-09-25-23_00_00_UTC, so the loop compares them as times. in text order, the hour that repeats when DST ends sorts wrong. - portainer's database is checked in the snapshot with
bbolt checkbefore the backup. a copy that fails is still backed up, and the run then reports a failure, see databases. --backup-id cephfs, above, keeps every run in one group,host/cephfs, whichever node the container is on. the marker sits here because a line that ends in\must keep it last.
cephfs-backup.service: the unit the timer starts, 10 lines
cephfs-backup.timer: hourly at quarter past, 9 lines, 1 note
each in the code opens a note on that line. download cephfs-backup.timer
- a run missed while the container was stopped or moving between nodes runs as soon as it is back.
the script checks portainer's database in the snapshot with bbolt check
before backing it up, see databases.
debian has no bbolt command, so build it in the container from source at a
pinned version, then remove go:
apt-get -y install golang-go
env HOME=/root GOBIN=/usr/local/bin go install go.etcd.io/bbolt/cmd/bbolt@v1.4.3
apt-get -y purge golang-go golang-1.24-go golang-1.24-src golang-src
apt-get -y autoremove --purge
rm -rf /root/go /root/.cache/go-build
DatastoreBackupcan back up and restore the backups its token owns, but not prune or delete them, so a compromised container cannot wipe the history. retention is the prune job on the PBS side--change-detection-mode=metadatacompares file metadata with the previous backup instead of re-reading everything--ns Fileskeeps these apart from the VM backups in the same datastoreproxmox-backup-clientreads only the first line ofPBS_PASSWORD_FILE, so the newline at the end of the file is fine
checking a restore¶
a backup that ran is not a backup that restores. verify-restore, in the
container, restores one top-level directory from the newest backup into
/var/tmp and compares it with the snapshot that backup read: the contents,
and the owner, group and mode of every entry. i run it before relying on a
change to how the backup runs:
pct exec <ctid> -- /usr/local/sbin/verify-restore
pct exec <ctid> -- /usr/local/sbin/verify-restore <directory>
- not
/tmp: on debian 13 that's a tmpfs, which counts against the container's memory, so restoring a directory bigger than that gets the restore killed./var/tmpis on the container's disk, and the script stops first if it lacks room
verify-restore.sh: restores one directory from the newest backup and compares it with the snapshot, 61 lines
still to do¶
- an alert when it stops. the script reports each run to a gatus external endpoint, and gatus alerts when no report arrives within two hours. the endpoint and its token don't exist yet, so for now the script logs that and backs up anyway
- the backups hold portainer's database, which has credentials in it. the copy that leaves the box is encrypted, see backups. the datastore on truenas isn't
- putting a folder back on cephFS from PBS. the backup container mounts cephFS read-only, so a restore has to land somewhere else and be copied back
databases¶
a snapshot can catch a database's files mid-write, so each database also gets a copy that is consistent on its own. which copy, and how to restore each, is on databases.