PBS: back up a directory with proxmox-backup-client from a snapshot
In short. Give the client host a token with
DatastoreBackupon the tenant's namespace, putPBS_REPOSITORY,PBS_PASSWORDandPBS_FINGERPRINTin a root-only environment file, and runproxmox-backup-client backup data.pxar:<path>/.snap/<latest>with--nsand a stable--backup-idfrom a systemd timer. Read from the newest snapshot, not the live tree, so the archive is consistent while users write. Verify by listing the group and restoring one file.
The container backup does not contain the files
A tenant's container backs up green every night, and the restore drill shows the application directory empty. The bulk data lives on a CephFS subvolume bind-mounted into the container at /srv/data, and vzdump does not follow bind mounts. The guest backup covers the runtime; the files need their own job.
flowchart LR
SV["CephFS subvolume<br/>/mnt/cephfs/volumes/tenant-x/files"] --> SNAP[".snap/scheduled-...<br/>6h keep 3, daily keep 14"]
SV -- "bind mount" --> LXC["container<br/>vzdump skips this"]
SNAP -- "proxmox-backup-client backup<br/>data.pxar, nightly timer" --> PBS["PBS, ns tenant-x<br/>host/files-tenant-x"]
PBS -- "pull sync" --> REP["replica at the other site"]What you need
- PBS 4.x with a datastore and a namespace for the tenant.
- A host that mounts the CephFS and has
proxmox-backup-clientinstalled. Every PVE 9.2 node ships it, and the nodes mount CephFS already, so the job runs on one node here. - CephFS snapshots scheduled on the subvolume path, so
.snaphas something in it.jqis not needed;lsandsortare. - Names below: datastore
backups, namespacetenant-x, subvolume/mnt/cephfs/volumes/tenant-x/files, backup idfiles-tenant-x.
Steps
-
On the PBS, create a user and a token for the client, scoped to the tenant's namespace with a role that can write backups and read its own, nothing else:
proxmox-backup-manager user create files@pbs --comment "pxar archives from CephFS" proxmox-backup-manager user generate-token files@pbs node-a1 proxmox-backup-manager acl update /datastore/backups/tenant-x DatastoreBackup --auth-id 'files@pbs!node-a1' proxmox-backup-manager cert infoKeep the token secret and the fingerprint for the next step.
-
On the client host, write an environment file that only root can read:
install -d -m 0750 /etc/proxmox-backup-client install -m 0600 /dev/null /etc/proxmox-backup-client/tenant-x.envPBS_REPOSITORY=files@pbs!node-a1@10.1.5.49:backups PBS_PASSWORD=<token secret> PBS_FINGERPRINT=AA:BB:CC:...The repository address is the PBS on the storage VLAN, so the archive crosses the 10G jumbo-frame network and not management.
-
Run the backup once by hand, from the newest snapshot:
set -a; . /etc/proxmox-backup-client/tenant-x.env; set +a sub=/mnt/cephfs/volumes/tenant-x/files latest=$(ls -1 "$sub/.snap" | sort | tail -n 1) proxmox-backup-client backup "data.pxar:$sub/.snap/$latest" \ --ns tenant-x --backup-id files-tenant-x \ --skip-lost-and-found \ --exclude '**/cache/**' --exclude '**/*.tmp'The archive name before the colon is yours to choose;
data.pxaris the convention here. The backup id is stable so every night lands in the same group and deduplicates against last night.--excludetakes glob patterns relative to the archive root, and a.pxarexcludefile in the tree does the same job if the tenant wants to own the list. The snapshot directory contains the subvolume's UUID-named directory, so the archive root is one level above the data; that is fine and consistent. -
Put it in a script and a timer.
/usr/local/sbin/pbs-files-tenant-x:#!/bin/bash set -euo pipefail set -a; . /etc/proxmox-backup-client/tenant-x.env; set +a sub=/mnt/cephfs/volumes/tenant-x/files latest=$(ls -1 "$sub/.snap" | sort | tail -n 1) [ -n "$latest" ] || { echo "no snapshot in $sub/.snap" >&2; exit 1; } exec proxmox-backup-client backup "data.pxar:$sub/.snap/$latest" \ --ns tenant-x --backup-id files-tenant-x \ --skip-lost-and-found --exclude '**/cache/**'/etc/systemd/system/pbs-files-tenant-x.serviceand its timer:[Unit] Description=pxar archive of tenant-x files from the latest CephFS snapshot [Service] Type=oneshot ExecStart=/usr/local/sbin/pbs-files-tenant-x[Unit] Description=nightly pxar archive, tenant-x [Timer] OnCalendar=*-*-* 03:30:00 RandomizedDelaySec=10m Persistent=true [Install] WantedBy=timers.targetchmod 0750 /usr/local/sbin/pbs-files-tenant-x systemctl daemon-reload systemctl enable --now pbs-files-tenant-x.timer03:30 sits inside the 01:00 to 04:00 backup window and after the container's own job, so the sync at 05:00 picks up both.
-
Make sure there is always a snapshot to read. The script fails loudly if
.snapis empty, which is the right behaviour: a backup from the live tree while users write would be internally inconsistent, and a backup that silently falls back to it would hide a broken snapshot schedule.
Verify it worked
List the group and its snapshots from the client host, with the same token:
proxmox-backup-client list --ns tenant-x
proxmox-backup-client snapshot list host/files-tenant-x --ns tenant-x
Then restore one file. In the PBS GUI, open the datastore, the namespace, the snapshot, and use the file browser on data.pxar to download a single file. From the command line, restore the archive into a scratch directory and check a file you know:
proxmox-backup-client restore host/files-tenant-x/2026-07-31T03:35:12Z data.pxar /tmp/restore-test --ns tenant-x
diff /tmp/restore-test/<uuid>/some/file /mnt/cephfs/volumes/tenant-x/files/<uuid>/some/file
If the file comes back identical and the snapshot verifies clean the next morning, the job is real. Check systemctl list-timers shows the next run and journalctl -u pbs-files-tenant-x shows last night's.
Gotchas
ls | sort | tailassumes snapshot names sort by time. The names fromceph fs snap-scheduledo; hand-made ones may not. Use a date in the name if you make your own.- A
.snapdirectory is only there if snapshots are enabled on the filesystem and scheduled on the subvolume path, not the UUID directory inside it. The related post covers that trap. - The token has
DatastoreBackup, which cannot prune or delete. Pruning this group is the PBS's prune job's business, not the client's. - A first run of a large tree takes hours. Later runs are faster, and
--change-detection-mode metadataon PBS 3.2 and newer makes them faster still by skipping unchanged files. --skip-lost-and-foundis harmless on CephFS and essential on an ext4 mount; leave it in so the script works wherever it is copied.
FAQ
Why not back up from inside the container? The container would need PBS credentials, or a Ceph key and monitor access, and either is more blast radius than a tenant container should have. The node already mounts the filesystem and already has nothing a tenant can reach.
Why from a snapshot and not the live directory? Because the archive takes an hour and users write during it. A snapshot is a frozen view, so a file written halfway through the run is either wholly in the archive or wholly not. The thirty-minute recovery point is the snapshot schedule; the archive is what survives the cluster.
Can this restore a whole subvolume?
Yes, with proxmox-backup-client restore into a fresh subvolume, then a scratch container pointed at it, then a swap of the bind mount. The restore drill post walks through it.