VPS data is rarely lost all at once. More often it is an rm -rf in the wrong
directory, an update that breaks an application, or a database emptied by a careless
script. Recovering from that takes two things: a recent restore point, and a copy that
depends on neither the machine nor its host.
This post covers what FFxF backs up for you, how restoring works, and how to set up your own off-site copy with restic. The restic part was tested end to end on two hourly Nano VMs running Debian 13: a 1 GiB backup, a deleted directory, a restore, then a check of every file. The timings and the output are the real ones.
1. What FFxF backs up for you
Every VM gets two kinds of backups. Both copy the machine's whole disk to a backup server separate from the hypervisor (Proxmox Backup Server).
| Manual backups | Automatic backups | |
|---|---|---|
| Price | included | 3 CAD per month per VM |
| Triggered by | you, from the console | a nightly run |
| How many are kept | up to 7 | the last 7 nights |
| Deleting | you do it, to free a slot | they rotate on their own and cannot be deleted |
| Machine stopped? | no, the copy is taken live | |
In the console, open the machine, go to Backups and click
Create Backup. Name it after the reason it exists, such as
before-php-upgrade. Only the blocks that changed since the previous backup
are stored, so the second and later backups are faster than the first. Automatic backups
do not count against your 7 slots.
Creating a backup is a console-only action. The API can
list backups, restore one and delete the ones you took; the last two need the
backups.write scope.
2. Before the first one: install the QEMU agent
A live backup copies a disk that is still being written to. With the QEMU guest agent running in the VM, the hypervisor freezes the filesystems for a moment as the copy starts, so what is on disk is consistent. Without it you get the equivalent of a power cut: ext4 recovers from that, a database does not always.
On our Debian 13 test machine the agent channel was there, but the package was not installed. Our Debian 13 image now includes it (fixed after this test); on an older machine or another system, check:
$ dpkg -l qemu-guest-agent
dpkg-query: no packages found matching qemu-guest-agent
$ ls /dev/virtio-ports/
org.qemu.guest_agent.0
$ sudo apt-get install -y qemu-guest-agent
$ systemctl is-active qemu-guest-agent
active
For a database, go one step further: a scheduled pg_dump or
mysqldump before the backup gives you a file you can load anywhere, without
restoring the whole machine.
3. Restoring: the whole disk, in one go
On the Backups page, Restore asks you to tick an explicit confirmation: the restore replaces the entire current disk. The machine is stopped while it runs, then comes back in the state of the backup. Through the API, the listing for a machine with no backups yet looks like this:
$ curl -s -H "Authorization: Bearer $FFXF_TOKEN" \
https://api.ffxf.net/v1/vms/315/backups
{"data":[],"meta":{"automatic_enabled":false,"retention":7}}
Each entry in data has an id, a type
(manual or automatic), a date and a size. A restore takes that
id as is and returns an action you can poll:
$ curl -s -X POST -H "Authorization: Bearer $FFXF_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
"https://api.ffxf.net/v1/vms/315/backups/<id>/restore"
Here is the catch: you cannot get a single directory back. If you deleted
/srv/app/uploads at 3 p.m. and the backup is from last night, restoring the
machine also wipes everything written elsewhere since then. To recover one file without
rolling everything back, you need a file-level copy, which is what the next section
builds.
4. An off-site copy with restic
restic encrypts, deduplicates and sends your files to a repository: an SFTP server, S3-compatible storage, a disk. For the test, the repository was a second Nano VM that accepts one thing only: SFTP, locked into a single directory.
On the machine that receives the copies:
sudo useradd --create-home --shell /usr/sbin/nologin sauvegarde
sudo install -d -m 755 -o root -g root /srv/sftp
sudo install -d -m 700 -o sauvegarde -g sauvegarde /srv/sftp/restic
sudo install -d -m 700 -o sauvegarde -g sauvegarde /home/sauvegarde/.ssh
sudo install -m 600 -o sauvegarde -g sauvegarde restic.pub /home/sauvegarde/.ssh/authorized_keys
sudo tee /etc/ssh/sshd_config.d/sauvegarde.conf <<'EOF'
Match User sauvegarde
ForceCommand internal-sftp
ChrootDirectory /srv/sftp
AllowTcpForwarding no
X11Forwarding no
EOF
sudo sshd -t && sudo systemctl reload ssh
restic.pub is the public key created on the machine being backed up, with
sudo ssh-keygen -t ed25519 -N "" -f /root/.ssh/restic. Still on that
machine, as root: an SSH alias for the repository, then the repository password and the
initialisation.
# /root/.ssh/config
Host depot
HostName 2001:db8:100::5c
User sauvegarde
IdentityFile /root/.ssh/restic
# as root
apt-get install -y restic
openssl rand -base64 32 > /root/.restic-pass && chmod 600 /root/.restic-pass
export RESTIC_REPOSITORY=sftp:depot:/restic
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic init
Because of the chroot, the client sees the repository as /restic. The first
backup covered 2,002 test files (2,000 small files, a 1 GiB random file and a config
file) plus /etc:
$ time restic backup /srv/app /etc
no parent snapshot found, will read all files
Files: 2565 new, 0 changed, 0 unmodified
Dirs: 161 new, 0 changed, 0 unmodified
Added to the repository: 1.003 GiB (1.001 GiB stored)
processed 2565 files, 1.002 GiB in 0:26
snapshot 6a6ad570 saved
real 0m26.571s
26.6 seconds, 19 of them CPU time: on a single vCPU, encryption is the bottleneck, not the network. Random data does not compress, hence the 1.001 GiB stored. After adding three files, the next run only reads what changed:
Files: 3 new, 0 changed, 2565 unmodified
Added to the repository: 732.947 KiB (136.792 KiB stored)
processed 2568 files, 1.002 GiB in 0:00
real 0m1.005s
5. Break it, restore it, check it
Before breaking anything, every test file's hash went into
/root/temoin.sha256 with sha256sum. Then the deletion:
$ sudo rm -rf /srv/app/uploads
$ ls /srv/app
config
$ time restic restore latest --target / --include /srv/app/uploads
restoring snapshot 2f2d626d of [/srv/app /etc] at 2026-11-03 16:10:10.034756322 +0000 UTC by root@blog-04-sauvegardes-src to /
Summary: Restored 2007 / 2005 files/dirs (1.000 GiB / 1.000 GiB) in 0:09
real 0m10.775s
$ cd /srv/app && sha256sum -c /root/temoin.sha256 | grep -c ': OK'
2002
The directory is back in 10.8 seconds, all 2,002 hashes match, and nothing else on the machine moved. That is the difference from a disk restore: you choose what comes back.
Next, make it run on its own. A systemd service and timer, with the variables in
/etc/restic.env (mode 600):
# /etc/restic.env
RESTIC_REPOSITORY=sftp:depot:/restic
RESTIC_PASSWORD_FILE=/root/.restic-pass
HOME=/root
# /etc/systemd/system/restic-backup.service
[Unit]
Description=Off-site restic copy
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/restic.env
ExecStart=/usr/bin/restic backup /srv/app /etc
ExecStart=/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Off-site restic copy, nightly
[Timer]
OnCalendar=*-*-* 04:30
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now restic-backup.timer
$ sudo systemctl start restic-backup.service
$ systemctl show restic-backup.service -p Result
Result=success
$ sudo restic check
no errors were found
Our Debian 13 machine ran on UTC, so 04:30 means 04:30 UTC; check with
systemctl list-timers. forget --prune keeps 7 daily, 4 weekly
and 6 monthly snapshots and frees the rest.
6. The 3-2-1 rule, applied
Three copies of your data, on two different media, one of them off-site. For a VM at FFxF, that means:
- Copy 1: the VM's disk, in production.
- Copy 2: FFxF backups, on a separate backup server, to recover quickly from a mistake.
- Copy 3: the restic repository, somewhere other than us: another host, object storage, or a NAS at home.
Our test put the repository on a second FFxF VM in the same datacentre. That is fine for checking the mechanics; it is not an off-site copy. Keep the repository password off the machine being backed up too, in a password manager: without it, the repository cannot be read, by you or anyone else.
7. Limits, and what it costs
- Destroying the VM destroys its backups. Manual and automatic backups alike are deleted with the machine. Before you cancel, copy what matters elsewhere.
- So does an unpaid invoice. On the 14th day overdue, the VM and its backups are destroyed (terms, section 2.6).
- Not a disaster recovery plan. Our backups are meant for operator mistakes. They live on the same infrastructure as the VM, hence copy 3.
- All or nothing. An FFxF restore replaces the whole disk. No single-file restore, and no restore onto another machine.
- Short retention. At most 7 manual backups, 7 nights of automatic ones. To go back six months, that is restic's job.
- restic needs watching. A full repository or a changed SSH host key
makes the service fail, and nobody notices unless something is monitoring it. Run
restic checknow and then, and actually restore a directory once a quarter.
On price: manual backups are included, the automatic option is 3 CAD per month per VM. A Nano used as a repository costs 0.014 CAD an hour IPv6-only, or 6.50 CAD a month on the monthly plan, with 20 GB of disk. Our two test machines lived for a few minutes and were each billed one hour, 0.028 CAD in total.