Backups: vzdump, Proxmox Backup Server y la estrategia 3-2-1

Por: Artiko
proxmoxproxmox-vebackupvzdumppbsrestoreretencion3-2-1sysadmin

Backups: vzdump, Proxmox Backup Server y la estrategia 3-2-1

Tienes el cluster montado, el storage elegido y quizá hasta Ceph corriendo. Todo eso protege contra el fallo de una pieza de hardware. Nada de eso protege contra rm -rf a las tres de la madrugada, contra un ransomware que cifra los discos desde dentro de una VM, contra un apt full-upgrade que rompe el arranque de una máquina crítica, ni contra un incendio en la sala donde vive el rack. Redundancia no es backup, y la diferencia se paga muy cara el día que se nota.

Este capítulo cubre la cadena completa: cómo funciona vzdump por dentro, qué hace exactamente cada modo de backup, cómo se programan los jobs y con qué sintaxis, por qué la retención nunca borra lo que la gente espera que borre, y todo Proxmox Backup Server: el chunk store, la deduplicación, la diferencia entre prune y garbage collection, verify, sync, cifrado en cliente, cinta LTO y el backend S3 que en PBS 4.2 dejó de ser preview. Al final tendrás una estrategia 3-2-1 concreta, con comandos, para homelab y para PyME, más un checklist para comprobar trimestralmente que funciona.

Las referencias de versión son Proxmox VE 9.2 sobre Debian 13 Trixie y Proxmox Backup Server 4.2 (29 de abril de 2026, también sobre Debian 13, kernel 7.0, ZFS 2.4.1, escrito en Rust). PVE escucha en HTTPS 8006, PBS en HTTPS 8007.

vzdump: donde vive, quien lo ejecuta y como se invoca

vzdump es la única herramienta de backup de PVE, y sirve tanto para VMs QEMU como para contenedores LXC. No hay dos subsistemas: hay uno con dos ramas internas.

ElementoRuta o endpoint
Binario/usr/bin/vzdump
Configuración global/etc/vzdump.conf
Jobs programados/etc/pve/jobs.cfg
Demonio que ejecuta los jobspvescheduler
Ejecutar un backup por APIPOST /api2/json/nodes/{node}/vzdump
CRUD de jobs por API/api2/json/cluster/backup

La sintaxis es mínima:

vzdump help
vzdump 777                                      # backup del guest 777 con los defaults
vzdump 777 --storage pbs01 --mode snapshot
vzdump --all --exclude 900,901,9000 --storage pbs01

pvescheduler sustituyó al viejo /etc/cron.d/vzdump de PVE 6.x y evalúa los schedules una vez por minuto. Ese detalle importa: aunque escribas segundos en un evento de calendario, la resolución real es de un minuto.

Modos de backup: la VM y el contenedor no juegan al mismo juego

Hay tres modos, stop, suspend y snapshot, y el default es snapshot en ambos tipos de guest. Pero el mecanismo detrás del mismo nombre es distinto según hables de una VM o de un contenedor, y de ahí sale la mitad de los fallos de backup que verás.

Para VMs QEMU:

ModoDowntimeConsistenciaMecanismo
stopAltoMáximaApagado ordenado del guest. La VM vuelve a arrancar en cuanto el backup empieza a copiar, no cuando termina
suspendMedio-altoNo necesariamente mejor que snapshotSuspende la VM y luego llama internamente al modo snapshot. Existe solo por compatibilidad
snapshot (default)MínimoBuena si hay QEMU Guest AgentLive backup de QEMU. Con el agente activo emite guest-fsfreeze-freeze y guest-fsfreeze-thaw

El dato que cambia el diseño de todo lo demás es este, literal de la documentación: “PVE live backup provides snapshot-like semantics on any storage type. It does not require that the underlying storage supports snapshots”. El backup en modo snapshot de una VM no depende de que el storage tenga snapshots. Puedes tener los discos en un dir sobre NFS y funcionará.

Efecto secundario que confunde: como el backup lo hace un proceso QEMU en segundo plano, una VM apagada aparecerá como “running” durante un rato mientras se leen sus discos. La VM no arranca; solo se leen sus imágenes.

Para que el freeze del filesystem se emita hace falta el agente configurado y corriendo, con agent enabled=1 y freeze-fs=1 (el default de freeze-fs es 1). Repasa la instalación del agente en el capítulo 3 y el caso particular de Windows en el capítulo 4: hay una interacción conocida entre guest-fsfreeze-freeze y algunas versiones de SQL Server que rompe la cadena de backups diferenciales.

Para contenedores LXC:

ModoDowntimeRequisitoEspacio extra
stopAlto, todo el backupNingunoNo
suspendMínimoEspacio en --tmpdirSí, una copia completa
snapshot (default)Mínimo, un freeze breveTodos los volúmenes respaldados en storage con snapshotsEl del snapshot

El modo suspend de un contenedor hace rsync de los datos a un directorio temporal, suspende el CT, hace un segundo rsync de los deltas, lo reanuda y archiva. El modo snapshot suspende brevemente para asegurar consistencia, crea un snapshot de los volúmenes, archiva el snapshot en un tar y lo borra.

Gotcha: el modo snapshot de un CT exige que todos los volúmenes que entran al backup vivan en un storage con soporte de snapshots (ZFS, LVM-thin, RBD, btrfs). Un solo mount point en un storage de tipo dir hace fallar el job entero con unable to create snapshot ... storage does not support snapshots. Las dos salidas son mode=suspend o excluir ese mount point con backup=0. Los tipos de storage y qué soporta cada uno están en el capítulo 7.

Otra asimetría: live restore no existe para contenedores. La documentación lo limita explícitamente a backups de VM en PBS.

Todas las opciones de CLI y sus defaults reales

Esta tabla es de vzdump.1 en PVE 9.2. Conocer los defaults evita la mitad de las sorpresas.

OpciónTipoDefaultDescripción
--allboolean0Respalda todos los guests conocidos del host
--bwlimitinteger0Límite de I/O en KiB/s. 0 es sin límite
--compress0|1|gzip|lzo|zstd0Algoritmo de compresión
--dumpdirstringDirectorio destino, excluyente con --storage
--excludestringExcluye guests concretos, implica --all
--exclude-patharrayExcluye ficheros o directorios con globs de shell
--fleecing[enabled=<1|0>][,storage=<ID>]Backup fleecing, solo VM
--ioniceinteger 0-87Prioridad de I/O con el scheduler BFQ. 8 es clase idle
--job-idstringID del job, se guarda en metadatos
--lockwaitinteger180Espera máxima por el lock global, en minutos
--mailnotificationalways|failurealwaysDeprecado
--mailtostringDeprecado
--modesnapshot|stop|suspendsnapshotModo de backup
--nodestringSolo se ejecuta si corre en ese nodo
--notes-templatestringPlantilla de las notas del backup
--notification-modeauto|legacy-sendmail|notification-systemautoSistema de notificación
--pbs-change-detection-modelegacy|data|metadataDetección de cambios en CT hacia PBS
--performance[max-workers=<int>][,pbs-entries-max=<int>]Ajustes de rendimiento
--pigzinteger0Usa pigz en vez de gzip cuando es mayor que 0
--poolstringRespalda todos los guests de un pool
--protectedbooleanMarca el backup como protegido
--prune-backupskeep-*keep-all=1Política de retención
--quietboolean0Modo silencioso
--removeboolean1Purga backups viejos según prune-backups
--scriptstringHook script
--stdexcludesboolean1Excluye ficheros temporales y logs
--stdoutbooleanEscribe el tar a stdout
--stopboolean0Detiene los jobs de backup en curso en este host
--stopwaitinteger10Espera máxima al apagado del guest, en minutos
--storagestorage IDStorage destino
--tmpdirstringDirectorio de ficheros temporales
--zstdinteger1Hilos de zstd. 0 significa la mitad de los cores

Dos defaults merecen atención especial. Uno. prune-backups es keep-all=1: si no configuras retención en ningún sitio, no se borra nada y el storage se llena. Dos. remove es 1, así que en cuanto defines una política de retención, el purgado ocurre al terminar cada ejecución sin que tengas que pedirlo.

--mailto y --mailnotification están deprecados desde PVE 8.x en favor del sistema de notificaciones. Siguen funcionando, pero el camino nuevo es el de la sección de notificaciones más abajo.

/etc/vzdump.conf y el orden de precedencia que sorprende a todos

/etc/vzdump.conf usa formato clave: valor, una por línea. Las líneas en blanco y las que empiezan por # se ignoran.

# /etc/vzdump.conf
tmpdir: /mnt/fast_local_disk
storage: my_backup_storage
mode: snapshot
bwlimit: 10000
ionice: 7
zstd: 4
stdexcludes: 1
prune-backups: keep-last=3,keep-daily=13,keep-weekly=8,keep-monthly=11,keep-yearly=9
script: /usr/local/bin/vzdump-hook-script.pl
notes-template: {{guestname}} - {{node}} - {{vmid}}

Las claves aceptadas son las mismas de la CLI: bwlimit, compress, dumpdir, exclude-path, fleecing, ionice, lockwait, mailnotification (deprecada), mailto (deprecada), mode, notes-template, pbs-change-detection-mode, performance, pigz, prune-backups, protected, remove, script, stdexcludes, stopwait, storage, tmpdir y zstd.

El orden de precedencia, de mayor a menor:

  1. Valores definidos en el job de /etc/pve/jobs.cfg.
  2. Flags pasados en la línea de comandos.
  3. /etc/vzdump.conf.
  4. Defaults del código.

Advertencia: un storage: en /etc/vzdump.conf secuestra los backups manuales. Escribes vzdump 101 esperando que vaya a local y acaba en el storage del fichero de configuración. Si te pasa, revisa ese archivo antes que ninguna otra cosa.

Compresion zstd, gzip y lzo, y por que no aplica a PBS

AlgoritmoExtensiónMultihiloComentario
zstd.zstSí, con --zstd NEl más rápido y el recomendado. Default de facto en los jobs nuevos de la GUI
gzip.gzSolo vía pigz con --pigz NMás universal, más lento
lzo.lzoNoLegado. Muy rápido, ratio pobre
0sin extensiónSin comprimir
vzdump 101 --storage backups --compress zstd --zstd 4     # 4 hilos explicitos
vzdump 101 --storage backups --compress zstd --zstd 0     # la mitad de los cores del host
vzdump 101 --storage backups --compress gzip --pigz 4

Gotcha: la compresión de vzdump no aplica cuando el destino es un storage de tipo pbs. PBS comprime cada chunk con zstd por su cuenta del lado cliente, y el flag --compress simplemente se ignora. Poner compress zstd en un job que va a PBS no está mal, no hace nada.

Backup fleecing: que problema resuelve y que storage necesita

Cuando arranca un backup en modo snapshot, QEMU instala un filtro copy-before-write en su capa de bloques. Si el guest escribe sobre un bloque que el backup todavía no ha copiado, QEMU tiene que mandar primero el bloque antiguo al destino, y la escritura del guest se bloquea hasta que el destino responde. Con un PBS al otro lado de una WAN o un NFS lento, el I/O del guest queda atado a la latencia del destino.

El fleecing rompe esa dependencia: el bloque antiguo se vuelca a una imagen local y rápida, el guest recibe el ack de inmediato, y el job de backup lee de esa imagen y sube al destino a su ritmo.

sequenceDiagram
    participant G as Guest
    participant Q as QEMU
    participant F as Imagen de fleecing local
    participant T as Destino de backup

    Note over G,T: Sin fleecing
    G->>Q: Escribe sobre un bloque aun no copiado
    Q->>T: copy-before-write bloqueante
    T-->>Q: ack lento
    Q-->>G: ack tardio y el guest esperando

    Note over G,T: Con fleecing
    G->>Q: Escribe sobre un bloque aun no copiado
    Q->>F: Vuelca el bloque antiguo en local
    F-->>Q: ack rapido
    Q-->>G: ack inmediato
    Q->>T: El job lee de F y sube en segundo plano

Activación por CLI y por configuración:

vzdump 123 --fleecing enabled=1,storage=local-lvm
# /etc/vzdump.conf
fleecing: enabled=1,storage=local-lvm

En la GUI está en la pestaña Advanced al editar un backup job.

Requisitos del storage de fleecing, según la documentación:

  • Almacenamiento local y rápido.
  • Con thin provisioning y soporte de discard.
  • Candidatos válidos: LVM-thin, RBD y ZFS con la opción sparse.
  • Idealmente distinto del storage de los discos originales y del destino del backup.
  • La imagen de fleecing se crea en qcow2 automáticamente y se destruye al terminar la tarea.
  • En storage thin, esa imagen puede llegar a crecer hasta el tamaño de la imagen original. Dimensiona con eso en mente.

Solo aplica a VMs; los contenedores no lo usan.

Gotcha: apuntar el fleecing a un dir sobre disco lento o a un NFS empeora las cosas, porque duplicas escrituras contra medios lentos. Si el fleecing te va peor que sin él, ese es casi siempre el motivo.

Nombres de fichero, notas y backups protegidos

En storages de fichero (no PBS) los nombres codifican tipo, VMID y timestamp:

vzdump-qemu-101-2026_08_09-03_00_12.vma.zst
vzdump-lxc-105-2009_10_09-11_04_43.tar
SegmentoEjemploSignificado
1vzdumpPrefijo fijo
2qemuTipo de guest: qemu, lxc u openvz legado
3101VMID
42026_08_09-03_00_12Timestamp YYYY_MM_DD-HH_MM_SS
5.vmaContenedor: .vma para VM, .tar para CT
6.zstExtensión de compresión

Junto al archivo aparecen tres ficheros paralelos con el mismo nombre base: .log con la salida de la tarea, .notes con las notas y .protected como marca de protección.

Las notas se generan con --notes-template y admiten cuatro variables: {{cluster}}, {{guestname}}, {{node}} y {{vmid}}.

vzdump 101 --storage pbs01 \
  --notes-template '{{guestname}} ({{vmid}}) @ {{node}} - cluster {{cluster}}'

Gotcha: por API y CLI las notas deben ir en una sola línea. Los saltos de línea se escapan como \n literal y la barra invertida como \\.

--protected 1 marca el backup como protegido: prune y retención lo ignoran por completo. Es una marca lógica de PVE y PBS, no del filesystem: quien tenga permiso de escritura sobre el fichero puede borrarlo igual. La opción de storage max-protected-backups limita cuántos protegidos por guest se permiten; -1 es ilimitado, y el default es ilimitado para quien tenga Datastore.Allocate y 5 para el resto.

pvesm set local --max-protected-backups 3

Excluir discos, mount points y rutas

Las reglas de exclusión no son iguales en VM y en CT, y esa asimetría cuesta espacio y tiempo de restauración.

En contenedores solo el rootfs entra al backup por defecto. Cada mount point adicional necesita backup=1 explícito (la GUI lo marca por defecto al crear uno nuevo). Los bind mounts y los device mounts nunca se respaldan, porque su contenido está fuera de la librería de storage de PVE.

# /etc/pve/lxc/105.conf
rootfs: local-lvm:vm-105-disk-0,size=8G
mp0: local-lvm:vm-105-disk-1,mp=/data,size=100G,backup=1
mp1: local-lvm:vm-105-disk-2,mp=/scratch,size=500G,backup=0

En VMs ocurre lo contrario: todos los discos entran, y se excluye uno con backup=0.

# /etc/pve/qemu-server/101.conf
scsi0: local-lvm:vm-101-disk-0,size=32G
scsi1: local-lvm:vm-101-disk-1,size=1T,backup=0,discard=on
qm set 101 --scsi1 local-lvm:vm-101-disk-1,backup=0

Las exclusiones estándar de --stdexcludes (default 1) son estas tres, y solo se aplican a contenedores en modo suspend:

/tmp/*
/var/tmp/*
/var/run/*pid

Se desactivan con --stdexcludes 0.

Las exclusiones manuales usan --exclude-path, aceptan globs de shell y la opción es repetible:

vzdump 777 --exclude-path /tmp/ --exclude-path '/var/foo*' --exclude-path '/var/lib/docker/**'

Reglas de match:

  • Un patrón que empieza por / se ancla a la raíz del guest.
  • Un patrón sin / inicial hace match relativo en cualquier subdirectorio.
  • En backups a PBS y en modo suspend, un patrón terminado en / hace match con directorios pero no con ficheros.

Los detalles de mount points, bind mounts y su relación con contenedores unprivileged están en el capítulo 6.

Hook scripts: las once fases y el entorno vacio

Se activan con --script /ruta/al/script o con script: /ruta en /etc/vzdump.conf. El ejemplo oficial se distribuye en el repositorio pve-manager como vzdump-hook-script.pl.

La invocación es script <phase> en las fases de job y script <phase> <mode> <vmid> en las fases de guest. Las once fases, en el orden en que se disparan:

FaseÁmbitoCuándo
job-initjobAntes de nada, antes incluso de activar los storages
job-startjobEl job arranca de verdad
backup-startguestEmpieza el backup de un guest
pre-stopguestJusto antes de parar o suspender el guest
pre-restartguestJusto antes de reanudar o arrancar el guest
post-restartguestEl guest ya está online otra vez
backup-endguestArchivo creado con éxito. Aquí TARGET es válido
backup-abortguestEl backup de ese guest falló
log-endguestLog escrito. Aquí LOGFILE es válido
job-endjobJob terminado con éxito
job-abortjobJob abortado

Variables de entorno disponibles:

VariableDisponible enContenido
STOREIDcuando hay --storageID del storage destino
DUMPDIRcuando hay --dumpdirDirectorio destino
VMTYPEfases de guestqemu o lxc
HOSTNAMEfases de guestNombre del guest
TARGETbackup-end y posterioresRuta del fichero de backup resultante
LOGFILElog-endRuta del fichero de log

Gotcha verificado en el código de PVE/VZDump.pm: el sub que lanza el hook empieza con local %ENV;, lo que vacía el entorno antes de poblar esas variables. Tu script corre sin PATH, sin HOME y sin LANG. Si llamas a curl o ssh por nombre, no los encuentra. Exporta un PATH en la primera línea o usa rutas absolutas a todos los binarios.

Segundo gotcha: un código de salida distinto de 0 en job-init, job-start, backup-start o pre-stop aborta el trabajo. Por eso las acciones no críticas llevan || true.

#!/bin/bash
# /usr/local/bin/vzdump-hook.sh   (chmod +x)
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

PHASE="$1"
MODE="${2:-}"
VMID="${3:-}"

log() { /usr/bin/logger -t vzdump-hook "$*"; }

case "$PHASE" in
  job-init)
    log "job iniciando; despertando el NAS"
    /usr/bin/etherwake -i vmbr0 AA:BB:CC:DD:EE:FF || true
    /bin/sleep 60
    ;;
  job-start)
    log "job arrancado, storage=${STOREID:-n/a} dumpdir=${DUMPDIR:-n/a}"
    ;;
  backup-start)
    log "backup de ${VMTYPE} ${VMID} (${HOSTNAME}) en modo ${MODE}"
    ;;
  pre-stop)
    [ "$VMID" = "110" ] && /usr/sbin/pct exec 110 -- /usr/bin/systemctl stop postgresql
    ;;
  post-restart)
    [ "$VMID" = "110" ] && /usr/sbin/pct exec 110 -- /usr/bin/systemctl start postgresql
    ;;
  backup-end)
    log "backup ok -> ${TARGET}"
    /usr/bin/scp "$TARGET" offsite:/srv/copias/ || log "scp offsite fallo"
    ;;
  backup-abort)
    log "ABORTADO backup de ${VMID}"
    /usr/bin/curl -fsS -X POST https://ntfy.example/backups -d "FALLO backup ${VMID}"
    ;;
  log-end)
    log "log en ${LOGFILE}"
    ;;
  job-end|job-abort)
    log "fin de job: ${PHASE}"
    ;;
esac
exit 0

Rendimiento: bwlimit, ionice, max-workers y change detection

--bwlimit se expresa en KiB/s y 0 desactiva el límite. Hay dos niveles y gana el más pequeño: el de la tarea y el del storage.

vzdump 101 --storage pbs01 --bwlimit 51200        # 50 MiB/s para esta tarea
pvesm set local-lvm --bwlimit restore=102400      # 100 MiB/s por defecto al restaurar

Los tipos de límite por storage son default, migration, restore, clone y move. Pasar 0 para saltarse el límite en una tarea concreta requiere el privilegio Datastore.Allocate.

--ionice acepta 0-8 con default 7, y 8 es la clase idle. Aquí está el detalle que casi nadie comprueba: solo tiene efecto con el scheduler BFQ.

cat /sys/block/sda/queue/scheduler
# [none] mq-deadline kyber bfq    <- con [none] activo, ionice no hace nada
echo bfq > /sys/block/sda/queue/scheduler

--performance tiene dos sub-opciones. max-workers=<int> fija cuántos workers concurrentes de QEMU trabajan en el backup de una VM, subiendo el throughput a costa de más carga. pbs-entries-max=<int> es el máximo de entradas de directorio en memoria al generar el pxar de un contenedor hacia PBS; subirlo evita fallos en filesystems con directorios enormes.

vzdump 101 --storage pbs01 --performance max-workers=8,pbs-entries-max=2097152

--pbs-change-detection-mode aplica solo a contenedores con destino PBS y es la mayor mejora de rendimiento disponible para CTs con muchos ficheros estáticos:

ModoFormatoComportamiento
legacy (default)pxar v1Lee y codifica todos los ficheros en cada backup
datapxar v2, .mpxar más .ppxarSepara metadatos y payload en streams distintos
metadatapxar v2Compara con el archivo de metadatos del snapshot anterior y reutiliza los chunks de los ficheros sin cambios, sin releerlos del disco
vzdump 123 --storage pbs01 --pbs-change-detection-mode metadata

Si tus contenedores tardan exactamente lo mismo cada noche aunque no cambie nada dentro, estás en legacy. En la GUI el selector de change detection solo aparece cuando el destino es PBS.

Restaurar: qmrestore, pct restore, live restore y un solo fichero

Un backup que no se ha restaurado nunca es una hipótesis, no una copia.

qmrestore para VMs

qmrestore <archive> <vmid> [OPTIONS]
OpciónDefaultDescripción
--bwlimitdel storage o datacenterLímite de I/O en KiB/s
--forcePermite sobrescribir una VM existente
--ha-managed0Registra la VM restaurada como recurso HA
--live-restoreArranca la VM ya y restaura en segundo plano. Solo PBS
--poolPool destino
--start0Arranca la VM al terminar
--storageel originalStorage destino
--uniqueGenera direcciones MAC aleatorias nuevas

La sintaxis de origen cuando el backup vive en PBS es <storage>:backup/<tipo>/<id>/<timestamp>:

# Desde fichero en un storage de directorio
qmrestore /var/lib/vz/dump/vzdump-qemu-101-2026_08_09-03_00_12.vma.zst 101 --force

# Desde PBS, a otro VMID, con MAC nueva y arrancando al terminar
qmrestore pbs01:backup/vm/101/2026-08-09T01:00:12Z 999 \
  --storage local-lvm --unique 1 --start 1

# Desde stdin
zstd -d < vzdump-qemu-101-2026_08_09-03_00_12.vma.zst | qmrestore - 101 --storage local-lvm

Para ver qué hay disponible:

pvesm list pbs01
pvesm list pbs01 --vmid 101
pvesm list local --content backup

Live restore crea la config, arranca la VM inmediatamente leyendo bloques bajo demanda desde PBS, y en paralelo copia el resto al storage local. La lectura de disco va limitada mientras dura. Advertencia: si la restauración falla a medias, la VM queda en un estado indefinido; no hay recuperación parcial, hay que borrarla y reintentar.

qmrestore pbs01:backup/vm/101/2026-08-09T01:00:12Z 101 \
  --storage local-lvm --live-restore 1

pct restore para contenedores

pct restore tiene dos modos y la diferencia no es cosmética.

El modo simple se activa cuando no indicas ni rootfs ni ningún mpX: extrae los mount points y sus opciones del backup, crea los volúmenes en el storage que le pases con --storage (default local), extrae los ficheros y añade a la configuración los bind y device mounts (limitado a root). Como esos nunca se respaldan, en el último paso no se restaura ningún fichero, solo las opciones de configuración. Es el modo que usa la interfaz web.

El modo avanzado se activa en cuanto indicas rootfs o cualquier mpX, y en palabras de la documentación “completely ignores” la configuración de mount points contenida en el archivo: usa solo lo que le pases por parámetro. Sirve para redistribuir el contenido a un layout nuevo.

# Ver la config que hay dentro del archivo, sin restaurar nada
pvesm extractconfig local:backup/vzdump-lxc-110-2026_08_09-03_00_00.tar.zst

# Modo simple, a otro CT, cambiando recursos
pct restore 205 pbs01:backup/ct/105/2026-08-09T02:15:00Z \
  --storage local-zfs --hostname web-restaurado \
  --cores 4 --memory 4096 --swap 512 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --unique 1 --start 1

# Modo avanzado: raiz en un storage y datos en otro
pct restore 205 pbs01:backup/ct/105/2026-08-09T02:15:00Z \
  --rootfs local-zfs:16 \
  --mp0 fast-nvme:50,mp=/var/lib/postgresql,backup=1

Gotcha: restaurar un CT privilegiado como unprivileged (o al revés) con --unprivileged fuerza el tipo, pero los UIDs del archivo pueden no cuadrar y los permisos quedan rotos.

Restauracion de un solo fichero

Solo funciona con backups almacenados en PBS. Para contenedores es directo: el archivo es un .pxar y se navega sin más. Para imágenes de VM, PBS no monta el filesystem del guest en el host (sería un riesgo de seguridad y de drivers): arranca una micro-VM QEMU efímera con el kernel e initramfs del paquete proxmox-backup-restore-image, le mapea la imagen y monta el filesystem dentro de ese sandbox.

Consecuencia práctica: hace falta /dev/kvm. En un PBS o PVE virtualizado sin nested virt, el file restore de imágenes de VM va muy lento o falla. Si la GUI te lista los discos pero no entra en las particiones, falta el paquete o falta KVM.

export PBS_REPOSITORY='backup@[email protected]:8007:store1'
export PBS_FINGERPRINT='64:d3:ff:3a:...:ab:fe'

proxmox-file-restore list vm/2000/2026-03-31T12:55:48Z /
proxmox-file-restore list vm/2000/2026-03-31T12:55:48Z /drive-scsi0.img.fidx
proxmox-file-restore list vm/2000/2026-03-31T12:55:48Z /drive-scsi0.img.fidx/part/2/etc
proxmox-file-restore extract vm/2000/2026-03-31T12:55:48Z \
  /drive-scsi0.img.fidx/part/2/etc/nginx/nginx.conf /tmp/recuperado/

Alternativas para hosts genéricos con proxmox-backup-client: montar un pxar por FUSE, mapear una imagen como loop device, o abrir el shell de catálogo.

proxmox-backup-client mount host/miservidor/2026-08-09T01:00:00Z root.pxar /mnt/restore
proxmox-backup-client map vm/101/2026-08-09T01:00:00Z drive-scsi0.img
proxmox-backup-client catalog shell host/miservidor/2026-08-09T01:00:00Z root.pxar

Jobs del datacenter, pvescheduler y eventos de calendario

Los jobs se definen en Datacenter -> Backup, viven en /etc/pve/jobs.cfg (replicado por pmxcfs a todo el cluster, ver capítulo 11) y los ejecuta pvescheduler.

vzdump: nocturno-criticos
	schedule mon..fri 22:30
	compress zstd
	zstd 4
	enabled 1
	mode snapshot
	notes-template {{guestname}} ({{vmid}}) @ {{node}}
	notification-mode notification-system
	prune-backups keep-last=3,keep-daily=14,keep-weekly=8,keep-monthly=12,keep-yearly=3
	repeat-missed 1
	storage pbs01
	vmid 100,102,106,107,115
	bwlimit 102400
	performance max-workers=4
	fleecing enabled=1,storage=local-lvm

Un job acepta las mismas claves que vzdump más las suyas propias: schedule, enabled, id, comment y repeat-missed (que lanza el job al arrancar si el nodo estaba apagado a la hora prevista). Las viejas starttime y dow de PVE 6.x son legado y se migran a schedule. Al revés, el endpoint POST /nodes/{node}/vzdump acepta todo excepto esas claves de job.

Gestión completa por CLI:

pvesh get /cluster/backup --output-format json-pretty
pvesh get /cluster/backup/backup-f3135cb2-1805

pvesh create /cluster/backup \
  --id backup-nocturno \
  --schedule '02:30' \
  --storage pbs01 \
  --all 1 --exclude 9000,9001 \
  --mode snapshot --compress zstd \
  --notes-template '{{guestname}} @ {{node}}' \
  --prune-backups 'keep-daily=7,keep-weekly=4,keep-monthly=6' \
  --enabled 1

pvesh set /cluster/backup/backup-nocturno --bwlimit 102400 --repeat-missed 1
pvesh delete /cluster/backup/backup-nocturno

Sintaxis de eventos de calendario

PVE y PBS usan un subconjunto propio del formato de systemd.time(7):

[WEEKDAY] [[YEARS-]MONTHS-DAYS] [HOURS:MINUTES[:SECONDS]]

Hace falta al menos uno de los tres componentes. Si falta el día se aplica a todos los días; si falta la hora, a las 00:00:00. Días de la semana en inglés abreviado y sin distinguir mayúsculas: mon, tue, wed, thu, fri, sat, sun.

Cada campo admite valor único (03), lista (01,02,03), rango (01..10), repetición (05/10, o sea empezando en 5 cada 10 unidades), comodín (*) y combinaciones (01,05..10,12/02).

Atajos disponibles: minutely, hourly, daily, weekly, monthly, yearly (o annually), quarterly y semiannually.

ScheduleSe dispara
03:00Todos los días a las 3 de la mañana
mon..fri 22Días laborables a las 22:00
sun 04:30Domingos a las 4:30
*:0/15Cada 15 minutos
mon..fri 8..17,22:0/15Cada 15 min entre las 8 y las 17 y también en la hora 22, solo laborables
*-*-01 05:00El día 1 de cada mes a las 5:00
sat *-1..7 15:00Primer sábado de cada mes a las 15:00
2026-10-21Una sola vez, el 21 de octubre de 2026 a las 00:00

El parser de Proxmox no soporta timestamps Unix, no soporta zonas horarias, no soporta resolución sub-segundo y no soporta la sintaxis de día invertido. Antes de dar por bueno un schedule, valídalo:

systemd-analyze calendar 'mon..fri 8..17,22:0/15'
systemd-analyze calendar --iterations=10 '*-*-01 05:00'
systemd-analyze calendar --iterations=5 'sat *-1..7 15:00'

Recuerda que tanto pvescheduler como proxmox-backup-proxy comprueban los schedules una vez por minuto.

Retencion: el algoritmo keep-* paso a paso

Las mismas opciones keep-* se usan en tres sitios (job de vzdump, definición del storage y prune de PBS) y el algoritmo es idéntico en los tres.

OpciónSignificado
keep-last=<N>Los N backups más recientes, sin mirar la fecha
keep-hourly=<N>El último backup de cada una de las últimas N horas con backups
keep-daily=<N>El último de cada uno de los últimos N días con backups
keep-weekly=<N>El último de cada una de las últimas N semanas ISO, de lunes a domingo
keep-monthly=<N>El último de cada uno de los últimos N meses con backups
keep-yearly=<N>El último de cada uno de los últimos N años con backups
keep-all=1Guardar todo. Excluyente con las demás
flowchart TD
    S["Lista de snapshots ordenada de mas nuevo a mas viejo"] --> PR{"Snapshot protegido?"}
    PR -->|si| KEEP["Se conserva y no se evalua"]
    PR -->|no| L["keep-last: marca los N mas recientes"]
    L --> H["keep-hourly: marca el ultimo de cada hora NO vacia"]
    H --> D["keep-daily: igual pero por dia con backups"]
    D --> W["keep-weekly: igual pero por semana ISO"]
    W --> M["keep-monthly: igual pero por mes con backups"]
    M --> Y["keep-yearly: igual pero por anio con backups"]
    Y --> Q{"Quedo marcado?"}
    Q -->|si| KEEP
    Q -->|no| DEL["Se elimina"]

Los puntos que se malinterpretan siempre:

Uno. Los intervalos vacíos no cuentan. keep-daily=7 no significa “los últimos 7 días naturales”, significa “los últimos 7 días en los que hubo backup”. Si el nodo estuvo dos semanas apagado, la regla retrocede hasta encontrar 7 días con datos.

Dos. Cada opción solo considera lo que las anteriores no han cubierto, y ninguna puede des-marcar lo que otra ya marcó.

Tres. Un mismo snapshot puede satisfacer varias reglas a la vez (el del domingo puede ser el daily del domingo y el weekly de esa semana). Por eso el total conservado suele ser menor que la suma de los keep-*.

Cuatro. Las semanas son ISO, de lunes a domingo, y pueden cruzar meses y años.

Cinco. Los backups incompletos o interrumpidos se eliminan en el prune, salvo que sean más recientes que el último backup exitoso.

Seis. Los snapshots protegidos nunca se evalúan.

El ejemplo de la documentación oficial para backup diario con archivo a diez años:

vzdump 777 --prune-backups keep-last=3,keep-daily=13,keep-weekly=8,keep-monthly=11,keep-yearly=9

keep-last=3 cubre backups manuales extra del mismo día, keep-daily=13 garantiza dos semanas, keep-weekly=8 garantiza dos meses, keep-monthly=11 garantiza un año y keep-yearly=9 cubre los nueve años restantes.

Antes de aplicar una política nueva, pruébala en el simulador oficial de prune o con un dry-run desde el cliente:

proxmox-backup-client prune host/elsa --dry-run --keep-daily 7 --keep-weekly 4

Dónde se define la retención en PVE y qué gana: el prune-backups del job manda sobre el del storage, y si no hay ninguno de los dos se aplica keep-all=1 y no se borra nada.

pvesm set pbs01 --prune-backups keep-last=3,keep-daily=14,keep-weekly=8,keep-monthly=12

Advertencia: cuando el destino es PBS hay dos capas de prune posibles, la de PVE (--remove 1 más prune-backups, aplicada al terminar cada job) y la de PBS (prune jobs del datastore). Configurar las dos con políticas distintas es el error operativo número uno: la más agresiva gana y nadie entiende por qué desaparecen snapshots. La práctica recomendada es hacer el prune en PBS, que es donde está el espacio y donde vive la protección frente a un PVE comprometido, y dejar keep-all=1 o directamente remove 0 en el lado de PVE.

Notificaciones de backup y matchers

La configuración vive en /etc/pve/notifications.cfg y los secretos en /etc/pve/priv/notifications.cfg. Los targets soportados son sendmail, smtp, gotify y webhook.

El campo notification-mode del job decide qué sistema se usa:

ValorComportamiento
auto (default)Si el job tiene mailto, usa legacy-sendmail; si no, el sistema nuevo
legacy-sendmailEnvía por sendmail a la dirección del job
notification-systemUsa los targets y matchers globales

Un backup emite estos metadatos, que son los que puedes filtrar en un matcher: type=vzdump, hostname=<nodo> y job-id=<id> (solo en jobs programados). Las severidades son info en éxito y error en fallo.

# /etc/pve/notifications.cfg
gotify: gotify-ops
	server https://gotify.example.com
	comment Alertas de infra

matcher: solo-errores-backup
	mode all
	match-field exact:type=vzdump
	match-severity error
	target gotify-ops

Los matchers admiten match-field (con prefijo exact: o regex:), match-severity, match-calendar (misma sintaxis de eventos de calendario), mode con valores all o any, e invert-match. El resto del sistema de notificaciones y su relación con el monitoreo lo verás en el capítulo 15.

Proxmox Backup Server: arquitectura, datastores y layout en disco

PBS 4.2 corre sobre Debian 13, está escrito en Rust y expone API y UI en HTTPS 8007. Dos servicios principales: proxmox-backup-proxy.service, que sirve la API y la UI sin privilegios como usuario backup, y proxmox-backup.service, el daemon privilegiado que escucha en localhost.

Toda la configuración vive en /etc/proxmox-backup/:

FicheroContenido
datastore.cfgDefinición de datastores
remote.cfgServidores PBS remotos
sync.cfgSync jobs
verification.cfgVerify jobs
prune.cfgPrune jobs
user.cfg / acl.cfgUsuarios y ACLs con formato acl:propagate:path:user:role
tape.cfg, tape-job.cfg, media-pool.cfgCinta
s3.cfgEndpoints S3
node.cfgConfig del nodo: fingerprint, ACME
notifications.cfgTargets y matchers

El layout de un datastore en disco es previsible y conviene conocerlo, porque es lo que salva el día en una recuperación de emergencia:

/mnt/datastore/store1/.lock
/mnt/datastore/store1/.chunks/0000/  ...  /mnt/datastore/store1/.chunks/ffff/
/mnt/datastore/store1/ns/produccion/vm/
/mnt/datastore/store1/vm/101/owner
/mnt/datastore/store1/vm/101/2026-08-09T01:00:12Z/index.json.blob
/mnt/datastore/store1/vm/101/2026-08-09T01:00:12Z/drive-scsi0.img.fidx
/mnt/datastore/store1/vm/101/2026-08-09T01:00:12Z/qemu-server.conf.blob
/mnt/datastore/store1/ct/105/2026-08-09T02:15:00Z/root.pxar.didx
/mnt/datastore/store1/ct/105/2026-08-09T02:15:00Z/catalog.pcat1.didx
/mnt/datastore/store1/host/miservidor/

Requisito de filesystem crítico: .chunks/ contiene 65.536 subdirectorios precreados, de 0000 a ffff. El filesystem debe soportar al menos 65.538 subdirectorios en un directorio (los 65.536 más . y ..). Eso excluye ext3 por completo, y también ext4 con dir_nlink deshabilitado a mano. Soportados: ext4, xfs y zfs.

Las extensiones que verás: .fidx para índices de tamaño fijo (imágenes de disco), .didx para índices dinámicos (archivos pxar), .blob para configs, manifests y logs, y .pcat1 para el catálogo de estructura de directorios.

Crear y ajustar datastores:

proxmox-backup-manager datastore create store1 /backup/disk1/store1
proxmox-backup-manager disk fs create store1 --disk sdX --filesystem ext4 --add-datastore true
proxmox-backup-manager disk zpool create zpool1 --devices sdX,sdY --raidlevel mirror --add-datastore true

proxmox-backup-manager datastore list
proxmox-backup-manager datastore show store1
proxmox-backup-manager datastore update store1 --gc-schedule 'Tue 04:27'
proxmox-backup-manager datastore remove store1     # borra la CONFIG, no los datos

Las opciones de tuning que de verdad mueven la aguja:

OpciónValoresDefaultEfecto
chunk-orderinode / noneinodeinode ordena los chunks por inodo al verificar o escribir cinta: óptimo en discos mecánicos. none itera en el orden del índice y puede ser más rápido en NVMe
sync-levelnone / filesystem / filefilesystemnone confía en el flush del kernel. filesystem hace syncfs al terminar el backup. file hace fsync por cada chunk: máxima consistencia, mínimo rendimiento
gc-cache-capacityentero1048576Slots de la caché LRU de la fase 1 de GC. Máximo 8388608. Más caché es GC más rápido y más RAM
default-verification-workersentero4Hilos de verificación por defecto
default-verification-readersentero1Hilos de lectura de chunks por defecto
proxmox-backup-manager datastore update store1 --tuning 'sync-level=filesystem,chunk-order=none'
proxmox-backup-manager datastore update store1 --maintenance-mode read-only
proxmox-backup-manager datastore update store1 --delete maintenance-mode

El chunk store, la deduplicacion y la inmutabilidad relativa

PBS no guarda ficheros de backup: guarda chunks direccionados por contenido, y los snapshots son listas ordenadas de hashes.

  • Chunks de tamaño fijo, típicamente 4 MiB, para las imágenes de disco de VM. Funciona porque los filesystems del guest asignan ficheros de forma más o menos contigua.
  • Chunks de tamaño variable para los archivos pxar, cortados con una variante de Buzhash sobre el stream. Los límites dependen del contenido y no de la posición, así que insertar bytes al principio de un fichero no rompe la deduplicación del resto.
  • Cada chunk se identifica por su SHA-256 y se guarda en .chunks/<4 dígitos hex>/<hash completo>.
  • Cada chunk se comprime con zstd en el cliente antes de subirse.

Con cifrado activo, el hash no se calcula sobre el texto cifrado sino sobre el texto plano concatenado con la clave. Eso evita colisiones entre claves distintas y permite deduplicación incremental sin recifrar. El efecto colateral importante: el servidor no puede verificar el SHA-256 de chunks cifrados y en verify solo comprueba el CRC-32.

Hay dos mecanismos de incremental. Uno, el cliente descarga la lista de chunks del snapshot previo y, si un chunk ya existe, manda solo el checksum. Dos, para VMs, QEMU mantiene dirty bitmaps con los bloques modificados desde el último backup; se pierden si la VM se apaga o se migra, y entonces el siguiente backup vuelve a leer todo el disco, aunque sigue subiendo solo lo nuevo.

De ahí sale la propiedad anti-ransomware más citada, literal de la documentación: “Proxmox Backup Server does not rewrite data for existing blocks”. Un host PVE comprometido no puede corromper ni modificar los backups existentes, porque los chunks nunca se reescriben. Lo que sí podría hacer es borrar snapshots, si sus credenciales tienen privilegio de prune. Por eso la recomendación de la que hablamos más abajo.

Prune frente a garbage collection: por que el espacio no se libera

Son dos operaciones distintas y confundirlas es el malentendido más habitual con PBS.

flowchart LR
    P["PRUNE"] --> P1["Borra INDICES de snapshots segun la politica keep-*"]
    P1 --> P2["El espacio NO se libera: los chunks siguen en disco"]
    P2 -.necesita.-> G["GARBAGE COLLECTION"]
    G --> G1["Fase MARK: recorre los indices vivos y toca el atime de sus chunks"]
    G1 --> G2["Fase SWEEP: borra los chunks con atime anterior al cutoff"]
    G2 --> G3["Ahora si se libera espacio"]

Los prune jobs se definen en /etc/proxmox-backup/prune.cfg y aceptan store, ns, max-depth, schedule, las opciones keep-*, disable y comment.

proxmox-backup-manager prune-job create prune-diario \
  --store store1 --schedule 'daily' \
  --keep-last 3 --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3

proxmox-backup-manager prune-job list
proxmox-backup-manager prune-job update prune-diario --keep-daily 21

# Equivalente integrado en el propio datastore
proxmox-backup-manager datastore update store1 \
  --prune-schedule 'daily' --keep-daily 7 --keep-weekly 4

La garbage collection es un mark and sweep que no bloquea el datastore entero. En la fase 1 lee todos los índices vivos y actualiza el atime de cada chunk referenciado, con una caché LRU (gc-cache-capacity) para no tocar mil veces el mismo. En la fase 2 recorre .chunks/ y borra los chunks cuyo atime es anterior al cutoff, que es el atime del escritor de backup activo más antiguo o, si no hay ninguno, 24 horas y 5 minutos antes del inicio de la GC.

Ese número raro tiene explicación: los filesystems se montan por defecto con relatime, que solo actualiza el atime cuando el fichero se modifica o cuando el atime anterior tiene más de 24 horas. Los 5 minutos cubren el desfase. Los chunks dentro del periodo de gracia aparecen en el log como “Pending removals”.

proxmox-backup-manager garbage-collection start store1
proxmox-backup-manager garbage-collection status store1
proxmox-backup-manager datastore update store1 --gc-schedule 'Tue 04:27'

Advertencia: nunca montes un datastore con noatime. La fase 1 no podría marcar nada y la GC borraría datos vivos. La comprobación gc-atime-safety-check, activada por defecto, detecta el caso y aborta. No la desactives.

Verify jobs, remotes y sync pull frente a push

Los verify jobs comprueban la integridad de los chunks (SHA-256 en claro, CRC-32 en los cifrados). Se configuran en /etc/proxmox-backup/verification.cfg.

ParámetroDefaultDescripción
ignore-verifiedSalta snapshots ya verificados con éxito
outdated-afterRe-verifica los que lleven más de N días sin verificar
scheduleEvento de calendario
read-threads1, rango 1-32Hilos de lectura de chunks
verify-threads4, rango 1-32Hilos de verificación
ns / max-depthÁmbito de namespaces
proxmox-backup-manager verify store1 --read-threads 4 --verify-threads 8 --ignore-verified false

La recomendación oficial es re-verificar todos los backups al menos una vez al mes. El patrón habitual son dos jobs: uno diario con ignore-verified=true y outdated-after=30 para lo nuevo, y otro semanal o mensual completo contra bit rot. Un snapshot que falla la verificación se marca como corrupto en la UI, y los sync jobs con --resync-corrupt lo vuelven a traer del remoto.

Un remote es otro PBS registrado, y vive en /etc/proxmox-backup/remote.cfg:

# En el PBS remoto
proxmox-backup-manager cert info | grep Fingerprint

# En el PBS local
proxmox-backup-manager remote create pbs2 \
  --host pbs2.mydomain.example \
  --auth-id sync@pam \
  --password 'SECRET' \
  --fingerprint 64:d3:ff:3a:50:38:53:5a:9b:f7:50:...:ab:fe

Los sync jobs tienen dirección pull (default: el PBS local trae datos del remoto) o push (el local empuja al remoto).

proxmox-backup-manager sync-job create pbs2-local \
  --remote pbs2 --remote-store local --store local --schedule 'Wed 02:30'
proxmox-backup-manager sync-job run pbs2-local

Opciones que se usan de verdad: --remove-vanished, --encrypted-only, --verified-only, --resync-corrupt, --rate-in y --rate-out para limitar ancho de banda, --worker-threads (default 1, rango 1-32, novedad de PBS 4.2), --transfer-last <N> para traer solo los N snapshots más recientes de cada grupo, --group-filter y --max-depth.

Tres detalles que muerden:

Uno. El flag protected no se propaga en un sync. Un snapshot protegido en origen llega sin proteger al destino.

Dos. El modo push requiere PBS 2.2 o superior en el remoto.

Tres. La dirección del sync es una decisión de seguridad, no de comodidad:

flowchart TD
    A["Un atacante compromete el cluster PVE"] --> B{"Como llega al backup"}
    B -->|"Token que usa PVE"| C["Solo tiene DatastoreBackup y no puede borrar"]
    B -->|"Credenciales del PBS local"| D{"El offsite es pull o push"}
    D -->|"PUSH desde el PBS local"| E["El PBS local guarda credenciales del remoto y el atacante las hereda"]
    D -->|"PULL desde el PBS remoto"| F["El PBS local no conoce al remoto y el atacante no puede tocarlo"]

En pull, el destino guarda las credenciales del origen y el origen no sabe nada del destino. Un atacante que tome el origen no encuentra ninguna credencial que le lleve al offsite. En push ocurre lo contrario. Por eso, cuando el objetivo es resistir ransomware, la copia offsite se hace con pull.

Cifrado en cliente, master key y paperkey

El cifrado de backups es exclusivo de storages PBS, usa AES-256-GCM y ocurre siempre en el cliente: los datos salen ya cifrados del host PVE y el servidor nunca ve la clave.

# Clave protegida por contrasena
proxmox-backup-client key create /root/pbs.key
# Sin contrasena, para automatizacion desatendida
proxmox-backup-client key create /root/pbs.key --kdf none
# Copia en papel con QR para la caja fuerte
proxmox-backup-client key paperkey /root/pbs.key --output-format text > qrkey.txt

Desde PVE, el valor mágico autogen genera y activa la clave en un solo paso (internamente ejecuta key create --kdf none):

pvesm set pbs01 --encryption-key autogen

Las rutas exactas en el lado PVE:

FicheroContenido
/etc/pve/priv/storage/<STORAGE-ID>.pwContraseña del usuario PBS
/etc/pve/priv/storage/<STORAGE-ID>.encClave AES-256-GCM
/etc/pve/priv/storage/<STORAGE-ID>.master.pemClave pública RSA maestra

La master key resuelve el caso “el cliente ya no existe”: generas un par RSA, guardas la privada offline e importas la pública en el cliente. A partir de ahí cada snapshot lleva dentro un rsa-encrypted.key con la clave AES cifrada con la pública, así que con la privada puedes recuperar cualquier backup aunque el host original haya desaparecido.

proxmox-backup-client key create-master-key
proxmox-backup-client key import-master-pubkey /path/to/master-public.pem
# Recuperacion
proxmox-backup-client restore /path/to/backup/ rsa-encrypted.key /target
proxmox-backup-client key import-with-master-key /target \
  --master-keyfile /path/to/master-private.pem \
  --encrypted-keyfile /path/to/rsa-encrypted.key

Advertencia literal de la documentación: sin la clave correspondiente, los backups cifrados son permanentemente inaccesibles. Guarda la clave fuera del sistema que respaldas (si arde el PVE, arde la clave), en un gestor de contraseñas, en un USB en ubicación física distinta y en papel con paperkey.

Namespaces, cinta LTO, datastores removibles y backend S3

Namespaces. Separación jerárquica dentro de un mismo datastore, con profundidad máxima de 8 niveles. Permiten que varios orígenes compartan datastore, y por tanto deduplicación, sin colisionar por VMID ni pisarse los permisos: el VMID 101 de dos clientes distintos convive sin problema en cliente-a/produccion y cliente-b. PBS 4.2 añadió mover grupos y namespaces enteros.

proxmox-backup-client namespace create cliente-a
proxmox-backup-client namespace create cliente-a/produccion
proxmox-backup-client snapshot list --ns cliente-a/produccion
proxmox-backup-client group move vm/101 --ns cliente-a --target-ns cliente-b \
  --repository backup@[email protected]:8007:store1

En PVE el namespace se elige en la definición del storage con pvesm set pbs01 --namespace cliente-a/produccion.

Cinta LTO. PBS soporta LTO-5 o superior (LTO-4 con soporte best-effort) mediante un driver propio en Rust que habla por la interfaz SCSI generic, para no chocar con el driver st del kernel. Los conceptos clave son el media pool (agrupación lógica de cintas con políticas) y el media set (conjunto de cintas escritas secuencialmente, que es la unidad de restauración). La política de asignación puede ser continue, always o un evento de calendario. Un LTO-8 llega a unos 360 MB/s y tarda un mínimo de nueve horas en llenar una cinta.

proxmox-tape drive scan
proxmox-tape drive create mydrive --path /dev/tape/by-id/scsi-12345-sg
proxmox-tape pool create semanal
proxmox-tape pool update semanal --allocation weekly --retention 'keep'
proxmox-tape backup-job create tape-semanal \
  --store prod --pool semanal --drive mydrive --schedule 'fri 20:00' \
  --eject-media true --export-media-set true
proxmox-tape catalog        # obligatorio antes de restaurar en drives standalone

El cifrado de cinta lo hace el hardware LTO-4 o superior con AES-GCM, se configura por media pool, y la clave protegida por contraseña se graba magnéticamente en cada cinta.

Datastores removibles (desde PBS 4.0). Un datastore con backing-device asociado, que se monta en /mnt/datastore/<name>. El filesystem recomendado es ext4 o xfs; FAT queda excluido por el modelo de propietarios. Con el datastore desmontado, verify, prune y GC se saltan, pero los sync jobs arrancan y fallan con error.

proxmox-backup-manager datastore unmount store1
proxmox-backup-manager datastore update store1 --gc-on-unmount true
proxmox-backup-debug inspect device /dev/sdX

Backend S3. Fue technology preview en PBS 4.0 y 4.1, y pasa a soporte oficial en 4.2.

proxmox-backup-manager s3 endpoint create my-s3-ep \
  --access-key 'my-access-key' --secret-key 'my-secret-key' \
  --endpoint '{{bucket}}.s3.{{region}}.amazonaws.com' --region eu-central-1

proxmox-backup-manager datastore create my-s3-store /mnt/datastore/my-s3-store-cache \
  --backend type=s3,client=my-s3-ep,bucket=pbs-s3-bucket

Consideraciones antes de meterte: necesita una caché local persistente de 64 a 128 GiB, solo funciona sobre HTTPS, y un datastore no se puede compartir entre varias instancias de PBS (solo una puede operarlo a la vez). Con chunks de 4 MiB el número de requests de API es alto, y los proveedores cobran por request además de por almacenamiento y egress. PBS 4.2 añade estadísticas de requests y tráfico y umbrales configurables.

Hardware de PBS y por que castiga a los discos mecanicos

ComponenteRecomendación
CPU64 bits moderno, mínimo 4 cores
RAM4 GB para sistema, cachés y daemons más 1 GB por cada TB de almacenamiento de backup
Disco de SO32 GB libres o más, con RAID hardware con BBU o ZFS redundante
Almacenamiento de backupSSD enterprise preferido
Si son HDDZFS special device en mirror para los metadatos
RedNICs multi-gigabit redundantes

Regla explícita de la documentación: “ZFS on top of any hardware RAID is not supported and can result in data loss”. Y otra que se ignora demasiado: la documentación no recomienda instalar PBS dentro del mismo host PVE al que respalda, además de que conceptualmente rompe el 3-2-1.

El motivo de tanto énfasis en el disco es estructural. Un snapshot de 100 GB con chunks de 4 MiB son unos 25.000 ficheros pequeños repartidos aleatoriamente por 65.536 directorios, porque el hash no tiene localidad espacial. Todas las operaciones son I/O aleatorio de ficheros pequeños: el backup escribe chunks dispersos, el verify los lee y calcula SHA-256, la fase 1 de GC hace stat() sobre cada chunk vivo y la fase 2 hace readdir y unlink sobre millones de entradas. Es carga limitada por metadatos, no por throughput. Un HDD de 7200 rpm da del orden de 100 a 200 IOPS; un SSD enterprise, entre 50.000 y 500.000. Por eso la GC de un datastore grande en discos mecánicos puede tardar horas.

El special vdev en mirror sobre SSD resuelve la parte cara: ZFS coloca ahí los metadatos y deja a los HDD el payload secuencial. Advertencia: si pierdes el special vdev, pierdes el pool entero, así que siempre en mirror y con SSD con protección de pérdida de energía. La regla de 1 GB de RAM por TB viene del ARC: sin RAM, ZFS no cachea metadatos y todo vuelve al disco. Los fundamentos de ZFS, special devices y ARC están en el capítulo 8.

EscenarioVeredicto
SSD enterprise puroIdeal, sin sorpresas
HDD más ZFS más special device mirror SSD más 1 GB RAM por TBCorrecto y económico. Es la receta estándar
HDD más ZFS sin special device y poca RAMMala idea. GC y verify se vuelven inviables al crecer
HDD más ext4 o xfs sin caché de metadatosFunciona razonablemente en datastores pequeños, sin las garantías de integridad de ZFS
ZFS sobre RAID hardwareNo soportado. Riesgo de pérdida de datos
PBS dentro del mismo host PVE que respaldaNo recomendado por la documentación

Integrar PBS como storage de PVE con el token correcto

Este es el punto donde la seguridad del backup se gana o se pierde.

# 1. En el PBS: fingerprint del certificado
proxmox-backup-manager cert info | grep Fingerprint

# 2. En el PBS: usuario y token dedicados, con ACL minima
proxmox-backup-manager user create pve-backup@pbs --email [email protected]
proxmox-backup-manager user generate-token pve-backup@pbs cluster1
proxmox-backup-manager acl update /datastore/store1 DatastoreBackup \
  --auth-id 'pve-backup@pbs!cluster1'

# 3. En PVE: dar de alta el storage
pvesm add pbs pbs01 --server pbs.example.com --datastore store1
pvesm set pbs01 --username 'pve-backup@pbs!cluster1' --password
pvesm set pbs01 --fingerprint 64:d3:ff:3a:50:38:53:5a:9b:f7:50:...:ab:fe
pvesm set pbs01 --encryption-key autogen

# 4. Comprobar
pvesm status --storage pbs01
pvesm list pbs01

Omitir el valor de --password hace que se pida por prompt y no quede en el historial de shell. El secreto del token se muestra una sola vez al generarlo.

Patrón de seguridad recomendado: el API token que usa PVE tiene solo el rol DatastoreBackup, sin Datastore.Prune. Así, un host PVE comprometido puede crear backups nuevos pero no puede borrar los históricos. El prune se ejecuta como job dentro de PBS, con credenciales que nunca salen del PBS. Los roles y privilegios equivalentes en el lado de PVE están en el capítulo 10.

Roles útiles de PBS: DatastoreBackup (crear y restaurar los propios), DatastoreReader (leer contenido arbitrario, el rol del usuario que hace pull desde otro PBS), DatastorePowerUser (backup, restore y prune de lo propio), DatastoreAdmin y DatastoreAudit.

Estrategia 3-2-1 en homelab y en PyME

La regla es 3 copias de los datos, en 2 tipos de medio distintos, con 1 copia offsite. La variante moderna es 3-2-1-1-0: una copia inmutable o con air gap, y cero errores de verificación.

flowchart TB
    subgraph PROD["1 - Produccion"]
      P["Discos de VMs y CTs en el cluster PVE"]
    end
    subgraph LOCAL["2 - Copia local en medio distinto"]
      B1["PBS on-premise con datastore propio"]
    end
    subgraph OFF["3 - Copia offsite"]
      B2["PBS remoto que hace sync pull"]
      T["Cintas LTO exportadas"]
      S3["Bucket S3"]
      USB["Datastore removible rotado"]
    end
    P -->|"vzdump con token DatastoreBackup"| B1
    B1 -->|"sync job pull desde el remoto"| B2
    B1 -->|"tape job"| T
    B1 -->|"backend s3"| S3
    B1 -->|"datastore removible"| USB
    B1 -.->|"verify job"| V["Cero errores"]
    B2 -.->|"verify job"| V

Homelab

El objetivo realista es sobrevivir a un fallo de disco, a un borrado accidental, a un ransomware dentro de un guest y a un incendio o robo del rack, con presupuesto de aficionado.

CopiaMedioImplementación
1NVMe o SSD del nodoZFS mirror o LVM-thin
2HDD distinto o NASPBS en un mini PC aparte, datastore en HDD con special device SSD
3USB rotado, PBS de un familiar o S3 baratoDatastore removible con run-on-mount, o sync job
# Job de backup en PVE: el prune real lo hace PBS
vzdump: homelab-nocturno
	schedule 02:00
	all 1
	exclude 9000,9001
	storage pbs01
	mode snapshot
	notification-mode notification-system
	prune-backups keep-all=1
	remove 0
	notes-template {{guestname}} @ {{node}}
# En PBS
proxmox-backup-manager datastore update store1 --prune-schedule 'daily 04:00'
proxmox-backup-manager datastore update store1 --gc-schedule 'sun 05:00'

# Offsite barato: disco USB rotado, el sync arranca al enchufarlo
proxmox-backup-manager datastore create offsite-usb /mnt/datastore/offsite-usb \
  --backing-device /dev/disk/by-uuid/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
proxmox-backup-manager datastore update offsite-usb --gc-on-unmount true
proxmox-backup-manager sync-job create a-usb \
  --store offsite-usb --remote-store store1 \
  --sync-direction pull --run-on-mount true --transfer-last 8

Retención sugerida para homelab: keep-last=2, keep-daily=7, keep-weekly=4, keep-monthly=6.

Lo que la gente se salta en homelab y luego lamenta: no respaldar /etc/pve del propio host (la configuración del cluster), no probar nunca una restauración, tener el PBS como VM dentro del mismo PVE que respalda, y guardar la clave de cifrado solo en el PVE que cifran.

PyME

### En PBS-A, on-premise
proxmox-backup-manager user create pve-cluster@pbs
proxmox-backup-manager user generate-token pve-cluster@pbs prod
proxmox-backup-manager acl update /datastore/prod DatastoreBackup --auth-id 'pve-cluster@pbs!prod'
# Sin Datastore.Prune: el prune lo hace PBS-A por su cuenta

proxmox-backup-manager user create sync-b@pbs
proxmox-backup-manager acl update /datastore/prod DatastoreReader --auth-id sync-b@pbs

proxmox-backup-manager datastore update prod \
  --prune-schedule 'daily 04:30' \
  --keep-last 3 --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 7
proxmox-backup-manager datastore update prod --gc-schedule 'sat 05:00'
proxmox-backup-manager datastore update prod --tuning 'chunk-order=inode,sync-level=filesystem'

### En PBS-B, sede secundaria: hace PULL
proxmox-backup-manager remote create pbs-a \
  --host pbs-a.interna.example --auth-id sync-b@pbs --password 'SECRET' \
  --fingerprint aa:bb:cc:...
proxmox-backup-manager sync-job create pull-de-a \
  --remote pbs-a --remote-store prod --store prod-replica \
  --schedule '*-*-* 01:00' \
  --remove-vanished false \
  --resync-corrupt true --verified-only true \
  --worker-threads 4 --rate-in '200MiB'

--remove-vanished false es deliberado: si alguien borra en A, en B se conserva. Es la segunda línea de defensa.

RequisitoHomelabPyME
Copias2-33-4
PBS dedicadoDeseableObligatorio, hardware separado
Dirección del sync offsitePush aceptablePull obligatorio
Cifrado en clienteRecomendado si el offsite es de tercerosObligatorio si sale de la organización
Token de PVE con pruneAceptableProhibido
VerifyMensualDiario incremental más mensual completo
Prueba de restauraciónAnualTrimestral y documentada
Air gapDisco USB desconectadoCinta exportada u Object Lock

Checklist de verificacion trimestral

  1. proxmox-backup-manager verify prod termina sin errores.
  2. Restaurar una VM completa a un VMID nuevo en un nodo de pruebas con qmrestore ... --unique 1.
  3. Restaurar un fichero suelto con File Restore.
  4. Restaurar desde el PBS secundario, no solo desde el primario.
  5. Comprobar que la clave de cifrado está accesible desde fuera del sistema respaldado.
  6. Leer una cinta al azar del archivo con proxmox-tape catalog y una restauración de prueba.
  7. Provocar un fallo a propósito y confirmar que la notificación llega de verdad.

Errores comunes y diagnostico

SíntomaCausa probableDiagnóstico o solución
unable to create snapshot ... storage does not support snapshots en un CTUn mount point vive en un storage sin snapshotspct config <id>; usar mode=suspend o backup=0 en ese mp
VM is locked (backup) y no se puede hacer nadaUn backup anterior murió sin liberar el lockComprobar que no hay tarea viva con ps aux | grep vzdump, luego qm unlock <vmid> o pct unlock <vmid>
El job programado no se ejecutapvescheduler parado, o el campo node del job no coincidesystemctl status pvescheduler; journalctl -u pvescheduler -n 100; revisar jobs.cfg
El schedule no dispara cuando esperasSintaxis del evento de calendariosystemd-analyze calendar --iterations=5 '<tu schedule>'
El prune no libera espacio en PBSFalta la garbage collectionproxmox-backup-manager garbage-collection start <store>
La GC aborta con error de atimeDatastore montado con noatimemount | grep datastore y remontar con relatime. No desactives gc-atime-safety-check
La GC tarda horas en HDDMetadatos en disco mecánicoAñadir special vdev mirror SSD; subir gc-cache-capacity
storage 'pbs01' is not onlineRed, fingerprint, ACL o relojpvesm status --storage pbs01; curl -k https://pbs:8007/; timedatectl en ambos
403 permission check failed desde PVE a PBSEl token no tiene ACL en el datastore o namespaceproxmox-backup-manager acl list y añadir DatastoreBackup
El backup llena /var/lib/vz en modo suspend de un CTtmpdir por defecto en el disco de sistematmpdir: /mnt/rapido en /etc/vzdump.conf
Backup lentísimo con destino remotoCopy-before-write bloqueando al guestActivar fleecing con storage local rápido
El fleecing empeora las cosasStorage de fleecing lento o sin thin provisioningUsar LVM-thin, ZFS sparse o RBD local
Los CT tardan lo mismo cada noche aunque no cambie nadaModo de detección legacy--pbs-change-detection-mode metadata
ionice no tiene ningún efectoEl scheduler no es BFQcat /sys/block/<dev>/queue/scheduler y cambiar a bfq
El hook script no encuentra curl o sshvzdump vacía %ENV con local %ENV;Exportar PATH al inicio del hook y usar rutas absolutas
El hook script aborta el backupCódigo de salida distinto de 0 en una fase crítica|| true en las acciones no críticas y exit 0 explícito
Live restore deja la VM rotaLa restauración falló a mediasBorrar la VM y restaurar de forma normal. No hay recuperación parcial
File Restore lista los discos pero no entra en las particionesFalta proxmox-backup-restore-image o no hay /dev/kvmapt install proxmox-backup-file-restore; verificar nested virt
Desaparecen snapshots que esperabas conservarDos políticas de prune, en PVE y en PBSDejar keep-all=1 y remove 0 en PVE y hacer el prune en PBS
El datastore no se crea sobre un filesystem viejoext3 no soporta 65.538 subdirectoriosUsar ext4, xfs o zfs

Dónde mirar cuando algo falla:

### PVE
pvesh get /nodes/$(hostname)/tasks --limit 50
pvesh get /nodes/$(hostname)/tasks/<UPID>/log --output-format text
ls /var/lib/vz/dump/*.log
journalctl -u pvescheduler -f
pveversion -v

### PBS
journalctl -u proxmox-backup-proxy -f
proxmox-backup-manager task list
proxmox-backup-manager task log <UPID>
proxmox-backup-manager versions --verbose
proxmox-backup-client benchmark        # aisla el cuello de botella: red, CPU o disco

Y el caso extremo, con el servidor PBS muerto pero los discos del datastore intactos: se puede reconstruir un fichero a partir del índice y los chunks.

proxmox-backup-debug recover index \
  /mnt/datastore/store1/vm/101/2026-08-09T01:00:12Z/drive-scsi0.img.fidx \
  /mnt/datastore/store1/.chunks

Requiere que índice y chunks estén íntegros, y las claves si el backup estaba cifrado. Es exactamente la razón por la que conviene documentar el layout del datastore y guardar las claves fuera del sistema respaldado.

Lo que queda operativo

Tienes vzdump entendido hasta sus defaults, los tres modos con sus requisitos reales de storage, fleecing para que el I/O del guest no dependa de la latencia del destino, exclusiones correctas en VM y en CT, hooks que no se rompen por el entorno vacío, y las tres formas de restaurar: completa, en caliente y a nivel de fichero. Del lado de PBS tienes el chunk store, la separación entre prune y garbage collection, verify, sync en la dirección segura, cifrado en cliente con master key, y una integración con PVE cuyo token no puede borrar nada. Y tienes una estrategia 3-2-1 concreta con un checklist para comprobar cada trimestre que no es teoría.

Lo que falta ahora es dejar de escribir todo esto a mano. En el capítulo 14 pasamos a la automatización: la API REST de PVE con tokens y sin CSRF, pvesh, Terraform y OpenTofu con el provider bpg, Ansible con la colección community.proxmox, y Packer para construir plantillas reproducibles. Los jobs de backup, los storages PBS y los tokens de esta sección son exactamente el tipo de recurso que dejarás declarado en código.