PBS: back up a directory with proxmox-backup-client from a snapshot

In short. Give the client host a token with DatastoreBackup on the tenant's namespace, put PBS_REPOSITORY, PBS_PASSWORD and PBS_FINGERPRINT in a root-only environment file, and run proxmox-backup-client backup data.pxar:<path>/.snap/<latest> with --ns and a stable --backup-id from 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

Steps

  1. 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 info
    

    Keep the token secret and the fingerprint for the next step.

  2. 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.env
    
    PBS_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.

  3. 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.pxar is the convention here. The backup id is stable so every night lands in the same group and deduplicates against last night. --exclude takes glob patterns relative to the archive root, and a .pxarexclude file 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.

  4. 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.service and 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.target
    
    chmod 0750 /usr/local/sbin/pbs-files-tenant-x
    systemctl daemon-reload
    systemctl enable --now pbs-files-tenant-x.timer
    

    03: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.

  5. Make sure there is always a snapshot to read. The script fails loudly if .snap is 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

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.

Related