Two default strategies, both worse than they look.
Provider snapshots. DigitalOcean, Hetzner, Linode and Vultr will all image your droplet nightly for a few dollars. This is genuinely useful and you should probably keep it on. It is also stored in the same account as the server, deleted by the same compromised credentials, and gone if the provider suspends you over a billing dispute. It protects against you breaking the server. It does not protect against losing the account.
A full disk image. Comprehensive, and mostly waste. A 40 GB Ubuntu server holds maybe 2 GB you could not rebuild from a package manager in ten minutes. You are paying to store /usr/lib over and over, and the restore path — write an image to a new disk — is slower and more brittle than reinstalling and restoring the parts that mattered.
The useful question is not "how do I copy the whole disk" but "what on this machine could I not recreate?" On most servers that list is short:
- Configuration you edited by hand
- Data your users created
- The list of what is installed
- Credentials and certificates
- Scheduled jobs
Everything else is a package download.
1. Work out what is actually on the box
Before writing a script, look:
# Biggest directories, excluding the OS
du -h --max-depth=2 /var /opt /srv /home 2>/dev/null | sort -rh | head -20
# What is listening, so you find services you forgot about
ss -tlnp
# What starts on boot
systemctl list-unit-files --state=enabled --type=service
That third command finds things people miss for years — a Redis nobody documented, a cron container, a stray Postgres from a proof of concept that is now load-bearing.
2. Capture the five things
mkdir -p ./server-backup/{etc,data,meta,dumps}
Configuration:
tar cf ./server-backup/etc/etc.tar -C / etc
/etc is small, changes rarely, and is where every hand-edit lives: nginx vhosts, sshd config, systemd units, fstab, netplan. Uncompressed tar so deduplication works — gzip output changes completely after the first altered byte, which makes a barely-changed archive look brand new to a chunker.
Data:
tar cf ./server-backup/data/www.tar -C / var/www
tar cf ./server-backup/data/home.tar -C / home
tar cf ./server-backup/data/apps.tar -C / opt/myapp srv
Adjust to what step 1 actually found.
The install list, not the installs:
# Debian / Ubuntu
apt-mark showmanual > ./server-backup/meta/packages.txt
# RHEL / Fedora / Rocky
# dnf repoquery --userinstalled --qf '%{name}' > ./server-backup/meta/packages.txt
# Third-party repos, or half your packages will not install on restore
tar cf ./server-backup/meta/apt-sources.tar -C / etc/apt/sources.list.d
apt-mark showmanual lists what someone deliberately installed rather than every transitive dependency. Restoring is then one command instead of a 40 GB image.
Scheduled jobs:
for u in $(cut -f1 -d: /etc/passwd); do
crontab -u "$u" -l 2>/dev/null | sed "s/^/# user: $u\n/"
done > ./server-backup/meta/crontabs.txt
systemctl list-timers --all > ./server-backup/meta/timers.txt
User crontabs live in /var/spool/cron and are not in /etc. This is the single most commonly missed item on a server rebuild: everything works, and then a week later someone notices the nightly report stopped arriving in March.
Databases — dump, never copy:
pg_dump --format=custom --username=postgres app_db > ./server-backup/dumps/app_db.dump
mysqldump --single-transaction --user=root app_db > ./server-backup/dumps/app_db.sql
Copying /var/lib/postgresql from a running server gives you a torn file. See the Postgres and MySQL guides for the consistency rules.
3. What to deliberately skip
/proc /sys /dev /run virtual, not real files
/tmp /var/tmp by definition disposable
/var/cache /var/lib/apt re-downloadable
/swapfile gigabytes of nothing
/var/lib/docker images rebuild; volumes need their own handling
/var/lib/mysql /var/lib/postgresql dump these instead, never copy live
/var/lib/docker deserves its own note: backing it up wholesale captures image layers you can pull again and volume data in an inconsistent state. Handle volumes properly — see Docker volume backups.
4. Get it off the server
Everything so far lands in ./server-backup, still on the machine you are protecting. Push it somewhere with credentials the server's provider does not control.
Sign in at Backup Data, claim the free 5 GB workspace, mint an API key scoped backup:write, backup:read, snapshots:read, then:
npm install @lighthouse-web3/baas-js-sdk
export LH_API_KEY="lh_xxxxxxxxxxxxxxxxxxxxxxxx"
export LH_WORKSPACE_ID="your-workspace-uuid"
import { BackupClient, generateKeyfile } from "@lighthouse-web3/baas-js-sdk";
const client = new BackupClient({
apiKey: process.env.LH_API_KEY,
workspaceId: process.env.LH_WORKSPACE_ID,
});
const snapshot = await client.backup(["./server-backup"], {
description: "nightly server backup",
tags: { type: "server", host: "web-01", env: "prod" },
encryption: {
keyfilePath: "/secure/lh.keyfile",
passphrase: process.env.LH_KEYFILE_PASSPHRASE,
},
});
console.log(`snapshotId=${snapshot.snapshotId} totalSize=${snapshot.totalSize}`);
(A Go SDK offers the same surface.)
Encrypt, because /etc is full of secrets — /etc/shadow, TLS private keys under /etc/ssl and /etc/letsencrypt, SSH host keys, application config with database passwords in it. AES-GCM on your machine; the server stores only ciphertext.
You hold the keys. Lose the keyfile and passphrase and the data is unrecoverable, with no server-side reset. Store the keyfile somewhere other than the server it protects — a keyfile on the box that died is a keyfile you do not have.
Because /etc and the package list barely change, deduplication means a nightly run of this uploads almost nothing after the first.
5. Prove you can rebuild
The test for a server backup is not "did the files come back." It is "can I stand up a working replacement from nothing but this?"
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const target = mkdtempSync(join(tmpdir(), "server-restore-drill-"));
await client.restore(snapshot.snapshotId, target, {
onProgress: (e) => console.log(`[${e.phase}] ${e.current}/${e.total}`),
});
Every restore verifies chunk integrity against expected checksums before reassembly.
Then, on a fresh VPS or a local VM with the same OS version:
# 1. Packages
xargs -a "$TARGET/server-backup/meta/packages.txt" apt-get install -y
# 2. Config — selectively. Do NOT untar the whole /etc over a fresh system.
tar xf "$TARGET/server-backup/etc/etc.tar" -C /tmp/etc-restore
cp -r /tmp/etc-restore/etc/nginx/sites-available/* /etc/nginx/sites-available/
cp -r /tmp/etc-restore/etc/letsencrypt /etc/
# 3. Data
tar xf "$TARGET/server-backup/data/www.tar" -C /
# 4. Database
pg_restore --username=postgres --dbname=app_db "$TARGET/server-backup/dumps/app_db.dump"
# 5. Jobs — read crontabs.txt and reinstall them
Restore /etc selectively, not wholesale. Untarring an old /etc over a fresh install overwrites /etc/fstab with UUIDs for disks that do not exist, replaces /etc/passwd with UIDs that do not match, and can leave a machine that will not boot. Copy the directories you actually edited.
Time the whole rebuild with a stopwatch. That wall-clock number is your real RTO — see RPO vs RTO for why the measured number and the assumed number are usually hours apart.
The drill checklist:
- ✅ Rebuild on a fresh machine, never over the live one
- ✅ Install from the package list, do not restore
/usr - ✅ Copy
/etcselectively — never overwritefstab,passwd,shadow - ✅ Reinstall crontabs and timers, and confirm they fire
- ✅ Check file ownership after untarring as root
- ✅ Load a real page over HTTPS, so certificates are tested too
- ✅ Record wall-clock time, keep a log across drills
- ✅ Destroy the test VPS when done, so it stops costing money
6. Schedule it
# /etc/cron.d/server-backup (nightly at 02:00)
0 2 * * * root /opt/backup/server-backup.sh >> /var/log/server-backup.log 2>&1
And set retention, so storage stays bounded:
await client.pruneSnapshots({
sourceId: snapshot.sourceId,
keepLatest: 14,
before: new Date(Date.now() - 30 * 86400_000).toISOString(),
dryRun: true, // drop to false once the preview looks right
});
Then quarterly, do the rebuild drill. Not the file restore — the full rebuild. It is the only thing that tells you whether the list in section 2 is missing something, and it always is the first time.
The pipeline, complete
| Captured | Why not the whole disk |
|---|---|
/etc | The only config that was hand-edited |
/var/www, /home, /opt, /srv | The only data you cannot re-download |
| Package list | One command rebuilds /usr, at 1/1000th the bytes |
| Crontabs and timers | Not in /etc, and the most-missed item |
| Database dumps | A live file copy is a torn file |
| Off-provider copy | Provider snapshots die with the account |
Keep the provider snapshots. They are the fastest way back from "I broke the server at 2am." They just are not the answer to "the account is gone," and those are different questions with different answers.
Start with the free tier: backupdata.io has 5 GB free, no card, and the 10-minute quickstart gets you to a first verified snapshot today.