Simple Restic Backups on Nixos
When it comes to all the things that nix lets you do, defining complex services deterministically in text, is probably the most powerful. For example I have nix manage all of my system backup jobs. Which could be handled with a shell script, but can be so much more extensible when configured entirely within nix. I hope showing off how I manage this can help you further understand what you can do with nix, especially with configuring services.
This post will go over how I use nix to manage restic, which is my preferred backup system. Nix supports other programs, which you can lookup at https://search.nixos.org/options or https://nixos.org/manual/nixos/stable/options to see there configuration options.
The Important Config Options
There are (at time of writing) 22 configuration options for the restic backup service in nix. Though you don't need to set all of them to get restic working. I'll go over the most important ones that need to be set and what they mean.
what's <name>
The restic service in nix is setup to have separate configuration options for
each backup job that you set. <name>
is the placeholder that the documentation
uses, and can be changed to whatever you think best describes that backup. For example.
services.restic.backups.my-remote-backup = {
thing = True;
}; # Settings aren't shared between each backup
services.restic.backups.my-other-remote-backup = {
thing = False;
}; They can be set diffrently
services.restic.backups.my-local-backup = {
other-things = 1;
};
Each of these would be a separate systemd service with its own wrapper and timer1. This is quite handy when managing multiple backups which each need to be configured differently.
paths
and exclude
paths is a simple list of strings that represent the paths that you want restic to backup. While exclude will tell restic what you don't want included in the final backup. Useful for caches, a steam library and anything you wouldn't want in the final snapshot.
services.restic.backups.<name>.paths = [
"/home/" # backup all user home directories
];
services.restic.backups.<name>.exclude = [
"/home/*/.cache"
"/home/*/somereallybigfile/youdon'tneed/backedup"
];
initialize
This is a boolean that when set to true will allow the restic service to initialize repositories. As by default the backup job will fail if it does not find a preexisting repository at the set URL, which will require you to setup each repository manually before this service will work.
I like letting restic deal with initialization on its own, and I would recommend this to you as well.
services.restic.backups.<name>.initialize = True;
pruneOpts
This one doesn't absolutely need to be set but I recommend some pruning of your repositories just to clean up older and less important snapshots. It's simple to define as well, just pass each prune options as a string in a list. The example below is just what I use, you are free to change things around to suit your needs.
services.restic.backups.<name>.pruneOpts = [
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 10"
];
timerConfig
Having your backups occur regularly is extremely important, and timerconfig
is
what defines the systemd unit that triggers the backup you defined. The options
are the standard system.timer(5)
that systemd uses. So setting a regular
backup is as simple as defining OnCalendar
to "daily"/"weekly" or whatever
else you prefer. Persistent
Should also be set to true so your machine can
catch up on backups That it's missed while powered off.
Important note, if the computer uses wifi to connect to the internet (e.g
laptop). The backup will fail at boot due to the wifi not being connected at
that point. It'll work normally after it connects, or if it had Ethernet at
boot. The simplest workaround I've found is setting OnBootSec
to a minute or 2
so I can log in. After which the wifi connection will be working, and the
backup will occur just fine.
services.restic.backups.<name>.timerConfig = {
# OnBootSec = "3m"; # uncomment this this line if your on wifi
OnCalendar = "daily";
Persistent = true;
};
passwordFile
If you encrypt your backup (which you should) this is where you would tell nix where the password would be stored. I would recommend using sops-nix when it comes to managing secrets. Though you could also just hardcore a path and place a file there manually if you don't want to setup sops.
services.restic.backups.<name>.passwordFile = config.sops.secrets.restic-passphrase.path;
# or
services.restic.backups.<name>.passwordFile = "/home/user/.restic-password.txt";
repoFile
/ environmentFile
The repoFile
defines the URL2 that the repository is located at. While the
environmentFile
defines the environment variables3 needed to access the
repo. These would be the best way to define repo locations, though it is possible to
use rcloneConfigFile
if you would prefer to use that back end.
services.restic.backups.<name>.environmentFile = config.sops.templates."environmentFile".path;
services.restic.backups.<name>.repositoryFile = config.sops.templates."repositoryFile".path;
again you could just make these files and then hard code the paths, but again sops-nix lets you create files that contain your secrets with templates. So I really would recommend it or a similar system when encoding secrets in a git repo.
# Example sops.templates
sops.templates."repositoryfile".content = ''
s3:s3.example.com/${config.sops.placeholder."bucket"}
'';
sops.templates."accessfile".content = ''
AWS_ACCESS_KEY_ID="${config.sops.placeholder."keyid"}"
AWS_SECRET_ACCESS_KEY="${config.sops.placeholder."accesskey"}"
'';
My Setup
This is my personal setup from my own flake, which is meant to be imported into
your system config after which you enable the option (e.g restic.enable
= true
). You can use this as a base for your own backup setting, or
a template to what you will implement in your own system config.
# SPDX-FileCopyrightText: 2024 Imran Mustafa <imran@imranmustafa.net>
#
# SPDX-License-Identifier: GPL-3.0-or-later
{
pkgs,
lib,
config,
... # these are the funciton package imports
}: {
options = {
restic.enable = lib.mkEnableOption "restic"; # making this easy to toggle with an option
};
config = lib.mkIf config.restic.enable {
services.restic = {
backups = let
initialize = true;
exclude = [
"/home/*/.cache"
];
paths = [
"/home"
];
checkOpts = [
"--with-cache" # just to make checks faster
];
extraBackupArgs = [
];
extraOptions = [
];
passwordFile = config.sops.secrets.restic-passphrase.path;
pruneOpts = [
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 10"
];
timerConfig = {
OnBootSec = "3m";
OnCalendar = "daily";
Persistent = true;
};
in {
backup = {
inherit initialize exclude paths timerConfig checkOpts extraBackupArgs extraOptions passwordFile pruneOpts;
# using inherit lets you share common options between backups and make everything cleaner
environmentFile = config.sops.templates."environmentFile".path;
repositoryFile = config.sops.templates."repositoryFile".path;
};
};
};
};
}
One thing that you might not get is the let statement that comes after
backups
. It is simply there to define common options that I would normally
want all my backups to share. This makes it easy to share variables between
multiple backups without having to redefine the same values for each. For
example if I wanted to set up another backup job to another location I could
just define this.
other = {
inherit initialize exclude paths timerConfig checkOpts extraBackupArgs extraOptions passwordFile pruneOpts;
environmentFile = config.sops.templates."other-environmentFile".path;
repositoryFile = config.sops.templates."other-repositoryFile".path;
};
Another neat thing that nix does is it creates a wrapper for each of the backups
you setup (I.e restic-<name>
) that already have the environment values
set. Making it simple to interact with the repos.
This should be enough to get you going on the path to writing your own backup service for your Nixos systems. As well as showing off the ways that nix can be more extensible while being simpler to implement then more conventional methods of implementing a automated backup.
If set.
URL = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
Formatted in accordance with systemd.exec(5)