Skip to content

Backup Management

Maxime edited this page Jun 1, 2026 · 2 revisions

Backup Management

This guide covers the full backup lifecycle for QEMU VMs and LXC containers in PVE4J — from creating backups, inspecting and managing the resulting files, applying retention, restoring a guest, to driving cluster-wide scheduled backup jobs.

All backup operations follow the Proxmox VE API exactly. Long-running operations return a PveTask and support .waitForCompletion(proxmox) / .onCompletion(...).

Table of Contents

Creating a Backup (single VM)

The simplest entry point backs up one VM. The VMID is set for you.

try {
    PveTask task = proxmox.getNodes()
            .get("pve-node-01")
            .getQemu()
            .get(100)
            .backup()                 // uses Proxmox defaults
            .waitForCompletion(proxmox)
            .execute();

    System.out.println("Backup completed!");
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Backup with Options

import fr.freshperf.pve4j.entities.nodes.node.qemu.PveQemuBackupOptions;

try {
    PveQemuBackupOptions options = PveQemuBackupOptions.builder()
            .storage("DATA")                       // target storage for the archive
            .mode("snapshot")                      // "snapshot" (default), "suspend", "stop"
            .compress("zstd")                      // "0", "1", "gzip", "lzo", "zstd"
            .notesTemplate("{{guestname}} - {{node}}")
            .protected_(true)                      // protect the archive from pruning/deletion
            .pruneBackups("keep-last=3,keep-daily=7")
            .bwlimit(102400)                       // I/O limit in KiB/s (0 = unlimited)
            .build();

    PveTask task = proxmox.getNodes()
            .get("pve-node-01")
            .getQemu()
            .get(100)
            .backup(options)
            .waitForCompletion(proxmox)
            .execute();

    System.out.println("Backup completed with options!");
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

PveQemuBackupOptions exposes the full per-VM vzdump surface (storage, mode, compress, notes-template, protected, remove, bwlimit, ionice, prune-backups, performance, fleecing, pigz, zstd, lockwait, stopwait, script, tmpdir, dumpdir, quiet, stdexcludes, mailto, mailnotification, notification-mode, job-id).

Creating Backups at the Node Level (vzdump)

To back up several guests at once (a list of VMIDs, a whole pool, or all guests on the node), use the node-level vzdump facade.

import fr.freshperf.pve4j.entities.nodes.node.vzdump.PveVzdumpOptions;

try {
    PveVzdumpOptions options = PveVzdumpOptions.builder()
            .vmid("100,101,102")        // or .all(true), or .pool("production")
            .storage("DATA")
            .mode("snapshot")
            .compress("zstd")
            .build();

    PveTask task = proxmox.getNodes()
            .get("pve-node-01")
            .getVzdump()
            .backup(options)
            .waitForCompletion(proxmox)
            .execute();

    System.out.println("Node-level backup started!");
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Backup Defaults

Read the effective default vzdump parameters for a node (optionally for a given storage).

import fr.freshperf.pve4j.entities.nodes.node.vzdump.PveVzdumpDefaults;

try {
    PveVzdumpDefaults defaults = proxmox.getNodes()
            .get("pve-node-01")
            .getVzdump()
            .getDefaults("DATA")     // or getDefaults() for the default storage
            .execute();

    System.out.println("Default mode: " + defaults.getMode());
    System.out.println("Default compress: " + defaults.getCompress());
    System.out.println("Default retention: " + defaults.getPruneBackups());
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Extracting a Guest Config from an Archive

Read the guest configuration stored inside a backup archive, without restoring it. The argument is a backup volume identifier (volid).

try {
    String config = proxmox.getNodes()
            .get("pve-node-01")
            .getVzdump()
            .extractConfig("DATA:backup/vzdump-qemu-100-2026_05_31-18_21_01.vma.zst")
            .execute();

    System.out.println(config);
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Listing Backups

Backups are listed through the storage that holds them. You can list all backups, or only those belonging to a specific VM.

import fr.freshperf.pve4j.entities.nodes.node.storage.PveStorageContent;
import java.util.List;

try {
    List<PveStorageContent> backups = proxmox.getNodes()
            .get("pve-node-01")
            .getStorage()
            .get("DATA")
            .getBackups(100)        // or getBackups() for every backup on the storage
            .execute();

    for (PveStorageContent backup : backups) {
        System.out.println("Volid: " + backup.getVolid());     // pass this to restore/extractConfig
        System.out.println("Size:  " + backup.getSize() + " bytes");
        System.out.println("Notes: " + backup.getNotes());
        System.out.println("Protected: " + backup.isProtected());
        System.out.println("---");
    }
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

The volid returned here (e.g. DATA:backup/vzdump-qemu-100-...vma.zst) is exactly the value expected by restore and extractConfig.

Inspecting a Backup Volume

import fr.freshperf.pve4j.entities.nodes.node.storage.PveStorageVolume;

try {
    PveStorageVolume volume = proxmox.getNodes()
            .get("pve-node-01")
            .getStorage()
            .get("DATA")
            .getVolume("DATA:backup/vzdump-qemu-100-2026_05_31-18_21_01.vma.zst")
            .execute();

    System.out.println("Path: " + volume.getPath());
    System.out.println("Format: " + volume.getFormat());
    System.out.println("Protected: " + volume.isProtected());
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Editing Notes and Protection

Update a backup's notes and/or its protection status. Pass null to leave a value unchanged.

try {
    proxmox.getNodes()
            .get("pve-node-01")
            .getStorage()
            .get("DATA")
            .updateVolumeAttributes(
                    "DATA:backup/vzdump-qemu-100-2026_05_31-18_21_01.vma.zst",
                    "Keep until end of quarter",   // new notes
                    true)                           // protect from deletion/pruning
            .execute();

    System.out.println("Backup attributes updated!");
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Deleting a Backup

try {
    PveTask task = proxmox.getNodes()
            .get("pve-node-01")
            .getStorage()
            .get("DATA")
            .deleteVolume("DATA:backup/vzdump-qemu-100-2026_05_31-18_21_01.vma.zst")
            .waitForCompletion(proxmox)
            .execute();

    System.out.println("Backup deleted!");
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Protected backups cannot be deleted until you clear their protection with updateVolumeAttributes.

Retention / Pruning

Preview which backups a prune would keep or remove (dry-run), then prune for real. Both use the same options; when no retention string is given, the storage configuration is used.

import fr.freshperf.pve4j.entities.nodes.node.storage.PvePruneBackupsOptions;
import fr.freshperf.pve4j.entities.nodes.node.storage.PvePruneBackupEntry;
import java.util.List;

try {
    PvePruneBackupsOptions options = PvePruneBackupsOptions.builder()
            .pruneBackups("keep-last=3,keep-weekly=4")
            .type("qemu")
            .vmid(100)
            .build();

    var storage = proxmox.getNodes().get("pve-node-01").getStorage().get("DATA");

    // 1. Dry-run preview
    List<PvePruneBackupEntry> preview = storage.getPruneBackups(options).execute();
    for (PvePruneBackupEntry entry : preview) {
        System.out.println(entry.getVolid() + " -> " + entry.getMark()); // keep / remove / protected / renamed
    }

    // 2. Actually prune
    PveTask task = storage.pruneBackups(options)
            .waitForCompletion(proxmox)
            .execute();

    System.out.println("Prune completed!");
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Restoring a VM

Restore a VM from a backup archive. The archive is a Proxmox volume identifier in the form <storage>:backup/<filename>not a bare filename or a path. Passing a bare filename triggers a Proxmox error: "Only root can pass arbitrary filesystem paths".

import fr.freshperf.pve4j.entities.nodes.node.qemu.PveQemuRestoreOptions;

try {
    PveQemuRestoreOptions options = PveQemuRestoreOptions.builder()
            // Either build the volid from storage + file name...
            .archive("DATA", "vzdump-qemu-100-2026_05_31-18_21_01.vma.zst")
            // ...or pass the full volid directly:
            // .archive("DATA:backup/vzdump-qemu-100-2026_05_31-18_21_01.vma.zst")
            .force(true)            // overwrite an existing VM with the same VMID
            .storage("local-lvm")   // target storage for the restored disks (optional)
            .start(false)           // start after restore (optional)
            .build();

    PveTask task = proxmox.getNodes()
            .get("pve-node-01")
            .getQemu()
            .restore(100, options)
            .waitForCompletion(proxmox)
            .execute();

    System.out.println("VM restored!");
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

The most reliable way to obtain a valid archive value is to take the volid straight from Listing Backups:

String volid = proxmox.getNodes().get("pve-node-01").getStorage().get("DATA")
        .getBackups(100).execute().get(0).getVolid();

proxmox.getNodes().get("pve-node-01").getQemu()
        .restore(100, PveQemuRestoreOptions.builder().archive(volid).force(true).build())
        .waitForCompletion(proxmox)
        .execute();

Other restore options: unique(true) (random MAC addresses), liveRestore(true), bwlimit(int), pool(String), name(String), description(String).

Restoring a Container (LXC)

Restoring an LXC container works just like a VM restore, but through the getLxc() facade and PveLxcRestoreOptions. The archive is the backup volume identifier (<storage>:backup/<filename>) of an LXC backup (vzdump-lxc-...tar.zst), exactly as returned by Listing Backups.

import fr.freshperf.pve4j.entities.nodes.node.lxc.PveLxcRestoreOptions;

try {
    PveLxcRestoreOptions options = PveLxcRestoreOptions.builder()
            // Either build the volid from storage + file name...
            .archive("DATA", "vzdump-lxc-200-2026_05_31-18_21_01.tar.zst")
            // ...or pass the full volid directly:
            // .archive("DATA:backup/vzdump-lxc-200-2026_05_31-18_21_01.tar.zst")
            .force(true)            // overwrite an existing container with the same VMID
            .storage("local-lvm")   // target storage for the restored rootfs (optional)
            .unique(true)           // assign a random MAC address (optional)
            .start(false)           // start after restore (optional)
            .build();

    PveTask task = proxmox.getNodes()
            .get("pve-node-01")
            .getLxc()
            .restore(200, options)
            .waitForCompletion(proxmox)
            .execute();

    System.out.println("Container restored!");
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Other restore options: bwlimit(int), pool(String), hostname(String), password(String), description(String).

Scheduled Backup Jobs

Backup jobs are cluster-wide schedules that periodically back up the selected guests. They are accessed via proxmox.getCluster().getBackup().

List and Read Jobs

import fr.freshperf.pve4j.entities.cluster.backup.PveBackupJob;
import java.util.List;

try {
    List<PveBackupJob> jobs = proxmox.getCluster().getBackup().getJobs().execute();
    for (PveBackupJob job : jobs) {
        System.out.printf("%s | enabled=%s | schedule=%s | storage=%s%n",
                job.getId(), job.isEnabled(), job.getSchedule(), job.getStorage());
    }

    PveBackupJob job = proxmox.getCluster().getBackup().getJob(jobs.get(0).getId()).execute();
    System.out.println("Next run (unix): " + job.getNextRun());
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Create a Job

import fr.freshperf.pve4j.entities.cluster.backup.PveBackupJobCreateOptions;

try {
    PveBackupJobCreateOptions options = PveBackupJobCreateOptions.builder()
            .schedule("mon..fri 02:00")     // systemd calendar event subset
            .storage("DATA")
            .vmid("100,101")                // or .all(true) / .pool("production")
            .mode("snapshot")
            .compress("zstd")
            .enabled(true)
            .comment("Weeknight backups")
            .pruneBackups("keep-daily=7,keep-weekly=4")
            .build();

    proxmox.getCluster().getBackup().createJob(options).execute();
    System.out.println("Backup job created!");
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Update / Delete a Job

import fr.freshperf.pve4j.entities.cluster.backup.PveBackupJobUpdateOptions;
import java.util.List;

try {
    // Update some settings; use delete(...) to reset settings back to their defaults.
    PveBackupJobUpdateOptions update = PveBackupJobUpdateOptions.builder()
            .enabled(false)
            .delete(List.of("comment"))
            .build();

    proxmox.getCluster().getBackup().updateJob("backup-xxxxxxxx-xxxx", update).execute();

    // Delete a job
    proxmox.getCluster().getBackup().deleteJob("backup-xxxxxxxx-xxxx").execute();
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

Inspect Coverage

import fr.freshperf.pve4j.entities.cluster.backup.PveBackupIncludedVolumes;
import fr.freshperf.pve4j.entities.cluster.backup.PveBackupGuest;
import java.util.List;

try {
    // Which guests/volumes a given job covers
    PveBackupIncludedVolumes included = proxmox.getCluster()
            .getBackup()
            .getIncludedVolumes("backup-xxxxxxxx-xxxx")
            .execute();

    for (PveBackupIncludedVolumes.Guest guest : included.getChildren()) {
        System.out.println("Guest " + guest.getId() + " (" + guest.getType() + ")");
        for (PveBackupIncludedVolumes.Volume volume : guest.getChildren()) {
            System.out.printf("  %s included=%s (%s)%n",
                    volume.getId(), volume.isIncluded(), volume.getReason());
        }
    }

    // Guests not covered by ANY backup job
    List<PveBackupGuest> uncovered = proxmox.getCluster()
            .getBackup()
            .getGuestsNotBackedUp()
            .execute();

    for (PveBackupGuest guest : uncovered) {
        System.out.printf("Not backed up: %d (%s) %s%n",
                guest.getVmid(), guest.getType(), guest.getName());
    }
} catch (ProxmoxAPIError | InterruptedException e) {
    e.printStackTrace();
}

The backup-info directory index itself is available via proxmox.getCluster().getBackup().getBackupInfo(), which returns the names of the available sub-resources; the data is exposed by the methods above.

Next Steps

Clone this wiki locally