Automating VPS backups: when cron and restic stop being enough
Learn when cron and restic are enough for VPS backups—and how to add safe scheduling, monitoring, retention, and restore testing before you outgrow local automation.

Start with the storage and recovery boundary
Use cron or a systemd timer for a simple, clearly owned VPS only when you can also answer four recovery questions: what is protected, where the encrypted repository lives, who can decrypt it, and how someone will restore it if the VPS is gone. The fastest safe path is to put the repository in storage outside the VPS failure domain, preserve recovery information away from that server, and prove an isolated restore before relying on a schedule.
Restic is an encrypted backup engine, not a complete operating process. Its repository is useful only when the storage account, repository location, credentials, and password or recovery key remain available during an incident. Keep the repository name or prefix, storage endpoint, access procedure, and recovery password in an access-controlled recovery record that does not depend solely on the VPS. If the restic password or recovery key is lost, encrypted repository data cannot be recovered from the repository alone. See the [recovery ownership plan for encryption keys](/blog/backup-encryption-key-loss-recovery-ownership-plan) before automating a job.
A scheduler needs storage credentials, but those secrets should not appear in a crontab, shell history, process arguments, emailed command output, or world-readable files. Use your host's restricted secret mechanism, restrict the backup account and configuration-file permissions, and test the job under the same account the scheduler uses. Storage permissions, quotas, provider availability, and access to the recovery key remain part of your recovery plan.
- Use storage independent of the VPS for the recovery copy; a backup stored only on the failed VPS is not an independent recovery path.
- Record the repository address and storage access process without recording plaintext secrets in tickets or source control.
- Give backup credentials only the repository-prefix access they need, including the operations required for retention if pruning will run.
Define success before you schedule it
A successful command exit is useful evidence, but it is not the definition of a recoverable service. Define the backup scope first: application uploads and state, configuration needed to recreate the service, database-consistent artifacts, and recovery records. For a practical scope review, use [what to back up on a Linux server](/blog/what-to-back-up-linux-server).
Treat databases differently from ordinary files. Copying live database files can produce an unusable result unless the database engine and method explicitly support it. Create an engine-aware logical dump or physical backup, confirm that artifact completed, then back up the artifact with the rest of the selected paths. For PostgreSQL, decide whether you need one database or cluster-wide globals as described in [pg_dump vs. pg_dumpall](/blog/pg-dump-vs-pg-dumpall).
Retention is a separate, destructive action. Run it only after a new backup has completed successfully, record its outcome separately, and keep enough history to recover from a problem discovered late. A retention policy selects snapshots to retain; pruning removes their unreferenced repository data. Review the operational consequences in [restic retention policy: forget, prune, and safe schedules](/blog/restic-retention-policy).
- Backup preparation succeeded: the database export or other consistency step produced the expected artifact.
- Repository write succeeded: a new snapshot exists for the intended host and paths.
- Maintenance succeeded or failed visibly: retention and pruning did not silently remove expected recovery points.
- Recovery evidence exists: an isolated restore and application check have been performed on a defined cadence.
Cron handles time, not accountability
Cron is reasonable when one stable host has a short, well-understood workflow and an owner who reviews it. It starts a command at a time; it does not tell another system that the host disappeared, the schedule was deleted, a credential expired, DNS failed, or a wrapper script never started. A cron job that sends output nowhere is especially risky because failure evidence may remain only on the machine that needs recovery.
A systemd timer is often easier to operate on modern Linux hosts because its paired service has explicit status and journal records. A timer configured with Persistent=true can run a missed activation after the timer becomes active again. That is helpful after ordinary downtime, but it does not replace an external freshness check, and it should not be mistaken for proof that a missed backup was completed while the host was unavailable.
For either scheduler, send a completion signal or publish a success timestamp only after the backup workflow has created the intended snapshot. Your monitor should alert both on a non-zero command result and on absence of a recent successful recovery point. The latter catches failures that never become a failed command. [How to monitor restic backups and alert on failed scheduled jobs](/blog/monitor-restic-backups-alert-scheduled-job-failures) explains how to cover both signals.
- Cron default: simple time-based execution; you must arrange logs, locking, notifications, and stale-run detection.
- Systemd timer default: time-based execution with service state and journal integration; you still need off-host monitoring and restore tests.
- Central control plane: useful when an operator needs one view of schedules, outcomes, snapshots, and failures across multiple hosts.
Build a wrapper that fails safely
Do not place a long restic command directly in crontab. Put the workflow in a root- or backup-user-owned script with restrictive permissions, explicit paths, predictable environment variables, structured logging, and a lock that spans database preparation, repository writes, and cleanup. The scheduler should call that one script and capture its exit status.
The following is an **illustrative starting pattern**, not a copy-and-run production script. Test it first on a non-production host and adapt paths, the database command, secret handling, retention, and repository configuration. It intentionally does not include storage credentials or a restic password; load those through a restricted mechanism appropriate to your distribution and environment.
bash
#!/usr/bin/env bash
# /usr/local/sbin/run-vps-backup
# Prerequisites: restic is installed; restricted environment file exists;
# /srv/app and /var/backups/postgres are intended backup sources.
set -Eeuo pipefail
umask 077
exec 9>/run/lock/vps-backup.lock
flock -n 9 || { echo "backup already running" >&2; exit 75; }
source /etc/restic/vps-backup.env # mode 0600; do not echo secrets
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
dump_dir="/var/backups/postgres/${stamp}"
mkdir -p "$dump_dir"
trap 'rm -rf "$dump_dir"' EXIT
# Replace with the database method and account appropriate to your service.
# This writes a PostgreSQL custom-format dump; it does not overwrite production.
/usr/bin/pg_dump --format=custom --file="$dump_dir/app.dump" appdb
/usr/bin/restic backup /srv/app /etc/myapp "$dump_dir" \
--tag vps --tag appdb --tag "host:$(hostname -s)"
/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
/usr/bin/restic snapshots --latest 1
Expected evidence is a zero exit status, a newly listed snapshot, and logs showing whether preparation, backup, or retention ran. Failure signals include backup already running, an incomplete database export, authentication errors, a missing repository, or a failed forget --prune. Do not publish a success heartbeat when any required stage fails. Also avoid deleting an old dump until its enclosing backup workflow has completed; the cleanup trap above removes only the newly created working directory.
- Use absolute executable paths because cron and service environments are intentionally minimal.
- Use
set -Eeuo pipefailso unset variables and failing pipeline stages do not look like success. - Set
umask 077before creating database artifacts so dumps are not readable by other local users. - Keep a distinct exit path for lock contention; a skipped run needs monitoring rather than being silently treated as success.
Prevent overlap and investigate interruption carefully
If a job can run longer than its interval, the next activation can overlap with preparation, backup transfer, or retention. Choose one policy deliberately: wait, skip and alert, or fail visibly. For most small VPS workflows, skipping the second run with a clear alert is safer than two independent scripts touching the same dump directory and repository maintenance at once. The lock must cover the complete workflow, not only restic backup.
After a reboot, terminated process, network outage, or storage error, inspect the scheduler logs and repository snapshots before starting cleanup. A new snapshot may be absent even though older snapshots remain healthy. Do not remove a repository lock just because it looks old: first confirm no restic process is still active and consult the [restic repository documentation](https://restic.readthedocs.io/en/stable/045_working_with_repos.html) for the appropriate recovery procedure.
A repository integrity check adds evidence that stored data can be read and its structure is consistent, but it cannot prove your backup scope was complete or that the application will start. Run checks according to your risk and storage-cost tolerance, then pair them with a restore rehearsal. [Restic repository health: what integrity checks prove—and what they do not](/blog/restic-repository-health-integrity-checks) explains that boundary.
- Before maintenance, verify which snapshot is newest and whether it contains the expected host and tags.
- Before unlocking anything, verify that no active restic or wrapper process still owns the operation.
- After an interrupted run, preserve the incident log and alert on freshness rather than assuming the next schedule will correct the gap.
Monitor freshness and prove the restore path
Monitor two separate conditions from outside the VPS: an immediate failure when the wrapper exits unsuccessfully, and a freshness alert when no successful recovery point arrives by its deadline. Include the host, backup set, last known successful snapshot time, affected stage, and the response owner. A backup monitor that only receives command failures will miss a powered-off host, disabled timer, or job that never began.
A restore test should use an isolated directory, disposable virtual machine, container, or staging environment—not the running production path. Select a known snapshot, restore to an empty target, validate file ownership and configuration, import the database artifact into a non-production database, and run an application-level smoke test. Record the snapshot used, elapsed time, failures, and fixes. That record is the evidence behind your recovery time objective (RTO), while backup frequency addresses the recovery point objective (RPO). [RPO vs. RTO for small teams](/blog/rpo-vs-rto-small-teams-backup-frequency) covers the distinction.
For a safe file-level restore rehearsal, use a new directory and inspect the copy before moving anything back. The commands below assume the repository environment has already been loaded through your restricted process. They do not overwrite live application files.
bash
# Illustrative restore test: adapt SNAPSHOT and the source path first.
# Run as an account permitted to read the repository, in a disposable location.
export RESTORE_DIR="/var/tmp/restore-test-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$RESTORE_DIR"
restic snapshots
restic restore SNAPSHOT_ID --target "$RESTORE_DIR" --include /srv/app
find "$RESTORE_DIR/srv/app" -maxdepth 2 -type f | head
# Inspect expected files and application configuration, then remove only this test target:
rm -rf "$RESTORE_DIR"
- Repository-level evidence: a snapshot exists and repository checks can read expected data.
- Artifact-level evidence: a database dump can be listed or restored by the relevant database tooling.
- Service-level evidence: the restored application starts, connects to its dependencies, and passes a defined smoke test.
- Operational evidence: responders can find the storage location and recovery credentials without access to the failed VPS.
Technical basis
First-party references
Technical claims and limitations in this guide were checked against these primary sources. Confirm version-specific behavior when designing a production recovery process.
Related from Rested
Common questions
Frequently asked questions
How do I automate restic backups with cron?
Put the full workflow in a restricted wrapper script, then have cron run that script. The wrapper should create application-consistent database artifacts where needed, prevent overlap, run restic, run retention only after success, log every stage, and report completion to an external monitor. Test the script under the same account and environment cron will use.
Should I use cron or a systemd timer for VPS backups?
Either can start a backup. Cron is simple for one stable, well-owned host. A systemd timer provides paired service status and journal integration, and Persistent=true can make up a missed activation after the timer becomes active again. Neither scheduler detects a dead host or proves recovery; use off-host freshness monitoring and restore tests either way.
How do I keep restic credentials out of cron logs?
Do not put secrets in the crontab, command arguments, or echoed environment output. Load them from a root- or backup-user-readable secret mechanism or restricted environment file, keep that file out of source control, and retain a separate controlled recovery copy. The recovery password or key must remain available to authorized responders outside the VPS.
What should happen when a scheduled backup is still running?
Choose an explicit policy—usually skip the second run and alert—and enforce it with a lock that covers database preparation, backup transfer, retention, and cleanup. Do not treat a skipped or locked run as a successful backup, because the newest usable recovery point may now be older than intended.
Does a successful restic backup prove that my VPS can be restored?
No. It proves the command completed its assigned work. You still need to confirm that the selected scope includes application state and configuration, database artifacts are consistent, storage and recovery credentials are available, and an isolated restore can start and pass an application-level check.