resticprofile examples: a production configuration, explained
The official documentation describes every key resticprofile accepts, one reference page at a time. It does not show how they fit together on a server you depend on, or where retention belongs when that server is not allowed to delete its own backups.
A production resticprofile setup needs two configuration files. The server's profile
holds the repository, an env-file with a storage key that cannot delete data, a
password-file, the sources and excludes, a database dump in run-before and a
systemd schedule. Because that key cannot delete backup data, forget, prune and
check run from a second profile on an admin host, using keep-within
rules and read-data-subset. Both samples below are YAML, checked against
resticprofile 0.33.1.
Versions and file format
We checked everything here against resticprofile 0.33.1, the latest release, published on 15
April 2026, and restic 0.19.1, published on 5 July 2026. resticprofile reads TOML, YAML, JSON and HCL, and its
documentation recommends TOML or YAML. The samples are YAML, and both produce the expected
restic command lines under that version's show and --dry-run.
Both files declare version: "1". Configuration format 2 exists, but the current
documentation still calls it a preview that is "for testing and feedback only", so a
production file should not depend on it. In version 1 a retention section does nothing until
you switch it on, and some inherited lists merge into the parent's list by position instead
of replacing it. Both behaviours come up below.
Two hosts, two profiles
The server backs up with a storage key that cannot delete data, so whoever compromises the
server cannot destroy its history. restic's documentation spells out the consequence:
forget and prune "require full read, write and
delete access to the repository", and the recommended setup for append-only repositories is a
separate, well-secured client for that maintenance. So the server profile has no retention
at all. A second profile, on an admin host the server cannot reach, removes old snapshots,
prunes and checks the data. The
Backblaze B2 version cleanup belongs
there too.
The admin host also holds a copy of the repository password. restic warns that losing the password means the data is irrecoverably lost, and a password that only exists on the server disappears with the server.
The server profile
# /etc/resticprofile/profiles.yaml on the server (web1)
version: "1"
web1:
repository: "s3:https://objects.example.net/acme-web1-restic"
password-file: "/etc/resticprofile/web1.key"
env-file:
- "/etc/resticprofile/web1.env"
lock: "/run/resticprofile-web1.lock"
status-file: "/var/lib/resticprofile/status.json"
prometheus-save-to-file: "/var/lib/node_exporter/textfile/resticprofile.prom"
backup:
source:
- "/etc"
- "/home"
- "/srv"
- "/var/www"
- "/var/backups/db"
exclude:
- "node_modules"
- "/home/*/.cache"
exclude-file: "excludes.txt"
exclude-caches: true
tag:
- "web1"
extended-status: true
run-before: "runuser -u postgres -- pg_dump --format=custom shop > /var/backups/db/shop.dump"
run-after-fail: 'logger -t resticprofile "web1 backup failed: $ERROR_MESSAGE"'
run-finally: "rm -f /var/backups/db/shop.dump"
schedule: "*-*-* 01:30"
schedule-permission: system
schedule-lock-wait: 2h
schedule-log: "/var/log/resticprofile-web1.log"
schedule-after-network-online: true # /etc/resticprofile/web1.env (owner root, mode 600)
# Storage key that can delete only restic's lock files. The key that can delete data stays on the admin host.
AWS_ACCESS_KEY_ID=server-key-id
AWS_SECRET_ACCESS_KEY=server-key-secret
# AWS_DEFAULT_REGION=eu-central-1 only if your provider needs a region # on web1, as root, once
install -d -m 700 /etc/resticprofile /var/backups/db
install -d -m 755 /var/lib/resticprofile
install -d -m 755 /var/lib/node_exporter/textfile # or your textfile collector's directory
resticprofile generate --random-key > /etc/resticprofile/web1.key
# write web1.env as shown above, then
chmod 600 /etc/resticprofile/web1.key /etc/resticprofile/web1.env
# copy web1.key to the admin host before anything else Repository and secrets
repository uses restic's form for S3-compatible storage,
s3:https://host/bucket. restic reads the storage credentials from
AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, which live in the dotenv
file named by env-file. The repository password lives in
password-file. Both files are root-only, so the YAML holds no secret and can go
into version control.
The server key needs one narrow delete right. restic removes its lock file at the end of
every command, and with a key that cannot delete anything each run leaves its lock behind.
The backup still exits 0, but the admin host's check and forget
then fail with "repository is already locked". Allow deletes under locks/ only,
which is also what restic's rest-server permits in append-only mode. On MinIO, this policy
left no locks behind and still refused to delete snapshots:
{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": ["s3:GetBucketLocation", "s3:ListBucket"], "Resource": ["arn:aws:s3:::acme-web1-restic"]},
{"Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": ["arn:aws:s3:::acme-web1-restic/*"]},
{"Effect": "Allow", "Action": ["s3:DeleteObject"], "Resource": ["arn:aws:s3:::acme-web1-restic/locks/*"]}
]
}
One default works against the secrets split. schedule-capture-environment
defaults to RESTIC_*, so every RESTIC_ variable present when you run
resticprofile schedule is copied into the generated systemd unit as an
Environment= line, and resticprofile 0.33.1 writes unit files with mode 0644. An
exported RESTIC_PASSWORD in that shell ends up readable by every local account,
and systemctl show prints it to them even if you tighten the file mode. Run
schedule from a clean shell, or set
schedule-capture-environment: [] in the section that holds the schedule.
Source, excludes and tags
An exclude pattern without a leading slash matches a complete path component
anywhere, so node_modules drops every directory of that name. A leading slash
anchors the pattern at the root. exclude-file takes one pattern per line with
# comments, and a relative path resolves against the directory holding the
configuration file. A path listed directly in source is always saved, but
excludes still filter what is inside it: list /srv/app/node_modules as a source
while node_modules is excluded and the snapshot holds only the empty directory.
exclude-caches skips the contents of any directory marked with a CACHEDIR.TAG
file and keeps the tag file itself.
tag labels every snapshot, and the admin side makes use of it. Running
resticprofile web1.backup --tag keep before a risky upgrade adds a
keep tag, and the maintenance policy's keep-tag never removes that
snapshot.
The database dump
The dump sits in the backup section's run-before. At the profile
root it would run before every restic command the profile runs, snapshots
included. pg_dump makes a consistent export even while the database is in use.
A hook that exits non-zero aborts the run and triggers run-after-fail, which
receives ERROR_MESSAGE, ERROR_EXIT_CODE and
ERROR_STDERR. In my view that is the right default, because a snapshot without its
database is a failure you only discover during a restore. Cleanup belongs in
run-finally rather than run-after, because run-after only
runs when everything succeeded, and a failed night would otherwise leave an unencrypted dump
on disk until the next good run.
Scheduling with systemd
schedule takes systemd calendar syntax, so
systemd-analyze calendar "*-*-* 01:30" shows the next run. With
schedule-permission: system, resticprofile writes
resticprofile-backup@profile-web1.service and a matching timer into
/etc/systemd/system, and the job runs as root. The default timer sets
Persistent=true, so a run missed while the machine was off starts once the timer
is active again after boot. To change the permission later, unschedule first, edit, then
schedule again.
lock is resticprofile's own lock file, which stops two runs of the profile
overlapping. schedule-lock-wait is how long a scheduled run waits for that lock
and for restic's repository lock. It matters here because in restic 0.19.1
check, forget and prune all take an exclusive lock, and
the admin host runs them against the same repository. schedule-log sends output
to a file that resticprofile appends to but never rotates, so give it a logrotate rule, or
leave the key out and systemd puts the output in the journal instead.
schedule-after-network-online adds After=network-online.target. That
only sets the order. The unit gets no Wants= line, so it waits for the network
only if something else on the host pulls that target in.
The unit calls resticprofile run-schedule, which reads the configuration afresh
every time. Changes to sources, excludes or hooks apply on the next run. Changing when a job
runs, or as whom, needs resticprofile schedule again.
Status and metrics
status-file keeps a JSON summary of the last backup, forget and check: success,
time, error and duration. resticprofile stores the forget result under the key
retention, so look for that name in your monitoring. prometheus-save-to-file writes metrics
in Prometheus text format, for the backup command only; point it at your textfile collector's
directory, and keep the .prom ending node_exporter expects. extended-status: true
adds file and byte counts at the cost of restic's normal progress output. resticprofile
creates neither file's directory, which is why the setup above creates both.
Retention versus forget
Both end in restic forget. The difference is when it runs and what it selects.
| Aspect | retention section | forget section |
|---|---|---|
| When it runs | Inside resticprofile backup, before or after the backup | When you run resticprofile forget, or on its own schedule |
| Switched on by | after-backup: true or before-backup: true. In version 1 neither is the default, so keep rules alone do nothing | Always active when called |
| After a failed backup | Skipped, because after-backup only follows a successful backup | Independent of backups |
| Which snapshots | Adds the backup's source list as --path unless you set path | Exactly the flags you write |
| Scheduling | Deprecated. Schedule forget instead | Supported |
| Delete rights needed on | The server that runs the backup | Whichever host runs it |
The path default is the subtle one. restic only considers snapshots that include
every listed path, so after you add a directory to source, retention stops
matching the snapshots taken before the change and never removes them. The last row is why
retention cannot live on an append-only server.
The admin maintenance profile
# /etc/resticprofile/profiles.yaml on the admin host
version: "1"
# Shared rules only: no repository, no schedule, never run directly.
policy:
forget:
keep-within: "14d"
keep-within-weekly: "3m"
keep-within-monthly: "1y"
keep-tag:
- "keep"
prune: true
check:
read-data-subset: "{{ .Now.Day }}/31"
web1:
inherit: policy
repository: "s3:https://objects.example.net/acme-web1-restic"
password-file: "/etc/resticprofile/keys/web1.key"
env-file:
- "/etc/resticprofile/web1-admin.env" # the key that CAN delete
status-file: "/var/lib/resticprofile/web1.json"
forget:
schedule: "Sun 03:00"
schedule-permission: system
schedule-lock-wait: 2h
schedule-log: "/var/log/resticprofile-web1-forget.log"
check:
schedule: "*-*-* 05:30"
schedule-permission: system
schedule-lock-wait: 2h
schedule-log: "/var/log/resticprofile-web1-check.log"
The policy keeps every snapshot from the last 14 days, the newest of each week for three
months and the newest of each month for a year. In restic durations, m means
months. The plain keep-within is deliberate. restic's documentation warns
that an attacker with append-only access can add garbage snapshots so that a count-based
rule, such as keeping three weekly snapshots, keeps the fakes and forgets the real ones. It
recommends keep-within for this setup, because that rule keeps every snapshot
in the window, real ones included. The bucketed keep-within-daily,
-weekly and -monthly forms keep only the newest snapshot in each
day, week or month, so a planted snapshot a few minutes newer than each real one displaces
it just as with a count. Beyond 14 days the weekly and monthly rules carry that risk, which is
why the admin host should look at unexpected snapshots before the window runs out. restic
measures the windows from the newest snapshot and ignores snapshots dated in the future.
prune: true adds --prune, so restic prunes only when forget
removed something.
read-data-subset uses the .Now.Day template, so the check on the
23rd downloads and verifies group 23 of 31, about a thirty-first of the pack files. A 31-day
month reads every group once; shorter months skip the highest groups until the next long
one. A percentage such as 5% is simpler, but restic notes that random subsets
do not guarantee full coverage. Either way, reading data costs download traffic from the
storage provider.
The policy profile holds only the rules and each server's profile inherits it.
Keep list values in one place and check the result with show. In version 1
under 0.33.1, source and exclude merge into the parent's list by
index, while tag and keep-tag replace the parent's list outright.
A one-item child source therefore replaces only the parent's first entry.
Commands to run before trusting it
# on the admin host, once, before the first backup
resticprofile web1.init
# on the server
resticprofile web1.show # the profile as resticprofile resolved it
resticprofile --dry-run web1.backup # print hooks and restic command, run nothing
resticprofile web1.backup # one real run, end to end
resticprofile schedule --all # install every schedule in the file
resticprofile status --all # the installed systemd timers
systemctl list-timers 'resticprofile-*'
# on the admin host
resticprofile web1.forget --dry-run # restic lists what the policy would remove
resticprofile web1.check # today's slice of the data, by hand
resticprofile schedule --all - Compare show with the dry-run
showprints the resolved profile with inheritance applied.--dry-runbeforeweb1.backupprints every hook and the restic command line and runs nothing. Compare the two.restic-arguments-filteris on by default and drops flags restic does not recognise, so a mistypedkeep-within-dalyappears in one and vanishes from the other. - Let restic judge the policyFlags after the command go to restic, so
web1.forget --dry-runon the admin host makes restic list what it would keep and remove, and remove nothing. - Schedule everything as the same userRun
schedule --allas root. The documentation warns against mixing user and system schedules in one run. - Know which status you are reading
resticprofile statusreports the installed timers and journal lines at warning level or above from the last month. Withschedule-logset, a failed night shows there only as systemd's "Failed with result 'exit-code'" line, without the reason. The status file and the log hold the result. - Restore somethingA clean check proves the repository is intact. It does not prove that the repository holds what you need. Restore to a scratch machine and time it.
On Windows
The same keys work. resticprofile schedules through Task Scheduler on Windows 10 or later. A
system schedule needs elevation to install, which resticprofile requests itself,
and user asks for the account password.
schedule-start-when-available, added in 0.33.0, runs a backup that was missed
while the machine was off, and use-fs-snapshot: true in the
backup section lets restic read locked files through VSS. If restic.exe is not detected, set
its full path as restic-binary under global. The two-profile split
carries over unchanged. If you are still choosing a tool, read
restic versus Borg first.
Downsides of the append-only split, and how Veeam compares
The split protects history by withholding one permission from the server. That is access control. It does not make the backups immutable, and its costs are worth weighing against a product built around immutability, such as Veeam Backup & Replication 13.
- The admin key can still destroy everything. The risk moves to the admin host and the storage account login. Whoever compromises either can delete the whole repository, and nothing in this design stops them.
- A second machine to run and watch. Retention, prune and check only happen on the admin host. If it stops, nothing expires, the bucket keeps growing, and nobody notices unless that host's runs are monitored as closely as the backups.
- Deletion is not the only attack. Object storage has no separate overwrite permission, so the server's key can write over an existing object, and on Backblaze B2 it can hide files. Versioning keeps the originals, but getting them back is manual, version by version, and only possible until the cleanup job removes non-current versions. That cleanup cutoff is your detection window.
- Retention can be gamed. The server can add snapshots, and planted ones
defeat count-based rules, as the gotcha below shows. restic's documentation recommends
keep-withinfor append-only repositories for this reason. - The server can still read every backup. It holds the repository password, so an intruder can decrypt all past and future snapshots. The split protects availability and does nothing for confidentiality.
- No console. Scheduling, alerting, reporting and restore tests are yours to build and keep working.
Veeam answers the first and third points differently. Its hardened repository is a Linux server on which backup files "cannot be moved, modified or deleted, but can be copied" for an immutability period of 7 to 9,999 days. Veeam uses the credentials that deploy it once and does not store them, so a compromised backup server cannot reach it. On object storage, Veeam sets S3 Object Lock in compliance mode on every object it uploads and manages the lock per backup chain. restic cannot do that. It runs on a locked bucket but never sets or extends a lock itself, as the Object Lock guide shows.
Veeam's version has its own costs:
- More infrastructure. A backup server, plus a dedicated Linux machine with local or block storage for the hardened repository. NFS and SMB are not allowed, one repository cannot be shared between backup servers, and Veeam Agent for Linux must not be installed on it.
- Root on the repository host is the weak point. The immutability is the
file system's immutable attribute. On our ext4 test system,
chattr -ias root cleared it and the file could then be deleted. Veeam's own appliance image does not allow SSH on a hardened repository and, unless Security Officer accounts are enabled, does not let you turn multi-factor authentication off. A hand-built one needs the same discipline. - More storage. A hardened repository accepts only forward incremental backups with periodic full backups, and an immutable file is merged or deleted only after its period ends. On object storage, Veeam adds a 10-day block generation period to the lock expiry, and its documentation warns that an immutability period longer than the job's retention "will result in extra charges".
- The bucket belongs to Veeam. It needs Object Lock and versioning from creation, no default retention, and no lifecycle rules, which Veeam says are not supported and may break backup and restore.
- A licence past ten workloads. Community Edition is free for up to 10 workloads; beyond that you need a paid licence, counted in instances per protected workload. Veeam's licensing page also notes that the Linux-based Veeam Software Appliance supports only data recovery in Community Edition.
In our view the restic split suits one to a handful of Linux servers on object storage. It is small, cheap and scriptable, and its weak points are known and testable. If you need backups that nobody, including you, can delete for a fixed period, or you already run VMware or Hyper-V, Veeam's hardened or object-lock repository gives you that and this design does not. Between the two sits restic on a bucket with Object Lock, with the limits described in that guide.
Gotchas
- A key that cannot delete leaves locks behind. With no delete right at
all, every server command printed "Remove(<lock/...>) failed: Access Denied" and still
exited 0. The admin host's
checkthen failed with "repository is already locked" (exit 11).schedule-lock-waitonly waits: once the lock was older thanrestic-stale-lock-age(1h by default), resticprofile gave up and suggestedforce-inactive-lock. Either allow deletes underlocks/as shown above, or setforce-inactive-lock: trueon the admin profile, which ranrestic unlockand let the check pass. - A forget on the server blocks every later backup. Running
web1.forgetwith the server key was refused ("Access Denied", exit 3) but left an exclusive lock the server could not remove, and the next backup failed with exit 11 until the admin host ranunlock. Keep forget, prune and unlock on the admin host. - Per-day keep-within rules can be gamed. In a test repository with 84
nightly snapshots, one planted snapshot 30 minutes after each real one left a single real
snapshot under
keep-within-daily: 14dwith the weekly and monthly rules above. Withkeep-within: 14dinstead, all 14 real snapshots from the last two weeks survived next to the fakes. - Changing the source list strands old snapshots. restic forget groups
snapshots by host and path list, and measures each window from that group's newest
snapshot. In a repository whose source list changed in June 2025, this policy run on 23
September 2026 still kept 20 snapshots of the old list and would keep them indefinitely. For a
repository that holds one server, add
group-by: "host"to theforgetsection, which pooled both lists in the test. - A missing directory only warns. Before the textfile directory existed,
each backup logged "saving prometheus file ... no such file or directory" and exited 0, so
the metrics never appeared. A
status-filein a missing directory behaves the same. Check that both files exist after the first run. - Captured variables are visible to every user. With
RESTIC_PASSWORDexported,schedulewrote it into the unit (mode 0644), andsystemctl show -p Environmentprinted it for the nobody account. Rerunningschedulefrom a clean shell rewrote the unit without it, andschedule-capture-environment: []stopped the capture altogether.
How this was tested. On 23 September 2026 we built
resticprofile from the v0.33.1 tag with Go 1.26.8 and ran it with restic 0.19.1 against MinIO
RELEASE.2025-09-07T16-13-09Z in Docker on an Ubuntu 24.04 host, as an S3-compatible stand-in,
with one key lacking s3:DeleteObject and one with full rights. The server and
admin host were two Debian 13 containers running systemd 257 as PID 1, with PostgreSQL 17.11
for the dump hook. We ran both samples as printed apart from endpoint and credentials, started the
units by hand, and checked Persistent= by stopping and restarting a container rather than with
a real reboot. Not tested: a commercial S3 provider, Windows and Task Scheduler, node_exporter
reading the .prom file, and the check rotation over a whole month. The Windows details come
from the documentation, and we rendered the check template for day 23 only. The downsides section rests on these tests and the
linked guides. The Veeam comparison comes from Veeam's version 13 documentation and was not
tested, except that on 26 September 2026 we cleared the immutable attribute as root with
chattr -i, using e2fsprogs 1.47.0 on ext4, and then deleted the file.
Sources
- resticprofile releases (v0.33.1, 15 April 2026), creativeprojects on GitHub
- Getting started: file formats, resticprofile documentation
- Configuration v2 proposal, resticprofile documentation
- Profile reference and retention section reference, resticprofile documentation
- Schedule configuration reference (capture-environment) and global section reference (restic-arguments-filter, restic-binary), resticprofile documentation
- Schedules: retention scheduling deprecated, resticprofile documentation
- Schedule configuration, systemd and schedule commands, resticprofile documentation
- Command hooks, variables and inheritance, resticprofile documentation
- Status file and Prometheus, resticprofile documentation
- Windows Task Scheduler, resticprofile documentation
- systemd/generate.go at v0.33.1 (unit file mode), resticprofile source
- restic releases (0.19.1, 5 July 2026), restic on GitHub
- Removing backup snapshots, including security considerations in append-only mode, restic documentation
- Working with repositories: checking integrity, restic documentation
- Backing up: excluding files and VSS, restic documentation
- Preparing a new repository: S3-compatible storage, restic documentation
- cmd_check.go at v0.19.1 (exclusive lock), restic source
- repo/repo.go at v0.14.0 (append-only mode still allows deleting locks), rest-server source
- pg_dump, PostgreSQL 18 documentation
- systemd.timer(5): Persistent= and systemd.exec(5): StandardOutput=, man7.org
- WriteToTextfile, Prometheus Go client documentation
- Hardened Repository, Requirements and Limitations and How Immutability Works, Veeam Backup & Replication 13 User Guide
- How Immutability Works (object storage, block generation) and Considerations and Limitations, Veeam Backup & Replication 13 User Guide
- Licensing, Veeam Backup & Replication 13 User Guide, and Downloads (Community Edition, up to 10 workloads), Veeam
- chattr(1): the immutable attribute, man7.org
Questions
What is the difference between retention and forget in resticprofile?
Both run restic forget. The retention section runs it inside the backup command, before or after the backup, and only when before-backup or after-backup is set; in config version 1 neither is on by default. It also passes the backup source list as --path unless you override it. The forget section configures the standalone forget command and can carry its own schedule. Scheduling retention directly is deprecated in favour of forget.
How do I schedule resticprofile check?
Add schedule, schedule-permission and schedule-lock-wait to the check section of the profile, then run resticprofile schedule. Put read-data-subset in the same section so each run downloads and verifies part of the pack files rather than only the repository structure. In restic 0.19.1 check takes an exclusive lock, so run it away from the backup window and give the backup a schedule-lock-wait long enough to outlast it.
How does resticprofile schedule backups on Linux?
With systemd available, resticprofile schedule writes a service and a timer named resticprofile-backup@profile-NAME and enables them. The schedule key uses systemd calendar syntax, so systemd-analyze calendar can test it. With schedule-permission: system the units go into /etc/systemd/system and run as root. The service calls resticprofile run-schedule, which re-reads the configuration, so only a change to when or as whom a job runs needs resticprofile schedule again.
Does resticprofile work on Windows?
Yes. It schedules through Windows Task Scheduler on Windows 10 or later and uses the same configuration keys. Installing a schedule with schedule-permission: system needs an elevated prompt, and user asks for the account password. Since version 0.33.0, schedule-start-when-available runs a backup that was missed while the machine was off. Add use-fs-snapshot: true to the backup section so restic reads locked files through VSS.
How do exclude patterns work in resticprofile?
resticprofile hands exclude and exclude-file to restic, so restic pattern rules apply. A pattern without a leading slash matches a complete path component anywhere, so node_modules drops every directory of that name, while a leading slash anchors the pattern at the root. A relative exclude-file path resolves against the directory holding the configuration file. A path listed directly in source is always saved, but excludes still filter what is inside it, so a source directory whose own name matches an exclude is saved empty.
Is an append-only restic key as safe as Veeam immutable backups?
No. An append-only key is access control. The backed-up server cannot delete its history, but whoever holds the admin key or the storage account can, and the server key can still overwrite objects, so recovery depends on bucket versioning. Veeam hardened repositories and Veeam object-lock repositories make backup files undeletable for a set period, even to a compromised backup server. The restic split is cheaper and simpler, but it does not make backups immutable.
Every Wardio-managed server backs up with resticprofile to storage in a different failure domain, with scheduled integrity checks. See pricing.