
Your first “real” backup isn’t the one that runs every night. It’s the one you can restore under pressure. This VPS backup automation tutorial builds a practical setup on Ubuntu: encrypted Restic backups, sane retention, and a restore test you can schedule and trust.
Aim for boring reliability. Capture site files, configs, and the server state you don’t want to recreate. Push it offsite. Then prove you can pull it back with a repeatable drill.
No complicated orchestration. No mystery steps.
What you’ll build (and what you won’t)
Set expectations before you start typing commands. This guide targets a common VPS hosting stack: Nginx/Apache, PHP-FPM, WordPress (or similar), plus system config that’s painful to rebuild.
- Backups: Encrypted, incremental, offsite Restic repository (S3-compatible object storage).
- Schedule: systemd timers (cleaner than cron for logging and status checks).
- Retention: Keep a sensible window without eating storage.
- Restore test: Automated extraction into a staging path and validation checks.
This is not a database-dump tutorial. Database backup methods vary by stack, and you may already run app-level dumps.
If you want full disaster recovery (DNS failover, multi-region, runbooks), use this as a baseline. Add a DR procedure later.
Prerequisites and the “don’t lock yourself out” checklist
Run everything as a user with sudo. Keep an active SSH session open while you set this up.
You’ll also want enough local disk for changed data during each run.
- Ubuntu 22.04 LTS or Ubuntu 24.04 LTS (both common in 2026 hosting).
- Root/sudo access
- An S3-compatible bucket (or any Restic-supported backend) and access keys
- A place to store secrets safely (we’ll use a root-owned environment file)
If your server still needs basic hardening, do that first. Backup credentials are valuable. Weak baseline security makes them easier to steal.
This companion guide is a solid checklist: Server hardening tutorial for Ubuntu VPS hosting.
If you need a server sized for reliable backups and restore tests, prioritize storage and I/O. NVMe and predictable performance matter.
Start with a HostMyCode VPS so you can control storage, schedules, and retention without shared-hosting limits.
Install Restic and create a dedicated backup user
Ubuntu packages can lag behind upstream. In 2026, the safest approach is either a current distro package or the official Restic release binary.
This tutorial installs from Ubuntu first. If your Restic version is dated, switch to the official binary.
sudo apt update
sudo apt install -y restic jq
Create a minimal local user to keep boundaries clean.
sudo adduser --system --group --home /var/lib/restic --shell /usr/sbin/nologin restic
Next, decide what the backup process should be able to read. On hosting VPSes, that usually includes /etc, /var/www, and selected service data under /var/lib.
Don’t open permissions broadly “just to make it work.” Start tight. Then add only what you can justify.
Choose what to back up on a hosting VPS
You want a backup set that restores fast. You also want enough coverage to rebuild services without surprises. This is a reasonable default for many hosting servers:
/etc(Nginx/Apache vhosts, PHP-FPM pools, SSL config, systemd units)/var/wwwor your document roots (WordPress files, uploads, custom apps)/home(if you host users or deploy via home directories)/var/spool/cronor relevant cron config (task schedules)/usr/local/bin(small custom scripts)
Common exclusions (because they’re huge, noisy, or easy to regenerate):
/var/cache,/tmp,/var/tmp/var/log(unless you explicitly need logs for compliance)- Large vendor directories that can be reinstalled (container layers, build caches)
If you run WordPress and want less cache churn to back up, caching helps. It can also cut CPU load and make the admin faster.
See: WordPress Redis object cache setup tutorial.
Configure an S3-compatible backend for Restic
Restic works cleanly with S3-compatible storage. You’ll set the repository location and credentials through environment variables.
Create a root-owned env file and lock permissions down:
sudo install -m 0600 -o root -g root /dev/null /etc/restic.env
Edit /etc/restic.env:
sudo nano /etc/restic.env
Example (replace values):
# Restic repository (S3)
export RESTIC_REPOSITORY="s3:https://s3.example.com/hostmycode-vps-backups/myvps01"
# S3 credentials
export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_KEY"
# Restic encryption password (store safely)
export RESTIC_PASSWORD="use-a-long-unique-passphrase"
# Optional: if your S3 provider requires a specific region
# export AWS_DEFAULT_REGION="us-east-1"
Security pitfall: don’t paste secrets into your shell during debugging. Keep them in the env file. Then source it as root before you run Restic.
Initialize the repository and validate access
Source the environment file, then initialize the repository once.
sudo -i
source /etc/restic.env
restic snapshots || true
restic init
Confirm Restic can read and write:
restic check
If you hit S3 errors, start with DNS resolution and outbound firewall rules. A strict UFW setup can block backups. Those failures can look like random network issues.
Keep this guide handy for diagnosis: UFW firewall troubleshooting tutorial.
Create a clean backup script with exclusions
Put the logic in a script. That keeps your systemd units simple. It also keeps behavior consistent across manual runs and timers.
Create /usr/local/sbin/restic-backup.sh:
sudo install -m 0750 -o root -g root /dev/null /usr/local/sbin/restic-backup.sh
sudo nano /usr/local/sbin/restic-backup.sh
Script example:
#!/usr/bin/env bash
set -euo pipefail
# Load credentials + repository settings
source /etc/restic.env
HOST_TAG="$(hostname -f 2>/dev/null || hostname)"
# Exclusions for typical hosting servers
EXCLUDES=(
"--exclude=/var/cache"
"--exclude=/tmp"
"--exclude=/var/tmp"
"--exclude=/var/log"
"--exclude=/var/lib/systemd/coredump"
)
# Paths to back up (adjust for your stack)
INCLUDES=(
"/etc"
"/var/www"
"/home"
"/usr/local/bin"
"/var/spool/cron"
)
# Run backup
restic backup \
--host "$HOST_TAG" \
--one-file-system \
"${EXCLUDES[@]}" \
"${INCLUDES[@]}"
# Retention policy: tune to your risk + budget
restic forget \
--host "$HOST_TAG" \
--keep-daily 14 \
--keep-weekly 8 \
--keep-monthly 12 \
--prune
# Quick repo health check (lightweight). Full check can be weekly.
restic check --read-data-subset=1/50
Run it once by hand:
sudo /usr/local/sbin/restic-backup.sh
If you hit “permission denied,” fix the underlying permissions instead of loosening everything. On many servers, root can read /var/www without special changes.
If your app writes data elsewhere, add only those specific paths.
Automate nightly runs with systemd service + timer
systemd timers give you status, logging, and easy enable/disable control. Start with a dedicated service unit:
sudo nano /etc/systemd/system/restic-backup.service
[Unit]
Description=Nightly Restic Backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-backup.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
# Hardening
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/lib/restic
NoNewPrivileges=true
Create the timer:
sudo nano /etc/systemd/system/restic-backup.timer
[Unit]
Description=Run Restic Backup Nightly
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
sudo systemctl list-timers --all | grep restic
After the first run, read the logs:
sudo journalctl -u restic-backup.service -n 200 --no-pager
Add a weekly deep integrity check (optional but smart)
The nightly subset check is a good smoke test. A heavier weekly restic check helps catch backend issues and corruption earlier.
It also avoids burning bandwidth every day.
Create /usr/local/sbin/restic-check-weekly.sh:
sudo install -m 0750 -o root -g root /dev/null /usr/local/sbin/restic-check-weekly.sh
sudo nano /usr/local/sbin/restic-check-weekly.sh
#!/usr/bin/env bash
set -euo pipefail
source /etc/restic.env
# Heavier check than the nightly subset
restic check --read-data-subset=5/50
Systemd units:
sudo nano /etc/systemd/system/restic-check-weekly.service
[Unit]
Description=Weekly Restic Deep Check
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-check-weekly.sh
sudo nano /etc/systemd/system/restic-check-weekly.timer
[Unit]
Description=Run Restic Deep Check Weekly
[Timer]
OnCalendar=Sun *-*-* 04:10:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now restic-check-weekly.timer
Build a restore test you can run without thinking
Untested backups are wishful thinking. Your restore test should be safe (no overwrites). It should also be fast enough that you won’t skip it for months.
Create a restore target directory:
sudo mkdir -p /var/restore-tests/restic
sudo chmod 0700 /var/restore-tests/restic
Now restore a small but meaningful sample. Good candidates include your Nginx vhost config, a WordPress wp-config.php, and a representative media file.
Adjust includes/excludes to match your server.
Create /usr/local/sbin/restic-restore-test.sh:
sudo install -m 0750 -o root -g root /dev/null /usr/local/sbin/restic-restore-test.sh
sudo nano /usr/local/sbin/restic-restore-test.sh
#!/usr/bin/env bash
set -euo pipefail
source /etc/restic.env
DEST="/var/restore-tests/restic/$(date +%F)"
mkdir -p "$DEST"
chmod 0700 "$DEST"
# Grab latest snapshot ID
SNAP_ID=$(restic snapshots --json | jq -r '.[-1].short_id')
# Restore only a few paths for a fast, safe test
restic restore "$SNAP_ID" --target "$DEST" \
--include "/etc/nginx" \
--include "/etc/apache2" \
--include "/var/www" \
--exclude "/var/www/*/wp-content/cache" \
--exclude "/var/www/*/wp-content/uploads/*"
# Basic validation checks
if [ -d "$DEST/etc/nginx" ]; then
nginx -t -c "$DEST/etc/nginx/nginx.conf" || true
fi
# Show what was restored
find "$DEST" -maxdepth 3 -type f | head -n 40
echo "Restore test complete: $DEST"
Run the test:
sudo /usr/local/sbin/restic-restore-test.sh
If you use Apache, a config test against restored paths is awkward without additional flags. Don’t get stuck there.
The real value is confirming three things: a snapshot exists, you can decrypt it, and the restored tree looks sane.
Automate the restore test monthly
A monthly restore test catches slow-burn failures. That includes rotated credentials, deleted buckets, permission regressions, and lost passphrases.
sudo nano /etc/systemd/system/restic-restore-test.service
[Unit]
Description=Monthly Restic Restore Test
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-restore-test.sh
sudo nano /etc/systemd/system/restic-restore-test.timer
[Unit]
Description=Run Restic Restore Test Monthly
[Timer]
OnCalendar=monthly
RandomizedDelaySec=2h
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now restic-restore-test.timer
Operational checklist: what to verify after you set this up
- Snapshots exist:
sudo -i→source /etc/restic.env→restic snapshots - Retention works: storage usage stays stable after a few weeks
- Restore test runs: you see new folders under
/var/restore-tests/restic/ - Timer health:
systemctl status restic-backup.timer - Logs are readable:
journalctl -u restic-backup.serviceshows errors clearly
Troubleshooting common backup failures on hosting VPS
Most failures repeat the same patterns. Fix the cause once. Then the automation goes quiet again.
1) “403 Forbidden” or “AccessDenied” to S3
- Confirm the repository URL is correct (especially the bucket path).
- Verify the access key has permission for list/get/put/delete on that bucket/prefix.
- Check server time. Large clock drift can break signed requests.
timedatectl status
journalctl -u systemd-timesyncd -n 100 --no-pager
2) “connection reset” or intermittent network timeouts
- Check outbound firewall rules and provider ACLs.
- Reduce concurrency if your provider rate-limits. Restic supports flags like
--option s3.connections=10(tune conservatively).
3) Backup runs, but it’s slow
- Exclude frequently changing caches (WordPress cache directories, app build artifacts).
- Run backups during low-traffic hours.
- On busy servers, consider upgrading disk I/O and CPU so compression/encryption doesn’t contend with PHP workers.
If you’re tuning a WordPress stack at the same time, this guide pairs well with backups: VPS performance optimization tutorial.
How backups fit into migrations (and why they reduce downtime)
Automated backups help in disasters. They also make migrations calmer.
If a change goes sideways, you can roll back quickly. For cleaner cutovers, lower your DNS TTL early. Keep it low until the new server is stable.
Use this guide for a safe TTL change window: DNS TTL reduction tutorial.
If you’re moving sites between VPS instances, pair your backup plan with a documented migration process: Server migration tutorial with rsync and DNS cutover.
Summary: a backup plan you can operate in 2026
This setup covers what most VPS backup routines miss: encryption by default, predictable automation, and a restore test you can repeat without risk.
Once it’s running, the question changes. Instead of “did it back up?” you ask: “is retention still right, and do restores still work?”
If you want full control over schedules, storage, and performance, run this on a HostMyCode VPS. If you don’t want to manage timers, retention, and verification yourself, consider managed VPS hosting from HostMyCode so you can focus on your sites instead of the backup plumbing.
If restore speed matters, pick a VPS that can handle nightly encryption and sustained I/O without stalling your apps. HostMyCode offers VPS plans for hands-on admins, and managed VPS hosting if you want help with backups, updates, and ongoing maintenance.
FAQ
Should I back up /var/log on a hosting VPS?
Usually not. Logs grow quickly and rarely help you rebuild services. If you need logs for compliance or investigations, store them separately (or ship them to a log host) instead of inflating your primary backup set.
How do I confirm my backups are encrypted?
With Restic, encryption is always enabled. The practical check is simple: restoring requires the repository password. Try restic snapshots without RESTIC_PASSWORD set and you should get an error.
What’s a reasonable retention policy for small business hosting?
A common baseline is 14 daily, 8 weekly, and 12 monthly snapshots. If you publish often (WooCommerce, busy blogs), keep more dailies. If storage costs climb, cut monthlies first.
Can I run this on a shared hosting plan?
Not reliably. Shared hosting often restricts background processes, systemd timers, and access to system paths. If you want backup automation you control end-to-end, use a VPS.
What’s the fastest way to catch a broken backup before it matters?
Schedule a monthly restore test and read the logs. It’s the simplest proof that credentials work, encryption is usable, the repo is reachable, and restores actually succeed.