Backups: vzdump, Proxmox Backup Server y la estrategia 3-2-1
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.
| Elemento | Ruta o endpoint |
|---|---|
| Binario | /usr/bin/vzdump |
| Configuración global | /etc/vzdump.conf |
| Jobs programados | /etc/pve/jobs.cfg |
| Demonio que ejecuta los jobs | pvescheduler |
| Ejecutar un backup por API | POST /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:
| Modo | Downtime | Consistencia | Mecanismo |
|---|---|---|---|
stop | Alto | Máxima | Apagado ordenado del guest. La VM vuelve a arrancar en cuanto el backup empieza a copiar, no cuando termina |
suspend | Medio-alto | No necesariamente mejor que snapshot | Suspende la VM y luego llama internamente al modo snapshot. Existe solo por compatibilidad |
snapshot (default) | Mínimo | Buena si hay QEMU Guest Agent | Live 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:
| Modo | Downtime | Requisito | Espacio extra |
|---|---|---|---|
stop | Alto, todo el backup | Ninguno | No |
suspend | Mínimo | Espacio en --tmpdir | Sí, una copia completa |
snapshot (default) | Mínimo, un freeze breve | Todos los volúmenes respaldados en storage con snapshots | El 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ón | Tipo | Default | Descripción |
|---|---|---|---|
--all | boolean | 0 | Respalda todos los guests conocidos del host |
--bwlimit | integer | 0 | Límite de I/O en KiB/s. 0 es sin límite |
--compress | 0|1|gzip|lzo|zstd | 0 | Algoritmo de compresión |
--dumpdir | string | — | Directorio destino, excluyente con --storage |
--exclude | string | — | Excluye guests concretos, implica --all |
--exclude-path | array | — | Excluye ficheros o directorios con globs de shell |
--fleecing | [enabled=<1|0>][,storage=<ID>] | — | Backup fleecing, solo VM |
--ionice | integer 0-8 | 7 | Prioridad de I/O con el scheduler BFQ. 8 es clase idle |
--job-id | string | — | ID del job, se guarda en metadatos |
--lockwait | integer | 180 | Espera máxima por el lock global, en minutos |
--mailnotification | always|failure | always | Deprecado |
--mailto | string | — | Deprecado |
--mode | snapshot|stop|suspend | snapshot | Modo de backup |
--node | string | — | Solo se ejecuta si corre en ese nodo |
--notes-template | string | — | Plantilla de las notas del backup |
--notification-mode | auto|legacy-sendmail|notification-system | auto | Sistema de notificación |
--pbs-change-detection-mode | legacy|data|metadata | — | Detección de cambios en CT hacia PBS |
--performance | [max-workers=<int>][,pbs-entries-max=<int>] | — | Ajustes de rendimiento |
--pigz | integer | 0 | Usa pigz en vez de gzip cuando es mayor que 0 |
--pool | string | — | Respalda todos los guests de un pool |
--protected | boolean | — | Marca el backup como protegido |
--prune-backups | keep-* | keep-all=1 | Política de retención |
--quiet | boolean | 0 | Modo silencioso |
--remove | boolean | 1 | Purga backups viejos según prune-backups |
--script | string | — | Hook script |
--stdexcludes | boolean | 1 | Excluye ficheros temporales y logs |
--stdout | boolean | — | Escribe el tar a stdout |
--stop | boolean | 0 | Detiene los jobs de backup en curso en este host |
--stopwait | integer | 10 | Espera máxima al apagado del guest, en minutos |
--storage | storage ID | — | Storage destino |
--tmpdir | string | — | Directorio de ficheros temporales |
--zstd | integer | 1 | Hilos 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:
- Valores definidos en el job de
/etc/pve/jobs.cfg. - Flags pasados en la línea de comandos.
/etc/vzdump.conf.- 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
| Algoritmo | Extensión | Multihilo | Comentario |
|---|---|---|---|
zstd | .zst | Sí, con --zstd N | El más rápido y el recomendado. Default de facto en los jobs nuevos de la GUI |
gzip | .gz | Solo vía pigz con --pigz N | Más universal, más lento |
lzo | .lzo | No | Legado. Muy rápido, ratio pobre |
0 | sin extensión | — | Sin 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
| Segmento | Ejemplo | Significado |
|---|---|---|
| 1 | vzdump | Prefijo fijo |
| 2 | qemu | Tipo de guest: qemu, lxc u openvz legado |
| 3 | 101 | VMID |
| 4 | 2026_08_09-03_00_12 | Timestamp YYYY_MM_DD-HH_MM_SS |
| 5 | .vma | Contenedor: .vma para VM, .tar para CT |
| 6 | .zst | Extensió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 | Ámbito | Cuándo |
|---|---|---|
job-init | job | Antes de nada, antes incluso de activar los storages |
job-start | job | El job arranca de verdad |
backup-start | guest | Empieza el backup de un guest |
pre-stop | guest | Justo antes de parar o suspender el guest |
pre-restart | guest | Justo antes de reanudar o arrancar el guest |
post-restart | guest | El guest ya está online otra vez |
backup-end | guest | Archivo creado con éxito. Aquí TARGET es válido |
backup-abort | guest | El backup de ese guest falló |
log-end | guest | Log escrito. Aquí LOGFILE es válido |
job-end | job | Job terminado con éxito |
job-abort | job | Job abortado |
Variables de entorno disponibles:
| Variable | Disponible en | Contenido |
|---|---|---|
STOREID | cuando hay --storage | ID del storage destino |
DUMPDIR | cuando hay --dumpdir | Directorio destino |
VMTYPE | fases de guest | qemu o lxc |
HOSTNAME | fases de guest | Nombre del guest |
TARGET | backup-end y posteriores | Ruta del fichero de backup resultante |
LOGFILE | log-end | Ruta 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:
| Modo | Formato | Comportamiento |
|---|---|---|
legacy (default) | pxar v1 | Lee y codifica todos los ficheros en cada backup |
data | pxar v2, .mpxar más .ppxar | Separa metadatos y payload en streams distintos |
metadata | pxar v2 | Compara 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ón | Default | Descripción |
|---|---|---|
--bwlimit | del storage o datacenter | Límite de I/O en KiB/s |
--force | — | Permite sobrescribir una VM existente |
--ha-managed | 0 | Registra la VM restaurada como recurso HA |
--live-restore | — | Arranca la VM ya y restaura en segundo plano. Solo PBS |
--pool | — | Pool destino |
--start | 0 | Arranca la VM al terminar |
--storage | el original | Storage destino |
--unique | — | Genera 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.
| Schedule | Se dispara |
|---|---|
03:00 | Todos los días a las 3 de la mañana |
mon..fri 22 | Días laborables a las 22:00 |
sun 04:30 | Domingos a las 4:30 |
*:0/15 | Cada 15 minutos |
mon..fri 8..17,22:0/15 | Cada 15 min entre las 8 y las 17 y también en la hora 22, solo laborables |
*-*-01 05:00 | El día 1 de cada mes a las 5:00 |
sat *-1..7 15:00 | Primer sábado de cada mes a las 15:00 |
2026-10-21 | Una 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ón | Significado |
|---|---|
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=1 | Guardar 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:
| Valor | Comportamiento |
|---|---|
auto (default) | Si el job tiene mailto, usa legacy-sendmail; si no, el sistema nuevo |
legacy-sendmail | Envía por sendmail a la dirección del job |
notification-system | Usa 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/:
| Fichero | Contenido |
|---|---|
datastore.cfg | Definición de datastores |
remote.cfg | Servidores PBS remotos |
sync.cfg | Sync jobs |
verification.cfg | Verify jobs |
prune.cfg | Prune jobs |
user.cfg / acl.cfg | Usuarios y ACLs con formato acl:propagate:path:user:role |
tape.cfg, tape-job.cfg, media-pool.cfg | Cinta |
s3.cfg | Endpoints S3 |
node.cfg | Config del nodo: fingerprint, ACME |
notifications.cfg | Targets 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ón | Valores | Default | Efecto |
|---|---|---|---|
chunk-order | inode / none | inode | inode 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-level | none / filesystem / file | filesystem | none 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-capacity | entero | 1048576 | Slots 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-workers | entero | 4 | Hilos de verificación por defecto |
default-verification-readers | entero | 1 | Hilos 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ámetro | Default | Descripción |
|---|---|---|
ignore-verified | — | Salta snapshots ya verificados con éxito |
outdated-after | — | Re-verifica los que lleven más de N días sin verificar |
schedule | — | Evento de calendario |
read-threads | 1, rango 1-32 | Hilos de lectura de chunks |
verify-threads | 4, rango 1-32 | Hilos 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:
| Fichero | Contenido |
|---|---|
/etc/pve/priv/storage/<STORAGE-ID>.pw | Contraseña del usuario PBS |
/etc/pve/priv/storage/<STORAGE-ID>.enc | Clave AES-256-GCM |
/etc/pve/priv/storage/<STORAGE-ID>.master.pem | Clave 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
| Componente | Recomendación |
|---|---|
| CPU | 64 bits moderno, mínimo 4 cores |
| RAM | 4 GB para sistema, cachés y daemons más 1 GB por cada TB de almacenamiento de backup |
| Disco de SO | 32 GB libres o más, con RAID hardware con BBU o ZFS redundante |
| Almacenamiento de backup | SSD enterprise preferido |
| Si son HDD | ZFS special device en mirror para los metadatos |
| Red | NICs 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.
| Escenario | Veredicto |
|---|---|
| SSD enterprise puro | Ideal, sin sorpresas |
| HDD más ZFS más special device mirror SSD más 1 GB RAM por TB | Correcto y económico. Es la receta estándar |
| HDD más ZFS sin special device y poca RAM | Mala idea. GC y verify se vuelven inviables al crecer |
| HDD más ext4 o xfs sin caché de metadatos | Funciona razonablemente en datastores pequeños, sin las garantías de integridad de ZFS |
| ZFS sobre RAID hardware | No soportado. Riesgo de pérdida de datos |
| PBS dentro del mismo host PVE que respalda | No 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.
| Copia | Medio | Implementación |
|---|---|---|
| 1 | NVMe o SSD del nodo | ZFS mirror o LVM-thin |
| 2 | HDD distinto o NAS | PBS en un mini PC aparte, datastore en HDD con special device SSD |
| 3 | USB rotado, PBS de un familiar o S3 barato | Datastore 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.
| Requisito | Homelab | PyME |
|---|---|---|
| Copias | 2-3 | 3-4 |
| PBS dedicado | Deseable | Obligatorio, hardware separado |
| Dirección del sync offsite | Push aceptable | Pull obligatorio |
| Cifrado en cliente | Recomendado si el offsite es de terceros | Obligatorio si sale de la organización |
| Token de PVE con prune | Aceptable | Prohibido |
| Verify | Mensual | Diario incremental más mensual completo |
| Prueba de restauración | Anual | Trimestral y documentada |
| Air gap | Disco USB desconectado | Cinta exportada u Object Lock |
Checklist de verificacion trimestral
proxmox-backup-manager verify prodtermina sin errores.- Restaurar una VM completa a un VMID nuevo en un nodo de pruebas con
qmrestore ... --unique 1. - Restaurar un fichero suelto con File Restore.
- Restaurar desde el PBS secundario, no solo desde el primario.
- Comprobar que la clave de cifrado está accesible desde fuera del sistema respaldado.
- Leer una cinta al azar del archivo con
proxmox-tape catalogy una restauración de prueba. - Provocar un fallo a propósito y confirmar que la notificación llega de verdad.
Errores comunes y diagnostico
| Síntoma | Causa probable | Diagnóstico o solución |
|---|---|---|
unable to create snapshot ... storage does not support snapshots en un CT | Un mount point vive en un storage sin snapshots | pct config <id>; usar mode=suspend o backup=0 en ese mp |
VM is locked (backup) y no se puede hacer nada | Un backup anterior murió sin liberar el lock | Comprobar que no hay tarea viva con ps aux | grep vzdump, luego qm unlock <vmid> o pct unlock <vmid> |
| El job programado no se ejecuta | pvescheduler parado, o el campo node del job no coincide | systemctl status pvescheduler; journalctl -u pvescheduler -n 100; revisar jobs.cfg |
| El schedule no dispara cuando esperas | Sintaxis del evento de calendario | systemd-analyze calendar --iterations=5 '<tu schedule>' |
| El prune no libera espacio en PBS | Falta la garbage collection | proxmox-backup-manager garbage-collection start <store> |
| La GC aborta con error de atime | Datastore montado con noatime | mount | grep datastore y remontar con relatime. No desactives gc-atime-safety-check |
| La GC tarda horas en HDD | Metadatos en disco mecánico | Añadir special vdev mirror SSD; subir gc-cache-capacity |
storage 'pbs01' is not online | Red, fingerprint, ACL o reloj | pvesm status --storage pbs01; curl -k https://pbs:8007/; timedatectl en ambos |
403 permission check failed desde PVE a PBS | El token no tiene ACL en el datastore o namespace | proxmox-backup-manager acl list y añadir DatastoreBackup |
El backup llena /var/lib/vz en modo suspend de un CT | tmpdir por defecto en el disco de sistema | tmpdir: /mnt/rapido en /etc/vzdump.conf |
| Backup lentísimo con destino remoto | Copy-before-write bloqueando al guest | Activar fleecing con storage local rápido |
| El fleecing empeora las cosas | Storage de fleecing lento o sin thin provisioning | Usar LVM-thin, ZFS sparse o RBD local |
| Los CT tardan lo mismo cada noche aunque no cambie nada | Modo de detección legacy | --pbs-change-detection-mode metadata |
ionice no tiene ningún efecto | El scheduler no es BFQ | cat /sys/block/<dev>/queue/scheduler y cambiar a bfq |
El hook script no encuentra curl o ssh | vzdump vacía %ENV con local %ENV; | Exportar PATH al inicio del hook y usar rutas absolutas |
| El hook script aborta el backup | Có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 rota | La restauración falló a medias | Borrar la VM y restaurar de forma normal. No hay recuperación parcial |
| File Restore lista los discos pero no entra en las particiones | Falta proxmox-backup-restore-image o no hay /dev/kvm | apt install proxmox-backup-file-restore; verificar nested virt |
| Desaparecen snapshots que esperabas conservar | Dos políticas de prune, en PVE y en PBS | Dejar keep-all=1 y remove 0 en PVE y hacer el prune en PBS |
| El datastore no se crea sobre un filesystem viejo | ext3 no soporta 65.538 subdirectorios | Usar 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.