If you run any public-facing services on a cloud droplet — a game server behind a tunnel, a small VPS-hosted app — it's easy to end up with zero backup coverage on it without ever noticing. Your hypervisor backs up the VMs and containers it manages; a droplet living on someone else's cloud isn't one of those, so nothing does it automatically. Here's how to get real Proxmox Backup Server coverage on it, plus a genuinely obscure Docker bug that can leave your backups completely 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 entirely — it's just a Linux box somewhere else, reachable over SSH, with no relationship to your home infrastructure at all unless you deliberately build one. It's completely normal to stand one up for some 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's unprotected.
Proxmox Backup Server's client (proxmox-backup-client) only ships Debian packages, with no
native build for other distros. You've got 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's worth being 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 actually completes, every other connection the host was relying on —
its own SSH session, its actual public-facing service — silently vanishes into a tunnel going nowhere.
Scope AllowedIPs to the single /32 address of the host you actually need to
reach, every time, no exceptions. This exact mistake has taken down a live production service before, and
it's a one-line config difference between "works" and "outage."
A simpler option that avoids new tunnels entirely: a firewall rule on the droplet, scoped to your home's static public IP, allowing just the backup agent's port. Combine it with a temporary SSH tunnel for any one-time setup step that needs the reverse direction (registering a new backup target, for instance), closed again immediately after. Nothing about this approach adds a new always-on network path at all — just a narrowly-scoped inbound allowance and a connection you open and close on purpose.
Create a dedicated user and API token specifically 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. If this token ever leaks (it's sitting in a config file on a public-facing cloud host, after all), 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 just point the backup client at its raw data directory.
Copying a live database's files while it's running risks capturing an inconsistent, half-written state —
fine for a static config file, not fine for a database. Use the database's own dump tool instead
(pg_dumpall for Postgres, mysqldump for MySQL/MariaDB) to produce one
transactionally-consistent file, and back up that:
docker compose exec -T postgres pg_dumpall -U youruser > /path/to/dump/postgres.sql
This also happens to make backups smaller and restores dramatically simpler — you get a single SQL file you can pipe straight back into a fresh database, rather than needing to reconstruct a running database engine's exact on-disk state.
A oneshot systemd service plus a timer unit is enough — no need for anything fancier 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 time with a random delay if you're doing this across several droplets pointed at the same PBS instance, so they're not all hitting it at exactly the same second every night.
The root cause: proxmox-backup-client uses O_TMPFILE — an anonymous, unnamed
temporary file — to stage downloaded chunk data. It needs this on every restore, and optionally during
backup itself (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 on cloud droplet images. The result:
Operation not supported (os error
95) — before a single byte of your restored data ever appears, regardless of what directory
you're restoring into. Bind-mounting a real host path as the restore target doesn't help,
because the failure happens on an internal staging path, not your output directory.Error downloading .didx from previous
manifest: Operation not supported. It's logged as non-fatal, easy to scroll past, and it's a real
symptom of the exact same bug that's 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, and
the exact same fix applies to a restore command run through the same image. If you're on the native-package
path instead of Docker, this whole class of bug doesn't exist — one more reason to prefer it when your
droplet's OS allows it.
strace installed, run the failing command under it, and grep the output for
the actual failing syscall rather than trusting the client's own (much less specific) error message.
openat(..., O_TMPFILE, ...) = -1 EOPNOTSUPP is unambiguous in a way "Operation not supported"
on its own never tells you. It's also worth ruling out the obvious suspects first and fast — container
networking mode, bind-mount type, seccomp/AppArmor/capabilities — since all of those are one flag away to
test, and none of them turned out to matter here.
This is the actual point of the section above: a backup job reporting success is not the same claim as "this data is restorable," and the only way those two things can silently diverge is exactly the kind of bug that hides behind a green checkmark for months. After setting this up — and periodically afterward — actually restore something and verify it:
A backup you've never restored from is a hypothesis, not a safety net. Given how easily a bug like the one above can hide for months without a single symptom in your normal backup logs, this isn't a one-time setup step — it's worth repeating any time you change how the client runs.