A complete walkthrough for setting up encrypted, deduplicated server backups with Restic and S3-compatible storage on Ubuntu, including systemd timers, verification, and restore procedures.
Introduction
Backups are the single most important safety net for any production server. Yet far too many teams rely on ad-hoc rsync scripts or manual snapshots that are never tested. Restic is an open-source backup program that is fast, secure, and efficient. It encrypts every backup by default, deduplicates data at the block level, and supports a wide range of storage backends including Amazon S3, Backblaze B2, and any S3-compatible endpoint such as MinIO.
In this guide we walk through installing Restic on Ubuntu, configuring an S3 backend, writing a backup script, scheduling it with a systemd timer, and verifying and restoring backups. By the end, we will have a fully automated backup pipeline that runs without human intervention.
Prerequisites
- Ubuntu 22.04 or 24.04 server with root or sudo access
- An S3 bucket (AWS, Backblaze B2, or MinIO) with an IAM user that has
s3:PutObject,s3:GetObject,s3:ListBucket, ands3:DeleteObjectpermissions - AWS CLI configured or environment variables ready
1. Install Restic
The version in the Ubuntu repositories is usually outdated. We recommend downloading the latest binary directly from GitHub:
# Download the latest Restic binary
RESTIC_VERSION=0.17.3
wget https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2
bunzip2 restic_${RESTIC_VERSION}_linux_amd64.bz2
chmod +x restic_${RESTIC_VERSION}_linux_amd64
sudo mv restic_${RESTIC_VERSION}_linux_amd64 /usr/local/bin/restic
# Verify
restic version
2. Configure S3 Backend Credentials
Create a credentials file that the backup script will source. Store it outside any web-accessible directory:
sudo mkdir -p /etc/restic
sudo tee /etc/restic/env.sh > /dev/null << 'EOF'
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export RESTIC_REPOSITORY="s3:s3.us-east-1.amazonaws.com/my-backup-bucket"
export RESTIC_PASSWORD="a-very-strong-encryption-passphrase"
EOF
sudo chmod 600 /etc/restic/env.sh
Important: Replace the placeholder values with your real credentials. The RESTIC_PASSWORD is used to encrypt the repository. Losing it means losing access to all backups permanently.
3. Initialize the Repository
source /etc/restic/env.sh
restic init
Restic creates the repository structure inside the S3 bucket. We only need to do this once.
4. Create the Backup Script
sudo tee /usr/local/bin/restic-backup.sh > /dev/null << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
# Load credentials
source /etc/restic/env.sh
# Directories to back up
BACKUP_PATHS="/etc /home /var/www /var/lib/mysql"
EXCLUDE_FILE="/etc/restic/excludes.txt"
# Create exclude file if it does not exist
if [ ! -f "$EXCLUDE_FILE" ]; then
cat > "$EXCLUDE_FILE" << 'EXCL'
*.tmp
*.log
*.cache
node_modules
.git
EXCL
fi
# Run the backup
restic backup $BACKUP_PATHS \
--exclude-file="$EXCLUDE_FILE" \
--tag "auto" \
--verbose
# Prune old snapshots: keep 7 daily, 4 weekly, 6 monthly
restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--prune
# Verify repository integrity
restic check
echo "[$(date)] Backup completed successfully"
SCRIPT
sudo chmod +x /usr/local/bin/restic-backup.sh
5. Schedule with systemd Timer
Using a systemd timer instead of cron gives us better logging, dependency management, and failure notifications.
5.1 Create the Service Unit
sudo tee /etc/systemd/system/restic-backup.service > /dev/null << 'EOF'
[Unit]
Description=Restic Backup to S3
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/restic-backup.sh
Nice=10
IOSchedulingClass=idle
[Install]
WantedBy=multi-user.target
EOF
5.2 Create the Timer Unit
sudo tee /etc/systemd/system/restic-backup.timer > /dev/null << 'EOF'
[Unit]
Description=Run Restic Backup Daily at 2 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=900
[Install]
WantedBy=timers.target
EOF
5.3 Enable and Start
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
# Verify the timer is active
systemctl list-timers | grep restic
6. Verifying Backups
A backup that has never been tested is not a backup. We should verify regularly:
source /etc/restic/env.sh
# List all snapshots
restic snapshots
# Check repository integrity
restic check --read-data-subset=5%
# Mount a snapshot to browse files (requires FUSE)
sudo apt install -y fuse
mkdir -p /mnt/restic
restic mount /mnt/restic &
ls /mnt/restic/snapshots/latest/
7. Restoring from a Backup
When disaster strikes, Restic makes restoration straightforward:
source /etc/restic/env.sh
# Restore the latest snapshot to a target directory
restic restore latest --target /tmp/restore
# Restore a specific snapshot
restic snapshots # find the snapshot ID
restic restore abc12345 --target /tmp/restore
# Restore specific files
restic restore latest --target /tmp/restore --include "/etc/nginx"
8. Monitoring and Alerting
For production environments, we recommend piping backup results to a monitoring system. A simple approach is to send a webhook on failure:
# Add to the end of restic-backup.sh
if [ $? -ne 0 ]; then
curl -X POST -H "Content-Type: application/json" \
-d '{"text":"Restic backup FAILED on '"$(hostname)"'"}' \
https://hooks.slack.com/services/YOUR/WEBHOOK/URL
fi
Alternatively, use a dead man's switch service like Healthchecks.io. Add a curl ping at the end of a successful backup so the service alerts us if the ping stops arriving.
With Restic and S3, we get encrypted, deduplicated, and versioned backups that run automatically via systemd. The entire setup takes less than 30 minutes and protects us against data loss, ransomware, and accidental deletion. We recommend testing a full restore at least once a quarter to ensure the pipeline works end to end.
Frequently asked questions
How do I install Restic on Ubuntu without the outdated apt package?
Download the release binary from Restic's GitHub releases, since the version in the Ubuntu repositories is usually outdated. For version 0.17.3 the file is restic_0.17.3_linux_amd64.bz2. Unpack it with bunzip2, make it executable with chmod +x, move it to /usr/local/bin/restic and confirm the install with restic version.
How do I connect Restic to an S3 bucket?
Put AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, RESTIC_REPOSITORY and RESTIC_PASSWORD in /etc/restic/env.sh and lock the file down with chmod 600. The repository value looks like s3:s3.us-east-1.amazonaws.com/my-backup-bucket. Source the file and run restic init once to create the repository. The IAM user needs s3:PutObject, s3:GetObject, s3:ListBucket and s3:DeleteObject.
Does Restic encrypt backups stored in S3?
Yes. Restic encrypts every backup by default and deduplicates data at the block level, whether the target is Amazon S3, Backblaze B2 or an S3-compatible endpoint such as MinIO. The RESTIC_PASSWORD value encrypts the repository, and losing that password means losing access to all of the backups permanently.
How do I schedule Restic backups with a systemd timer?
Create a oneshot restic-backup.service that runs /usr/local/bin/restic-backup.sh after network-online.target, with Nice=10 and IOSchedulingClass=idle. Pair it with restic-backup.timer using OnCalendar=*-*-* 02:00:00, Persistent=true and RandomizedDelaySec=900, then run sudo systemctl daemon-reload and sudo systemctl enable --now restic-backup.timer. A systemd timer gives better logging, dependency management and failure notifications than cron.
How do I prune old Restic snapshots automatically?
Run restic forget with retention flags and --prune right after each backup. For example, --keep-daily 7, --keep-weekly 4 and --keep-monthly 6 keep roughly a week of daily snapshots, a month of weekly ones and six months of monthly ones. Following up with restic check verifies repository integrity.
How do I verify that Restic backups can actually be restored?
Source /etc/restic/env.sh, list snapshots with restic snapshots and run restic check --read-data-subset=5% to check repository integrity against part of the stored data. To browse files, install FUSE, run restic mount /mnt/restic and look under /mnt/restic/snapshots/latest/. A full test restore at least once a quarter proves the pipeline works end to end.
How do I restore files from a Restic backup?
Run restic restore latest --target /tmp/restore to put the most recent snapshot into a separate directory. For an older point in time, find the snapshot ID with restic snapshots and pass it in place of latest, as in restic restore abc12345 --target /tmp/restore. Adding --include "/etc/nginx" restores only that path.
How do I get alerted if a Restic backup stops running?
One option is a dead man's switch service such as Healthchecks.io. Add a curl ping at the end of a successful backup, and the service raises an alert when the pings stop arriving. A webhook sent on failure, for example to a Slack channel, is another way to pipe backup results into monitoring.
Read how we handle it in Cloud & Server Management.
Related Articles
Server Hardening Checklist for Ubuntu 24.04 - The Complete Guide
A comprehensive server hardening checklist for Ubuntu 24.04, covering SSH configuration, firewall setup, fail2ban, unattended upgrades, CIS benchmarks, audit logging, and kernel hardening.
LaravelMastering Laravel Workers: A Step-by-Step Guide to Setting Up a Laravel Worker Server on Ubuntu
A comprehensive guide to setting up Laravel queue workers on Ubuntu, covering Redis configuration, job creation, Supervisor process management, Horizon monitoring, scaling, and troubleshooting.
Server & DevOpsHow to Run Cron in Kubernetes So Jobs Never Overlap or Vanish
Scheduled work in Kubernetes fails in two quiet ways, a slow job that starts a second copy of itself and a schedule that silently stops firing. Both are configuration, not luck. This covers the CronJob fields that control concurrency, missed schedules, history retention and failure handling, with the actual defaults, so the nightly billing run is still there in the morning and there is a log to read when it is not.