What this backup setup protects
A production backup plan should follow the 3-2-1 principle: keep at least three copies of data, store them on two media types, and keep one copy off-site. For a dedicated server, the live server is one copy, local staging or snapshots can be a second copy, and encrypted object storage in another location is the off-site copy.
This guide configures Restic because it encrypts data before upload, deduplicates unchanged files, supports retention policies, and can restore individual files or a complete directory tree. The setup is suitable for web hosting, self-hosted apps, game servers, databases, CI/CD runners, monitoring systems, VPN gateways, mail servers, and media services.
Restic file backups are not automatically application-consistent. Files such as PostgreSQL or MySQL database files must be backed up with native database dump tools or coordinated snapshots. This tutorial creates a PostgreSQL dump before Restic runs.
Prerequisites and minimum server requirements
- A dedicated server running Ubuntu 22.04 or Ubuntu 24.04 with root or sudo access.
- Minimum production source server: 4 vCPU, 8 GB RAM, and 240 GB NVMe storage.
- An S3-compatible object storage bucket in a separate failure domain from the server.
- An access key limited to the backup bucket; do not use account-wide storage credentials.
- At least 30 days of expected backup retention capacity in the remote bucket.
- Outbound HTTPS access from the dedicated server to the object storage endpoint.
Use a VPS for a small single-service website or a lightweight monitoring stack. Use a dedicated server for multi-terabyte datasets, busy databases, game-server fleets, mail storage, sustained backup uploads, or workloads requiring predictable disk I/O and CPU isolation.
Size the server and backup repository
Plan remote object storage for the initial full backup, daily changes, retained snapshots, and database dumps. Deduplication can reduce storage use significantly, but databases and compressed media often deduplicate poorly. Measure current data with sudo du -sh /etc /home /srv /var/www 2>/dev/null before choosing capacity.
Use these production starting points; increase remote storage if your retained data changes by more than 5% per day.
| Active protected data | vCPU | RAM | Disk | Monthly bandwidth |
|---|---|---|---|---|
| Up to 100 GB | 4 vCPU | 8 GB | 240 GB NVMe local disk + 1 TB remote object storage | 300 GB |
| 100 GB to 1 TB | 8 vCPU | 16 GB | 480 GB NVMe local disk + 4 TB remote object storage | 2 TB |
| 1 TB to 5 TB | 12 vCPU | 32 GB | 1.92 TB NVMe local disk + 20 TB remote object storage | 8 TB |
The disk figures include room for the operating system, logs, application data, and temporary database dumps; remote storage is separate and should not be mounted as a live filesystem.
1. Install Restic and database tools
Update package metadata and install Restic, PostgreSQL client utilities, and curl. Remove postgresql-client from the command if the server does not run PostgreSQL.
sudo apt update
sudo apt install -y restic postgresql-client curl
restic versionConfirm that Restic reports a version number. The commands below assume backup data is stored in an S3-compatible bucket named server-backups and a prefix named prod-01. Replace the endpoint, bucket, database name, and paths with your own values.
2. Create restricted backup credentials
Create a bucket specifically for backups, enable bucket versioning if available, and create an access key restricted to that bucket. The key needs permission to list, read, write, and delete objects under the repository prefix because Restic retention pruning removes expired pack files.
Keep the Restic repository password separate from object-storage credentials. If the password is lost, encrypted backups cannot be recovered. Store it in an offline password manager and keep an emergency recovery copy under your organization’s access-control process.
sudo install -d -m 0700 /root/.config/restic
sudo sh -c 'umask 077; openssl rand -base64 48 > /root/.config/restic/password'Create an environment file containing the repository location and storage credentials. Use the actual S3 endpoint supplied by your storage service.
sudo tee /root/.config/restic/backup.env > /dev/null <<'EOF'
export RESTIC_REPOSITORY='s3:https://s3.example.net/server-backups/prod-01'
export RESTIC_PASSWORD_FILE='/root/.config/restic/password'
export AWS_ACCESS_KEY_ID='REPLACE_WITH_RESTRICTED_ACCESS_KEY'
export AWS_SECRET_ACCESS_KEY='REPLACE_WITH_RESTRICTED_SECRET_KEY'
export AWS_DEFAULT_REGION='us-east-1'
EOF
sudo chmod 0600 /root/.config/restic/backup.envFor a custom endpoint requiring a specific region or signing configuration, use the values required by that object-storage service. Never place this file in a public Git repository or an application deployment directory.
3. Initialize and verify the encrypted repository
Load the environment and initialize the repository once. Restic creates encrypted metadata and key material in the remote location.
sudo bash -c 'source /root/.config/restic/backup.env && restic init'
sudo bash -c 'source /root/.config/restic/backup.env && restic snapshots'The first command should report that the repository was initialized. The second command normally returns no snapshots until the first backup completes. If initialization fails, check DNS resolution, firewall egress on TCP port 443, endpoint spelling, bucket permissions, and the system clock.
4. Create a backup script with a PostgreSQL dump
The script creates a temporary PostgreSQL dump, records installed packages, then backs up selected system and application directories. It does not back up volatile directories such as /proc, /sys, /dev, or temporary caches.
Change appdb to the database name used by your application. If PostgreSQL is remote, replace the local sudo -u postgres command with a restricted database account and a protected .pgpass file.
sudo tee /usr/local/sbin/restic-backup.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
source /root/.config/restic/backup.env
WORKDIR=$(mktemp -d /var/backups/restic.XXXXXX)
trap 'rm -rf "$WORKDIR"' EXIT
dpkg-query -W -f='${binary:Package}\t${Version}\n' > "$WORKDIR/packages.tsv"
sudo -u postgres pg_dump -Fc --no-owner appdb > "$WORKDIR/appdb.dump"
PATHS=()
for path in /etc /home /srv /var/www "$WORKDIR"; do
[ -e "$path" ] && PATHS+=("$path")
done
restic backup \
--tag dedicated-server \
--tag nightly \
--exclude /home/*/.cache \
--exclude /var/www/*/cache \
"${PATHS[@]}"
EOF
sudo chmod 0700 /usr/local/sbin/restic-backup.shFor Docker workloads, back up Compose files, environment files, named-volume exports, and database dumps rather than copying actively changing database volume files. For MySQL or MariaDB, use mariadb-dump --single-transaction for InnoDB databases. For game servers, stop or save the world before backup if the game does not safely flush files while running.
5. Run the first backup manually
Run the script manually before automating it. The first backup uploads all selected data and may take hours for multi-terabyte servers.
sudo /usr/local/sbin/restic-backup.sh
sudo bash -c 'source /root/.config/restic/backup.env && restic snapshots --tag dedicated-server'
sudo bash -c 'source /root/.config/restic/backup.env && restic stats latest'Review the snapshot summary and confirm that expected directories and the database dump are present. Restic snapshots show both raw file size and the amount of newly uploaded data after deduplication.
6. Add retention and integrity checks
A common retention policy keeps 7 daily, 4 weekly, and 12 monthly restore points. This provides recent recovery points while preserving a one-year history. Run prune only after forget; pruning reclaims unreferenced remote storage.
sudo tee /usr/local/sbin/restic-maintenance.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
source /root/.config/restic/backup.env
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
restic check
EOF
sudo chmod 0700 /usr/local/sbin/restic-maintenance.sh
sudo /usr/local/sbin/restic-maintenance.shRun maintenance after the first successful snapshot. If object-lock retention is enabled in the bucket, configure it to match or exceed your intended retention period; otherwise Restic may be unable to delete expired data during pruning.
7. Schedule backups with systemd timers
Systemd timers provide logs, missed-run handling, and clear service status. This configuration starts backups every night at 02:15 with up to 20 minutes of randomized delay. It runs weekly maintenance at 04:30 on Sunday.
sudo tee /etc/systemd/system/restic-backup.service > /dev/null <<'EOF'
[Unit]
Description=Restic encrypted dedicated server backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-backup.sh
Nice=10
IOSchedulingClass=best-effort
EOF
sudo tee /etc/systemd/system/restic-backup.timer > /dev/null <<'EOF'
[Unit]
Description=Nightly Restic backup timer
[Timer]
OnCalendar=*-*-* 02:15:00
RandomizedDelaySec=20m
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo tee /etc/systemd/system/restic-maintenance.service > /dev/null <<'EOF'
[Unit]
Description=Restic retention and repository check
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-maintenance.sh
Nice=15
IOSchedulingClass=idle
EOF
sudo tee /etc/systemd/system/restic-maintenance.timer > /dev/null <<'EOF'
[Unit]
Description=Weekly Restic maintenance timer
[Timer]
OnCalendar=Sun *-*-* 04:30:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer restic-maintenance.timer
systemctl list-timers --all | grep restic8. Test a restore every month
A backup is only useful if it restores correctly. Restore the latest snapshot to a separate directory, compare configuration files, and validate the database dump. Do not restore directly over production files during a test.
sudo mkdir -p /restore-test
sudo bash -c 'source /root/.config/restic/backup.env && restic restore latest --target /restore-test'
sudo diff -qr /etc /restore-test/etc | head -50
sudo -u postgres createdb restore_test
sudo -u postgres pg_restore --clean --if-exists --no-owner -d restore_test /restore-test/var/backups/restic.*/appdb.dumpThe temporary directory name inside the restored path varies because the script uses mktemp. Locate the dump with sudo find /restore-test -name appdb.dump before running pg_restore. After validation, remove the test database and restore directory.
sudo -u postgres dropdb restore_test
sudo rm -rf /restore-testTroubleshooting common backup failures
Repository is locked
An interrupted process can leave a lock. First confirm no backup or maintenance job is running with systemctl status restic-backup.service restic-maintenance.service. If no process is active, remove stale locks with sudo bash -c 'source /root/.config/restic/backup.env && restic unlock'.
Permission denied while reading application files
Run the backup service as root, but keep credentials readable only by root. Check inaccessible paths using sudo journalctl -u restic-backup.service -n 100 --no-pager. Add only required directories to the script instead of backing up the entire root filesystem.
Database dump fails
Verify the database exists and PostgreSQL is running with sudo systemctl status postgresql and sudo -u postgres psql -l. For larger databases, ensure local free disk space exceeds the expected dump size before the backup window begins.
Backups are slow or consume too much bandwidth
Schedule the timer outside peak traffic, use Nice=10 and best-effort I/O scheduling as shown, and exclude rebuildable caches. Check upload throughput with curl -o /dev/null https://speed.hetzner.de/100MB.bin only if that test endpoint is reachable from your network; otherwise measure traffic with iftop or provider monitoring.
Prune does not reduce remote storage immediately
Versioned buckets retain old object versions until their lifecycle policy removes them. Confirm versioning and lifecycle settings in the storage console, then calculate storage based on both current and noncurrent object retention.