Backing up a VPS: restore it, then copy off-site

What FFxF backs up for you, what a restore brings back and what it wipes, then an off-site copy with restic tested on two VMs: a directory deleted, restored, and every file checked.

A machine, its restore points stacked on separate storage, and a third copy heading elsewhere

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).

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:

bash
$ 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:

bash
$ 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:

bash
$ 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:

bash
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.

config
# /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:

bash
$ 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:

bash
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:

bash
$ 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):

restic.env
# /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
bash
$ 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 check now 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.

A laptop linked to a Windows server's screen, the pointer resting inside it

Connect to your Windows VPS with Remote Desktop

The delivery email gives the address, the Administrator user and the password. The rest takes five minutes: which client to open on your machine, what the certificate warning means, what to check when the connection fails, and what to set before leaving the server on its own.

Read the article 7 min read

A console shows a network boot menu with one line selected; below it, a still-empty disk waits for the install

netboot.xyz: install the operating system you want

An FFxF server can boot netboot.xyz, when you order it or reinstall it: an empty disk, the menu in the console, and the system of your choice. The whole path tested with Debian 13, and what to know about the network, which you type in by hand.

Read the article 6 min read

Support & discussions

Technical questions, incident reports, or infrastructure discussions, the team is reachable on Discord, Telegram, X, Instagram, Reddit, and IRC.