---
title: "Hostinger VPS: Backups with restic"
slug: "hostinger-vps-backups"
category: "ops"
tags: ["ops", "hostinger", "vps", "backup", "restic", "cron", "linux", "disaster-recovery"]
status: "stable"
last_updated: 2026-05-14
summary: "Set up restic for encrypted off-box backups on a Hostinger VPS: repo init, scheduled cron, retention policy, and restore drills."
related: ["[[ops/hostinger-vps]]", "[[ops/hostinger-vps-hardening]]", "[[ops/hostinger-vps-deploy-systemd]]", "[[backend/postgres]]", "[[ops/cloudflare-r2]]"]
---

## Overview

Local snapshots are not backups. A disk failure, ransomware event, or accidental `rm -rf` on the VPS destroys both the data and the snapshot. Backups must be off-box and tested with a restore drill. restic is the right tool: encrypted, deduplicated, and able to push to S3, Backblaze B2, SFTP, or any rclone-supported backend. This page covers setup, scheduling, retention, and the restore drill you must run at least once. See [[ops/hostinger-vps]] for the full VPS context and [[backend/postgres]] for database dump commands.

## Install restic and initialize a repository

```bash
apt install -y restic
```

Pick a backend. Backblaze B2 is inexpensive; AWS S3 or [[ops/cloudflare-r2]] also work. For B2:

```bash
export B2_ACCOUNT_ID=your_account_id
export B2_ACCOUNT_KEY=your_application_key
export RESTIC_REPOSITORY=b2:your-bucket-name:/vps-backups
export RESTIC_PASSWORD=your_strong_passphrase

restic init
```

Store the passphrase in a password manager and in a second location that is not on the VPS. Without it, the backup is permanently unreadable. The repository password encrypts every snapshot; restic itself cannot decrypt without it.

## Define what to back up

Back up the app directory, database dumps, and any config files that are not in git.

```bash
# Dump Postgres before backup
pg_dump -U postgres mydb | gzip > /tmp/mydb_$(date +%Y%m%d).sql.gz

# Run restic backup
restic backup \
  /srv/app \
  /etc/app \
  /etc/caddy \
  /tmp/mydb_$(date +%Y%m%d).sql.gz \
  --exclude /srv/app/node_modules \
  --exclude /srv/app/.git
```

Exclude large directories that can be reconstructed (node_modules, .git, build output). Including them wastes storage and slows backup without improving restorability.

## Write the backup script and cron entry

Create `/usr/local/bin/backup.sh`:

```bash
#!/bin/bash
set -euo pipefail

export B2_ACCOUNT_ID=your_account_id
export B2_ACCOUNT_KEY=your_application_key
export RESTIC_REPOSITORY=b2:your-bucket-name:/vps-backups
export RESTIC_PASSWORD=your_strong_passphrase

# Database dump
DUMP_FILE="/tmp/mydb_$(date +%Y%m%d_%H%M).sql.gz"
pg_dump -U postgres mydb | gzip > "$DUMP_FILE"

# Backup
restic backup \
  /srv/app \
  /etc/app \
  /etc/caddy \
  "$DUMP_FILE" \
  --exclude /srv/app/node_modules \
  --exclude /srv/app/.git

# Apply retention policy
restic forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 3 \
  --prune

# Clean up dump
rm -f "$DUMP_FILE"
```

Make it executable:

```bash
chmod 750 /usr/local/bin/backup.sh
chown root:root /usr/local/bin/backup.sh
```

Schedule with cron. Run as root so it can read all target directories:

```bash
crontab -e -u root
# Add:
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
```

Daily at 03:00. Adjust to a low-traffic window for your timezone.

## Set a retention policy that matches recovery objectives

The `restic forget --prune` command applies the policy and removes data not referenced by kept snapshots.

```
--keep-daily 7       # One snapshot per day for the last 7 days
--keep-weekly 4      # One per week for the last 4 weeks
--keep-monthly 3     # One per month for the last 3 months
```

This gives a 3-month window with fine granularity near the present. For regulated or high-value data, extend `--keep-monthly` and add `--keep-yearly`.

Run `--prune` in the same `forget` call or separately with `restic prune`. Without it, old snapshots are forgotten (unlisted) but the underlying data is not deleted.

## Run a restore drill before you need it

A backup you have not restored is an assumption. Schedule a restore drill quarterly at minimum.

```bash
# List snapshots
restic snapshots

# Restore the latest snapshot to a staging directory
restic restore latest --target /tmp/restore-test

# Verify key files exist and are intact
ls /tmp/restore-test/srv/app
ls /tmp/restore-test/tmp/*.sql.gz

# Restore a specific snapshot
restic restore abc123de --target /tmp/restore-test
```

For the database, load the dump into a test Postgres instance:

```bash
gunzip < /tmp/restore-test/tmp/mydb_*.sql.gz | psql -U postgres mydb_test
```

Confirm row counts and spot-check application-critical data. Document the result and the date in a runbook.

## Monitor backup health

Check that cron ran and that the last snapshot is recent:

```bash
restic snapshots --last --json | python3 -c "
import json, sys, datetime
snaps = json.load(sys.stdin)
if not snaps:
    print('NO SNAPSHOTS'); sys.exit(1)
ts = datetime.datetime.fromisoformat(snaps[-1]['time'].replace('Z', '+00:00'))
age = (datetime.datetime.now(datetime.timezone.utc) - ts).total_seconds() / 3600
print(f'Last snapshot: {ts} ({age:.1f}h ago)')
if age > 26:
    print('WARNING: backup is stale'); sys.exit(1)
"
```

Wire this check into your monitoring system. An alert on a stale backup catches cron failures, credential rotation issues, and network disruptions before a disaster exposes them.

## Related

- [[ops/hostinger-vps]]
- [[ops/hostinger-vps-hardening]]
- [[ops/hostinger-vps-deploy-systemd]]
- [[ops/hostinger-vps-reverse-proxy-caddy]]
- [[backend/postgres]]
- [[ops/cloudflare-r2]]
