Run any public-facing service on a cloud droplet and it is easy to end up with zero backup coverage without noticing. A game server behind a tunnel, a small VPS-hosted app. Your hypervisor backs up the VMs and containers it manages. A droplet living on someone else's cloud is not one of those, so nothing backs it up automatically. Here is how to get real Proxmox Backup Server coverage on it, plus an obscure Docker bug that leaves your backups unrestorable while looking perfectly healthy.
A Proxmox-hosted VM or container gets backed up because your hypervisor knows it exists and manages its disk directly. A DigitalOcean droplet is a different animal. It is a Linux box somewhere else, reachable over SSH, with no relationship to your home infrastructure unless you build one. It is completely normal to stand one up for a public-facing purpose, get it working, move on, and never circle back to backups. Nothing about the droplet running fine day to day tells you it is unprotected.
Proxmox Backup Server's client, proxmox-backup-client, only ships Debian packages. There is no
native build for other distros. That leaves two real options on a droplet:
Your PBS instance lives on your home network. The droplet is out on the public internet. You need a path between them, and it pays to be deliberate about which one you pick.
AllowedIPs setting controls more than which packets go through
it. A 0.0.0.0/0 full-tunnel config makes wg-quick build policy routing that
redirects all outbound traffic from that host into the tunnel, not just traffic meant for the new
peer. If that peer's handshake never completes, every other connection the host relied on vanishes into a
tunnel going nowhere. Its own SSH session. Its public-facing service. Scope AllowedIPs to the
single /32 address of the host you need to reach. Every time, no exceptions. This exact mistake
has taken down a live production service before, and it is a one-line difference between "works" and
"outage".
A simpler option avoids new tunnels entirely. Put a firewall rule on the droplet, scoped to your home's static public IP, allowing just the backup agent's port. For any one-time setup step needing the reverse direction, like registering a new backup target, open a temporary SSH tunnel and close it right after. This adds no always-on network path at all. Just a narrow inbound allowance and a connection you open and close on purpose.
Create a dedicated user and API token for this one droplet's backups, scoped to write new backups to its own namespace only. No read access to other hosts' backups. No delete. No admin rights. That token sits in a config file on a public-facing cloud host, so assume it leaks. The blast radius should be "someone can write junk backup data", not "someone can read or destroy every other backup on your PBS instance".
If the droplet runs a database, resist the urge to point the backup client at its raw data directory.
Copying a live database's files while it runs captures an inconsistent, half-written state. Fine for a
static config file. Not fine for a database. Use the database's own dump tool to produce one
transactionally consistent file, and back up that. pg_dumpall for Postgres,
mysqldump for MySQL and MariaDB:
docker compose exec -T postgres pg_dumpall -U youruser > /path/to/dump/postgres.sql
It also makes backups smaller and restores dramatically simpler. You get a single SQL file to pipe straight into a fresh database, instead of reconstructing a running database engine's exact on-disk state.
A oneshot systemd service plus a timer unit is enough. Nothing fancier is needed on a single droplet:
[Unit]
Description=Backup my-droplet to Proxmox Backup Server
[Service]
Type=oneshot
ExecStart=/bin/bash /root/pbs-backup/run-backup.sh
Stagger the timer's start with a random delay if you do this across several droplets pointed at the same PBS instance. Otherwise they all hit it at the same second every night.
Here is the root cause. proxmox-backup-client uses O_TMPFILE, an anonymous
unnamed temporary file, to stage downloaded chunk data. It needs that on every restore, and optionally
during backup, to look up the previous backup's chunk index for incremental deduplication. Docker's default
overlay2 storage driver does not support O_TMPFILE on a container's own writable
layer, on at least some kernel versions still common in cloud droplet images. Two consequences:
Operation not supported (os error
95). It fails before a single byte of restored data appears, whatever directory you restore
into. Bind-mounting a real host path as the restore target does not help. The failure happens on
an internal staging path, not your output directory.Error downloading .didx from previous manifest: Operation not supported. It is
logged as non-fatal and easy to scroll past. It is a real symptom of the exact bug about to make your
restores fail completely.The fix: bind-mount real host directories (on your droplet's actual filesystem, not the
container's overlay layer) over /tmp and /root/.cache inside the container:
docker run --rm --network host \
-v /root/pbs-backup/container-tmp:/tmp \
-v /root/pbs-backup/container-cache:/root/.cache \
--env-file /root/pbs-backup/pbs.env \
-v /path/to/data:/backup-source/data:ro \
your-pbs-client-image:latest backup \
data.pxar:/backup-source/data \
--backup-id my-droplet --ns my-droplet
Both paths now land on the droplet's real filesystem, where O_TMPFILE works normally. The same
fix applies to a restore command run through the same image. On the native-package path this whole class of
bug does not exist. One more reason to prefer it when your droplet's OS allows it.
strace installed. Run the failing command under it. Grep the output for the failing
syscall instead of trusting the client's much less specific error message.
openat(..., O_TMPFILE, ...) = -1 EOPNOTSUPP is unambiguous in a way "Operation not supported"
never is. Rule out the obvious suspects first and fast. Container networking mode, bind-mount type,
seccomp, AppArmor, capabilities. All of those are one flag away to test, and none of them mattered here.
That is the point of the section above. "The backup job reported success" and "this data is restorable" are two different claims. The only thing that makes them diverge silently is exactly the kind of bug that hides behind a green checkmark for months. Restore something and verify it, when you set this up and periodically after:
A backup you have never restored from is a hypothesis, not a safety net. A bug like the one above hides for months without a single symptom in your normal backup logs. So this is not a one-time setup step. Repeat it any time you change how the client runs.