
Your first full backup feels reassuring. Your first restore makes it real. This incremental backup tutorial shows how to set up encrypted, deduplicated backups with Restic to S3-compatible object storage on a VPS or dedicated server.
You’ll also run restore drills. Those drills surface problems before an outage forces you to improvise.
The objective is simple: back up the data that matters. Run it on a schedule you can defend. Apply retention rules you can explain to a client.
Then prove you can restore a single file, a full site tree, and a “server subset” cleanly.
What you’ll build (and what you won’t)
- Backups: Restic repositories stored in S3-compatible object storage (AWS S3, Wasabi, Backblaze B2 S3 API, MinIO, etc.).
- Security: Client-side encryption (Restic encrypts before upload) and least-privilege API keys.
- Automation: systemd timers (preferred) or cron on Linux.
- Retention: Keep daily/weekly/monthly snapshots with prune rules.
- Restore drills: File-level restore and full-path restore to a staging directory.
Not included: VM snapshot workflows. That’s a different toolchain, with different failure modes.
This guide backs up files and directories. That matches how most hosting admins recover websites and configs.
Prerequisites and a safe target list
This tutorial assumes Ubuntu 24.04 LTS or Debian 12 on a VPS/dedicated server, with root or sudo access.
The same approach works on AlmaLinux/Rocky. The package and service commands will differ.
For WordPress, mail, or multi-site hosting, prioritize the data that’s painful to rebuild:
/etc(system and service config)- Website roots (often
/var/www, or/home/*/public_htmlon control panel setups) - Application config and secrets (env files, deploy keys—be intentional)
- Logs are optional; they bloat repositories quickly
Database dumps belong in a complete backup plan. This article stays focused on file-level incremental backups for hosting operations.
If your sites rely on MySQL/MariaDB, pair this with a separate dump routine. Use your panel or your existing automation.
Need a stable machine to run this on? Start with a HostMyCode VPS for predictable storage and network.
Move to dedicated servers when you need consistent I/O under load.
Incremental backup tutorial: Install Restic and create your repository
On Ubuntu/Debian, install Restic from the distro packages. This keeps security updates flowing through normal patching.
Upstream binaries work too. For most hosting environments, packages are the sensible default.
sudo apt update
sudo apt install -y restic ca-certificates jq
Create a dedicated place for environment files and scripts:
sudo install -d -m 0750 /etc/restic
sudo install -d -m 0750 /opt/restic
Configure S3 credentials (least privilege)
Create an S3 bucket and an access key scoped to that bucket only.
Use a dedicated key per server. If one key leaks, it should not unlock every environment you run.
Create /etc/restic/restic.env:
sudo nano /etc/restic/restic.env
RESTIC_REPOSITORY=s3:https://s3.example.com/hostmycode-prod-vps-01
AWS_ACCESS_KEY_ID=REPLACE_ME
AWS_SECRET_ACCESS_KEY=REPLACE_ME
RESTIC_PASSWORD=REPLACE_WITH_A_LONG_RANDOM_PASSPHRASE
Lock the file down:
sudo chmod 0640 /etc/restic/restic.env
sudo chown root:root /etc/restic/restic.env
Initialize the repository:
set -a
source /etc/restic/restic.env
set +a
sudo -E restic init
If you hit TLS or endpoint errors, double-check the S3 endpoint URL.
Some providers require a custom region or path-style addressing. If so, you may need:
AWS_DEFAULT_REGION=us-east-1
AWS_S3_FORCE_PATH_STYLE=true
Leave those out unless your provider explicitly needs them.
Choose backup paths and exclusions that won’t bite you later
Most backup failures come down to scope.
Go too broad and you upload churn that never helps a restore. Go too narrow and you discover gaps during an incident.
Suggested base set for a hosting VPS
/etc/var/www(or your site roots)/home(only if you actually store web/app data there)/root(optional; include only if it contains configs you want)
Common exclusions
Create an exclude file at /etc/restic/excludes.txt:
sudo nano /etc/restic/excludes.txt
# OS/runtime noise
/proc
/sys
/dev
/run
/tmp
/var/tmp
# Package caches
/var/cache
# Logs (optional; comment out if you want them)
/var/log
# Containers (if you run Docker; consider backing up volumes separately)
/var/lib/docker
On control panel servers, treat panel-managed paths with care.
For cPanel/WHM, most account data lives under /home. You’ll usually want to exclude transient directories like /home/*/tmp.
Keep exclusions specific. Broad patterns can hide real content.
If you need secure admin access before you automate backups, follow our SSH hardening tutorial.
Don’t build automation on top of an exposed root login.
Create a backup script with locking and readable output
You can run Restic as a one-liner. A small script makes scheduling, logging, and troubleshooting easier.
Create /opt/restic/backup.sh:
sudo nano /opt/restic/backup.sh
#!/usr/bin/env bash
set -euo pipefail
# Load environment
set -a
source /etc/restic/restic.env
set +a
HOST_TAG="$(hostname -f 2>/dev/null || hostname)"
LOCKFILE="/run/restic-backup.lock"
exec 9>"$LOCKFILE"
if ! flock -n 9; then
echo "[$(date -Is)] Restic backup already running. Exiting."
exit 0
fi
echo "[$(date -Is)] Starting restic backup for $HOST_TAG"
# Tip: add --one-file-system if you don't want mounted volumes included
restic backup \
--tag "$HOST_TAG" \
--exclude-file /etc/restic/excludes.txt \
/etc /var/www /home \
--verbose
echo "[$(date -Is)] Applying retention policy"
restic forget \
--tag "$HOST_TAG" \
--keep-daily 14 \
--keep-weekly 8 \
--keep-monthly 12 \
--prune
echo "[$(date -Is)] Verifying repository structure (quick check)"
restic check --read-data-subset=2.5%
echo "[$(date -Is)] Done"
Make it executable:
sudo chmod 0750 /opt/restic/backup.sh
Why restic check --read-data-subset? It’s a practical compromise.
A full read check can get expensive on large repos. A small subset check still catches obvious corruption, credential issues, and broken endpoints early.
Automate with systemd timers (cleaner than cron)
systemd timers keep logs in the journal. They also handle missed runs after reboots.
They’re often easier to manage than a pile of cron entries.
Create the service
/etc/systemd/system/restic-backup.service:
sudo nano /etc/systemd/system/restic-backup.service
[Unit]
Description=Restic backup job
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/restic/backup.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
# Hardening
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/run
[Install]
WantedBy=multi-user.target
Note: ProtectHome=read-only still allows reading /home. That’s usually what you want for backups.
If your backup paths live elsewhere, adjust the sandboxing to match.
Create the timer
/etc/systemd/system/restic-backup.timer:
sudo nano /etc/systemd/system/restic-backup.timer
[Unit]
Description=Nightly Restic backup
[Timer]
OnCalendar=*-*-* 02:15: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
Run a first backup manually. Don’t wait for 2 AM:
sudo systemctl start restic-backup.service
sudo journalctl -u restic-backup.service -n 200 --no-pager
Restore drills: practice the two restores you’ll actually need
You don’t need a disaster to validate restores.
You need a staging directory and a few minutes of discipline.
1) Restore a single file (fast sanity check)
List snapshots:
set -a
source /etc/restic/restic.env
set +a
sudo -E restic snapshots
Pick the latest snapshot ID. Restore one file into /root/restore-test:
sudo mkdir -p /root/restore-test
sudo -E restic restore latest \
--target /root/restore-test \
--include /etc/ssh/sshd_config
Verify it exists:
sudo ls -l /root/restore-test/etc/ssh/sshd_config
2) Restore a site tree to a new path (what you’ll do under pressure)
Restoring in-place is risky. Small mistakes can turn into long incidents.
Restore to a staging path first. Inspect what you got, then decide how you’ll swap it into production.
sudo mkdir -p /srv/restore-staging
sudo -E restic restore latest \
--target /srv/restore-staging \
--include /var/www/example.com
Spot-check ownership and sizes:
sudo du -sh /srv/restore-staging/var/www/example.com
sudo find /srv/restore-staging/var/www/example.com -maxdepth 2 -type f | head
If your site is WordPress and you want to reduce file transfer risk during day-to-day admin work, pair this with the locked-down approach in our SFTP setup tutorial.
Retention policy that matches hosting reality
The retention in the script is a solid baseline for small-to-mid hosting nodes:
- 14 daily: covers “we changed something last week”
- 8 weekly: covers slow-burn issues
- 12 monthly: covers “we need last quarter” requests
Tune it based on storage cost, compliance needs, and what your clients expect.
For ecommerce, you’ll often keep files longer. You’ll also tighten the database strategy at the same time.
Hardening checklist: keep backups from becoming your weakest link
- Use a dedicated S3 key per server. Don’t reuse keys across environments.
- Store the Restic password securely. If you use a password manager, document access for your team.
- Restrict outbound access if possible (only to your S3 endpoint). If you’re unsure, start with a safe baseline; see our UFW firewall setup tutorial.
- Log backup results and alert on failures. A “silent” backup system is not a system.
Quick diagnostic: confirm you’re really deduplicating
After a few runs, look at repo stats:
set -a
source /etc/restic/restic.env
set +a
sudo -E restic stats --mode raw-data
If the repo grows almost as fast as your total data every day, you’re probably capturing volatile paths.
Common causes include logs, caches, tmp directories, or frequently rewritten large assets.
Operational monitoring: make failures visible
At minimum, alert if the timer fails. Also alert if nothing has run in the last 24 hours.
systemd already gives you the raw signals: exit status and journal output.
Two quick checks to run during routine maintenance:
sudo systemctl status restic-backup.timer --no-pager
sudo journalctl -u restic-backup.service --since "2 days ago" --no-pager
If you already run monitoring, track backup success alongside CPU, disk, and HTTP uptime.
Our server monitoring tutorial shows a clean way to wire alerts without turning your VPS into a long-running experiment.
Common mistakes (and how to fix them fast)
- Backups run, but restores fail due to missing password: your
RESTIC_PASSWORDisn’t available to the service. Keep it in/etc/restic/restic.envand source it in the script. - “403 AccessDenied” from S3: your key lacks
ListBucketor object permissions. Fix IAM policy or provider permissions, then rerunrestic snapshots. - Huge daily uploads: tighten exclusions. Start by excluding
/var/log, caches, and tmp directories. - Backup duration too long: reduce scope, schedule off-peak, and consider splitting large paths into separate repos (web vs. home) for parallelism later.
Where this fits in a HostMyCode hosting workflow
Restic-style incremental backups are a good fit when you need predictable restores and portability:
- VPS hosting where you control the OS and want portable backups across providers.
- Dedicated servers with multiple sites, where you want consistent retention and restore behavior.
- Reseller environments where client requests often mean restoring specific directories, not entire nodes.
If you’re planning a server move, get backups running first. Verify a restore before you migrate.
For the migration flow itself, our hosting migration checklist pairs nicely with this setup.
Summary: your minimal “production-ready” standard
- Encrypted Restic repo in S3-compatible storage
- Nightly automated runs via systemd timer
- Retention rules + prune
- Regular restore drills to a staging directory
- Monitoring that tells you when backups stop
If you want a stable place to run this long-term, a managed VPS hosting plan from HostMyCode can handle patching and baseline hardening while you keep full control of your backup design.
If you prefer to run everything yourself, a standard HostMyCode VPS gives you the access you need to automate Restic cleanly.
If backups are part of your uptime responsibility, run them on hosting that stays out of the way. Use a HostMyCode VPS for full root access and predictable performance, or choose managed VPS hosting if you want OS maintenance handled while you focus on sites, restores, and retention.
FAQ
Is Restic “incremental” or “full”?
Each run creates a new snapshot, but the data is deduplicated.
In practice, most daily backups upload only changed blocks. That’s what admins usually mean by incremental.
Should I back up the entire root filesystem?
Usually no.
Back up the data and config you can’t recreate quickly: /etc, site roots, and app config. Exclude pseudo-filesystems and caches.
How often should I run restic check?
A small subset check nightly (as shown) is a good baseline.
Run a full restic check monthly or after any storage incident.
Can I use this on a cPanel server?
Yes, but be careful with scope.
Focus on /home (accounts) and essential configs. Avoid backing up panel caches and transient directories.
What’s the one restore test I should always do?
Restore a real site directory to a staging path. Verify file ownership and content.
Then document the steps. That’s the restore you’ll repeat during incidents.