Skip to main content

Restic: encrypted backups with deduplication for your Linux server

Rogelio Guerra Riverón
Author
Rogelio Guerra Riverón
Building my own web infrastructure from scratch. Here I document each step: servers, networks, containers and everything that comes along.
Table of Contents

I already had backups with rsync and cron, but rsync copies files, not snapshots. If you accidentally delete a file and the backup syncs before you notice, you lose it. Restic solves that and adds something rsync will never provide: AES-256 encryption, deduplication, and snapshots with navigable history.

What makes Restic different
#

FeaturersyncRestic
AES-256 encryptionNoYes
DeduplicationNoYes (block-level)
Navigable snapshotsNoYes
Multiple backendsNoSFTP, S3, Backblaze, rclone…
Integrity checkingNorestic check
Retention policyManualrestic forget --prune

Deduplication is especially useful for database backups and configuration directories that change little: a Restic repository with 6 months of daily backups usually takes up much less space than 180 full copies.

Installation
#

On Ubuntu/Debian, the official repository version is usually outdated. Better to download the binary directly from the official GitHub releases:

# Descarga la última versión (ajusta la versión si hay una más reciente)
wget https://github.com/restic/restic/releases/download/v0.17.3/restic_0.17.3_linux_amd64.bz2
bunzip2 restic_0.17.3_linux_amd64.bz2
chmod +x restic_0.17.3_linux_amd64
sudo mv restic_0.17.3_linux_amd64 /usr/local/bin/restic

# Verificar
restic version

Or with system repositories if you don’t mind the version:

sudo apt install restic      # Debian/Ubuntu
sudo dnf install restic      # Fedora/RHEL

Key concepts before you start
#

  • Repository: the destination where Restic stores the backups. It can be a local folder, an SFTP server, an S3 bucket, etc.
  • Snapshot: every time you run restic backup, Restic saves a point-in-time snapshot. Snapshots share deduplicated blocks, so they don’t multiply the space used.
  • Password: the repository is encrypted with a password. Without it, the data is unreadable. Store it in a password manager or in a file separate from the backup.

Initialize a repository
#

Option A: local repository
#

restic init --repo /opt/backups/mi-servidor

Option B: repository on remote server via SFTP
#

restic init --repo sftp:usuario@servidor-backup:/opt/restic/mi-servidor

Option C: repository on S3 (or compatible: Backblaze B2, MinIO…)
#

export AWS_ACCESS_KEY_ID="TU_ACCESS_KEY_AQUI"
export AWS_SECRET_ACCESS_KEY="TU_SECRET_KEY_AQUI"

restic init --repo s3:s3.eu-west-1.amazonaws.com/mi-bucket-backups/mi-servidor

In all three cases, Restic will ask you for a password to encrypt the repository. Guard it well — without it you can’t restore anything.

Environment variables file
#

To avoid typing the password with every Restic command, create a variables file:

# /etc/restic/env.sh  (solo root puede leerlo)
sudo mkdir -p /etc/restic
sudo chmod 700 /etc/restic
sudo tee /etc/restic/env.sh > /dev/null <<'EOF'
export RESTIC_REPOSITORY="/opt/backups/mi-servidor"
export RESTIC_PASSWORD="TU_CONTRASENA_AQUI"
EOF
sudo chmod 600 /etc/restic/env.sh

From now on, before any Restic command:

source /etc/restic/env.sh

Or with environment variables inline for scripts:

env $(cat /etc/restic/env.sh | grep export | sed 's/export //') restic snapshots

First backup
#

source /etc/restic/env.sh

# Backup de los datos de Docker y configuraciones
restic backup \
  /opt/app/ \
  /etc/nginx/ \
  /home/usuario/ \
  --exclude='/opt/app/logs' \
  --exclude='*.tmp' \
  --tag servidor-web

The output shows how many files it processed, how many are new, and the space saved by deduplication:

Files:        1234 new,   567 changed, 8901 unmodified
Dirs:          45 new,    12 changed,  89 unmodified
Added to the repo: 234.567 MiB

processed 10702 files, 1.234 GiB in 0:23
snapshot a1b2c3d4 saved

View and navigate snapshots
#

# Listar todos los snapshots
restic snapshots

# Listar ficheros dentro de un snapshot específico
restic ls a1b2c3d4

# Buscar un fichero en todos los snapshots
restic find --tag servidor-web nombre-fichero.conf

Output of restic snapshots:

ID        Time                 Host          Tags           Paths
────────────────────────────────────────────────────────────────────
a1b2c3d4  2026-05-11 03:00:02  mi-servidor   servidor-web   /opt/app, /etc/nginx, /home/usuario
e5f6a7b8  2026-05-10 03:00:01  mi-servidor   servidor-web   /opt/app, /etc/nginx, /home/usuario

Restore data
#

Complete restoration of a snapshot
#

# Restaura todo en /tmp/restauracion para revisar antes de mover
restic restore a1b2c3d4 --target /tmp/restauracion

Restore only a file or directory
#

# Restaura solo nginx.conf del snapshot más reciente
restic restore latest --target /tmp/restauracion \
  --include /etc/nginx/nginx.conf

Mount the repository as a filesystem (useful for exploring)
#

# Requiere FUSE instalado: sudo apt install fuse
mkdir -p /mnt/restic-backups
restic mount /mnt/restic-backups &

# Ahora puedes navegar por todos los snapshots
ls /mnt/restic-backups/snapshots/
# → a1b2c3d4/  e5f6a7b8/  latest/

# Cuando acabes
fusermount -u /mnt/restic-backups

Automatic retention policy
#

Without a retention policy, the repository grows indefinitely. This is the configuration I use for daily backups:

# 7 días diarios, 4 semanas, 6 meses, 1 año
restic forget \
  --keep-daily   7 \
  --keep-weekly  4 \
  --keep-monthly 6 \
  --keep-yearly  1 \
  --tag servidor-web \
  --prune

--prune physically removes unreferenced data. Without it, forget only removes the snapshot metadata but doesn’t free up space.

Verify repository integrity
#

# Verificación rápida de metadatos
restic check

# Verificación completa leyendo todos los datos (lento, hazlo mensualmente)
restic check --read-data

If restic check fails, the repository has corruption. That’s why it’s always recommended to have at least two repositories in different destinations (the well-known 3-2-1 rule).

Automate with systemd timer
#

Restic integrates perfectly with systemd timers, which allow capturing output in journald and executing the task even if the server was powered off at the scheduled time.

/etc/systemd/system/restic-backup.service:

[Unit]
Description=Backup diario con Restic
After=network.target

[Service]
Type=oneshot
User=root
EnvironmentFile=/etc/restic/env.sh
ExecStart=/usr/local/bin/restic backup \
    /opt/app/ \
    /etc/nginx/ \
    /home/usuario/ \
    --exclude='/opt/app/logs' \
    --tag servidor-web
ExecStartPost=/usr/local/bin/restic forget \
    --keep-daily 7 \
    --keep-weekly 4 \
    --keep-monthly 6 \
    --tag servidor-web \
    --prune
StandardOutput=journal
StandardError=journal

/etc/systemd/system/restic-backup.timer:

[Unit]
Description=Ejecuta backup Restic diariamente

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10min

[Install]
WantedBy=timers.target

Activate:

sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer

# Verificar que está activo
sudo systemctl list-timers restic-backup.timer

RandomizedDelaySec=10min distributes backups in random windows to prevent all servers from hitting the SFTP or S3 destination at the same time.

Email notifications on completion
#

By combining with the email notification system with msmtp, we can receive a backup summary. Modify ExecStart with a wrapper script:

/usr/local/bin/restic-backup.sh:

#!/bin/bash
set -euo pipefail

source /etc/restic/env.sh

LOG=$(mktemp)
STATUS=0

restic backup \
    /opt/app/ \
    /etc/nginx/ \
    /home/usuario/ \
    --exclude='/opt/app/logs' \
    --tag servidor-web > "$LOG" 2>&1 || STATUS=$?

restic forget \
    --keep-daily 7 \
    --keep-weekly 4 \
    --keep-monthly 6 \
    --tag servidor-web \
    --prune >> "$LOG" 2>&1 || STATUS=$?

SUBJECT="[Backup] $(hostname) - $(date '+%Y-%m-%d')"
[ $STATUS -ne 0 ] && SUBJECT="[ERROR Backup] $(hostname) - $(date '+%Y-%m-%d')"

cat "$LOG" | mail -s "$SUBJECT" usuario@ejemplo.com
rm -f "$LOG"

exit $STATUS
chmod +x /usr/local/bin/restic-backup.sh

And in the .service, change ExecStart and ExecStartPost with a single line:

ExecStart=/usr/local/bin/restic-backup.sh

Weekly integrity check
#

Add a second timer for the weekly verification, which is more expensive:

/etc/systemd/system/restic-check.service:

[Unit]
Description=Verificación semanal de integridad Restic
After=network.target

[Service]
Type=oneshot
User=root
EnvironmentFile=/etc/restic/env.sh
ExecStart=/usr/local/bin/restic check
StandardOutput=journal
StandardError=journal

/etc/systemd/system/restic-check.timer:

[Unit]
Description=Verifica integridad del repositorio Restic cada semana

[Timer]
OnCalendar=Sun *-*-* 04:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable --now restic-check.timer

View the status in the system log
#

# Último backup
journalctl -u restic-backup.service -n 50

# Seguir en tiempo real durante la ejecución manual
journalctl -u restic-backup.service -f

# Historial de ejecuciones del timer
systemctl status restic-backup.timer

Conclusion
#

Restic doesn’t completely replace rsync for live directory synchronization cases, but for backups with history, it’s clearly superior: built-in encryption, transparent deduplication, and browsable snapshots without additional scripts. Integration with systemd timers and email notifications closes the loop: you know the backup ran, when it failed, and you can restore any point in time in minutes.

The real key to any backup strategy is not the software you choose, but verifying that you can restore. Test restic restore in a temporary directory before you really need it.