How to back up a VPS with Restic before it fails
A VPS feels solid until the day it does not: a bad deploy, a deleted volume, a compromised app, a full disk, a failed upgrade, or simply human error. The fix is not heroic incident response. The fix is boring: encrypted offsite backups, automatic schedules, retention, and regular restore tests.
Contents
The backup plan
The goal is not "a backup command ran once". The goal is this:
- Back up the data that matters. Usually that means app directories, Compose files, config, uploaded files, database dumps, SSH/access config, and notes needed to rebuild.
- Store it offsite. A backup on the same VPS is useful for quick mistakes, but useless if the server is gone. Put the real copy somewhere else.
- Encrypt before upload. Restic encrypts repositories, so the storage provider should not need to be trusted with plaintext.
- Automate it. Manual backups happen exactly until the week you are tired.
- Restore-test it. The only proof of a backup is a restore that worked.
Install Restic
On Ubuntu or Debian, the fastest start is the package manager:
sudo apt update
sudo apt install -y restic
restic version
Create a private place for Restic config and a strong repository password:
sudo mkdir -p /etc/restic /var/backups/pre-restic
sudo chmod 700 /etc/restic /var/backups/pre-restic
openssl rand -base64 48 | sudo tee /etc/restic/password >/dev/null
sudo chmod 600 /etc/restic/password
Choose an offsite repository
Restic calls the backup destination a repository. It can be a local path, but for a real VPS backup you normally want a remote repository.
Option A: SFTP on another server
SFTP is simple and works well if you have another server or storage box. Create an SSH key for backups, install the public key on the backup host, then configure Restic:
sudo tee /etc/restic/env >/dev/null <<'EOF'
export RESTIC_REPOSITORY="sftp:backupuser@backup.example:/srv/restic/my-vps"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
EOF
sudo chmod 600 /etc/restic/env
Option B: S3-compatible object storage
Many storage providers expose an S3-compatible API. Keep the access key limited to the backup bucket where possible:
sudo tee /etc/restic/env >/dev/null <<'EOF'
export RESTIC_REPOSITORY="s3:https://s3.example.com/my-bucket/my-vps"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
export AWS_ACCESS_KEY_ID="replace-me"
export AWS_SECRET_ACCESS_KEY="replace-me"
EOF
sudo chmod 600 /etc/restic/env
Initialize the repository once:
sudo bash -c 'source /etc/restic/env && restic init'
Back up files without junk
Do not blindly back up the whole filesystem. You want the parts needed to rebuild the machine, not pseudo-filesystems, caches, temporary files, or Docker overlay layers.
Create an exclude file:
sudo tee /etc/restic/excludes >/dev/null <<'EOF'
/dev
/proc
/sys
/run
/tmp
/mnt
/media
/var/cache
/var/tmp
/var/lib/docker/overlay2
/var/lib/docker/tmp
*.sock
EOF
sudo chmod 600 /etc/restic/excludes
Run the first backup manually:
sudo bash -c 'source /etc/restic/env && \
restic backup \
/etc \
/home \
/root \
/opt \
/srv \
/var/backups/pre-restic \
--exclude-file /etc/restic/excludes \
--tag vps'
Then list snapshots and check repository metadata:
sudo bash -c 'source /etc/restic/env && restic snapshots'
sudo bash -c 'source /etc/restic/env && restic check'
Docker volumes and databases
For Docker, the rule is simple: containers are replaceable, persistent data is not. Back up your `docker-compose.yml`, `.env.example`, Caddy/Nginx configs, and the bind mounts or named volumes that hold app data.
Find named volumes
docker volume ls
docker volume inspect volume_name --format '{{ .Mountpoint }}'
If the volume contains normal app files, export it to a tarball before Restic runs:
sudo docker run --rm \
-v volume_name:/data:ro \
-v /var/backups/pre-restic:/backup \
busybox \
tar czf /backup/volume_name.tgz -C /data .
For databases, prefer a real database dump. A filesystem copy of a live database can be inconsistent unless the database is stopped or snapshotted correctly.
PostgreSQL example
sudo docker exec postgres \
pg_dumpall -U postgres \
| sudo tee /var/backups/pre-restic/postgres.sql >/dev/null
MySQL/MariaDB example
sudo docker exec mysql \
mysqldump --all-databases --single-transaction -uroot -p'REPLACE_PASSWORD' \
| sudo tee /var/backups/pre-restic/mysql.sql >/dev/null
Automate with systemd
Create a small script that prepares dumps, runs Restic, applies retention, and checks the repository.
sudo tee /usr/local/sbin/restic-vps-backup >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
source /etc/restic/env
mkdir -p /var/backups/pre-restic
# Optional: add your database dumps or Docker volume exports here.
# Example:
# docker exec postgres pg_dumpall -U postgres > /var/backups/pre-restic/postgres.sql
restic backup \
/etc \
/home \
/root \
/opt \
/srv \
/var/backups/pre-restic \
--exclude-file /etc/restic/excludes \
--tag vps
restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--prune
restic check
EOF
sudo chmod 700 /usr/local/sbin/restic-vps-backup
Add a systemd service:
sudo tee /etc/systemd/system/restic-vps-backup.service >/dev/null <<'EOF'
[Unit]
Description=Restic VPS backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-vps-backup
EOF
Add a daily timer:
sudo tee /etc/systemd/system/restic-vps-backup.timer >/dev/null <<'EOF'
[Unit]
Description=Run Restic VPS backup daily
[Timer]
OnCalendar=*-*-* 03:20:00
RandomizedDelaySec=20m
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now restic-vps-backup.timer
systemctl list-timers restic-vps-backup.timer
Run it once and inspect the logs:
sudo systemctl start restic-vps-backup.service
journalctl -u restic-vps-backup.service -n 80 --no-pager
Test a restore
This is the part most people skip, and the part that matters most.
Restore the latest snapshot into a temporary directory:
sudo mkdir -p /tmp/restore-test
sudo bash -c 'source /etc/restic/env && \
restic restore latest --target /tmp/restore-test'
Check that key files are present:
ls -la /tmp/restore-test/etc
ls -la /tmp/restore-test/opt
ls -la /tmp/restore-test/var/backups/pre-restic
For a database, test that the dump can at least be read:
head -n 20 /tmp/restore-test/var/backups/pre-restic/postgres.sql
For a Docker volume tarball, test-list it:
tar tzf /tmp/restore-test/var/backups/pre-restic/volume_name.tgz | head
What to keep with every app
For each service you host, keep a small recovery bundle:
- `docker-compose.yml` or deployment script.
- `.env.example` with variable names, not secrets.
- Where the data lives: bind mount path or Docker volume name.
- Database dump command and restore command.
- DNS records and ports needed to bring it back.
- Backup schedule and the last successful restore-test date.
Security notes
- Use a storage account dedicated to backups, not your main personal account.
- Restrict storage credentials to one bucket/path when your provider supports it.
- Keep the Restic password outside the VPS, ideally in an offline password manager entry.
- Do not back up huge caches or generated files unless they are actually needed.
- Do not rely on a single copy. Keep at least one offsite copy and one easy local/provider snapshot for quick rollback.
Need a clean VPS to host your stack?
Deploy a no-KYC VPS, harden it, and set up backups before you put real data on it.
Deploy a VPSFAQ
Are VPS snapshots enough for backups?
Should I back up Docker containers or volumes?
Where should I store Restic backups?
How often should I test restores?
Further reading
- Restic official quickstart
- Restic repository backends
- Docker docs: back up, restore or migrate volumes
Pair this with our new VPS hardening checklist, the Nginx reverse proxy guide, and the pillar guide on anonymous VPS hosting.
GhostVPS is an anonymous, no-KYC VPS host on real DigitalOcean infrastructure. Pay with Bitcoin, Monero or USDT (TRC20); deploy in minutes from $9/mo. See pricing or open the panel.