borgbackup: use backup and databasebackup contracts

This commit is contained in:
ibizaman 2025-10-31 11:58:09 +01:00 committed by Pierre Penninckx
parent 08b0b52cfe
commit e74e71ccef
12 changed files with 1502 additions and 331 deletions

View file

@ -8,6 +8,8 @@ others.
Not all blocks are documented yet.
You can find all available blocks [in the repository](@REPO@/modules/blocks).
## Authentication {#blocks-category-authentication}
```{=include=} chapters html:into-file=//blocks-authelia.html
modules/blocks/authelia/docs/default.md
```
@ -16,26 +18,40 @@ modules/blocks/authelia/docs/default.md
modules/blocks/lldap/docs/default.md
```
```{=include=} chapters html:into-file=//blocks-mitmdump.html
modules/blocks/mitmdump/docs/default.md
```
## Backup {#blocks-category-backup}
```{=include=} chapters html:into-file=//blocks-monitoring.html
modules/blocks/monitoring/docs/default.md
```
```{=include=} chapters html:into-file=//blocks-postgresql.html
modules/blocks/postgresql/docs/default.md
```
```{=include=} chapters html:into-file=//blocks-sops.html
modules/blocks/sops/docs/default.md
```
```{=include=} chapters html:into-file=//blocks-ssl.html
modules/blocks/ssl/docs/default.md
```{=include=} chapters html:into-file=//blocks-borgbackup.html
modules/blocks/borgbackup/docs/default.md
```
```{=include=} chapters html:into-file=//blocks-restic.html
modules/blocks/restic/docs/default.md
```
## Database {#blocks-category-database}
```{=include=} chapters html:into-file=//blocks-postgresql.html
modules/blocks/postgresql/docs/default.md
```
## Secrets {#blocks-category-secrets}
```{=include=} chapters html:into-file=//blocks-sops.html
modules/blocks/sops/docs/default.md
```
## Network {#blocks-category-network}
```{=include=} chapters html:into-file=//blocks-ssl.html
modules/blocks/ssl/docs/default.md
```
## Introspection {#blocks-category-introspection}
```{=include=} chapters html:into-file=//blocks-monitoring.html
modules/blocks/monitoring/docs/default.md
```
```{=include=} chapters html:into-file=//blocks-mitmdump.html
modules/blocks/mitmdump/docs/default.md
```

View file

@ -326,6 +326,255 @@
"blocks-authelia-troubleshooting": [
"blocks-authelia.html#blocks-authelia-troubleshooting"
],
"blocks-borgbackup": [
"blocks-borgbackup.html#blocks-borgbackup"
],
"blocks-borgbackup-contract-provider": [
"blocks-borgbackup.html#blocks-borgbackup-contract-provider"
],
"blocks-borgbackup-maintenance": [
"blocks-borgbackup.html#blocks-borgbackup-maintenance"
],
"blocks-borgbackup-maintenance-troubleshooting": [
"blocks-borgbackup.html#blocks-borgbackup-maintenance-troubleshooting"
],
"blocks-borgbackup-monitoring": [
"blocks-borgbackup.html#blocks-borgbackup-monitoring"
],
"blocks-borgbackup-options": [
"blocks-borgbackup.html#blocks-borgbackup-options"
],
"blocks-borgbackup-options-shb.borgbackup.borgServer": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.borgServer"
],
"blocks-borgbackup-options-shb.borgbackup.databases": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.request": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.request"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.request.backupCmd": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.request.backupCmd"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.request.backupName": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.request.backupName"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.request.restoreCmd": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.request.restoreCmd"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.request.user": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.request.user"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.result": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.result"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.result.backupService": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.result.backupService"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.result.restoreScript": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.result.restoreScript"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.consistency": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.consistency"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.enable": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.enable"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.limitUploadKiBs": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.limitUploadKiBs"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request.group": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request.group"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request.mode": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request.mode"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request.owner": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request.owner"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request.restartUnits": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.request.restartUnits"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.result": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.result"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.result.path": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.passphrase.result.path"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.path": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.path"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.secrets": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.secrets"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.secrets._name_.source": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.secrets._name_.source"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.secrets._name_.transform": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.secrets._name_.transform"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.timerConfig": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.repository.timerConfig"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.retention": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.retention"
],
"blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.stateDir": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.databases._name_.settings.stateDir"
],
"blocks-borgbackup-options-shb.borgbackup.instances": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.request": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.request"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.request.excludePatterns": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.request.excludePatterns"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.request.hooks": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.request.hooks"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.request.hooks.afterBackup": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.request.hooks.afterBackup"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.request.hooks.beforeBackup": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.request.hooks.beforeBackup"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.request.sourceDirectories": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.request.sourceDirectories"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.request.user": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.request.user"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.result": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.result"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.result.backupService": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.result.backupService"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.result.restoreScript": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.result.restoreScript"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.consistency": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.consistency"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.enable": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.enable"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.limitUploadKiBs": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.limitUploadKiBs"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request.group": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request.group"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request.mode": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request.mode"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request.owner": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request.owner"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request.restartUnits": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.request.restartUnits"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.result": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.result"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.result.path": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.passphrase.result.path"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.path": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.path"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.secrets": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.secrets"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.secrets._name_.source": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.secrets._name_.source"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.secrets._name_.transform": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.secrets._name_.transform"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.timerConfig": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.repository.timerConfig"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.retention": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.retention"
],
"blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.stateDir": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.instances._name_.settings.stateDir"
],
"blocks-borgbackup-options-shb.borgbackup.performance": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.performance"
],
"blocks-borgbackup-options-shb.borgbackup.performance.ioPriority": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.performance.ioPriority"
],
"blocks-borgbackup-options-shb.borgbackup.performance.ioSchedulingClass": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.performance.ioSchedulingClass"
],
"blocks-borgbackup-options-shb.borgbackup.performance.niceness": [
"blocks-borgbackup.html#blocks-borgbackup-options-shb.borgbackup.performance.niceness"
],
"blocks-borgbackup-tests": [
"blocks-borgbackup.html#blocks-borgbackup-tests"
],
"blocks-borgbackup-usage": [
"blocks-borgbackup.html#blocks-borgbackup-usage"
],
"blocks-borgbackup-usage-multiple": [
"blocks-borgbackup.html#blocks-borgbackup-usage-multiple"
],
"blocks-borgbackup-usage-provider-contract": [
"blocks-borgbackup.html#blocks-borgbackup-usage-provider-contract"
],
"blocks-borgbackup-usage-provider-manual": [
"blocks-borgbackup.html#blocks-borgbackup-usage-provider-manual"
],
"blocks-borgbackup-usage-provider-remote": [
"blocks-borgbackup.html#blocks-borgbackup-usage-provider-remote"
],
"blocks-category-authentication": [
"blocks.html#blocks-category-authentication"
],
"blocks-category-backup": [
"blocks.html#blocks-category-backup"
],
"blocks-category-database": [
"blocks.html#blocks-category-database"
],
"blocks-category-introspection": [
"blocks.html#blocks-category-introspection"
],
"blocks-category-network": [
"blocks.html#blocks-category-network"
],
"blocks-category-secrets": [
"blocks.html#blocks-category-secrets"
],
"blocks-lldap": [
"blocks-lldap.html#blocks-lldap"
],
@ -860,9 +1109,6 @@
"blocks-restic-contract-provider": [
"blocks-restic.html#blocks-restic-contract-provider"
],
"blocks-restic-demo": [
"blocks-restic.html#blocks-restic-demo"
],
"blocks-restic-maintenance": [
"blocks-restic.html#blocks-restic-maintenance"
],
@ -2216,6 +2462,24 @@
"services-arr-options-shb.arr.sonarr.subdomain": [
"services-arr.html#services-arr-options-shb.arr.sonarr.subdomain"
],
"services-category-ai": [
"services.html#services-category-ai"
],
"services-category-automation": [
"services.html#services-category-automation"
],
"services-category-code": [
"services.html#services-category-code"
],
"services-category-documents": [
"services.html#services-category-documents"
],
"services-category-media": [
"services.html#services-category-media"
],
"services-category-passwords": [
"services.html#services-category-passwords"
],
"services-forgejo": [
"services-forgejo.html#services-forgejo"
],

View file

@ -1,8 +1,10 @@
<!-- Read these docs at https://shb.skarabox.com -->
# Services {#services}
Services are usually web applications that SHB help you self-host. Configuration of those is
purposely made opinionated and require as few nix options as can make sense. That is possible thanks to the extensive use of blocks provided by SHB.
Services are usually web applications that SHB help you self-host.
Configuration of those is purposely made more opinionated than the upstream nixpkgs modules
in exchange for requiring less options to define.
That is possible thanks to the extensive use of blocks provided by SHB.
::: {.note}
Not all services are yet documented. You can find all available services [in the repository](@REPO@/modules/services).
@ -42,38 +44,50 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes
[Pinchflat]: services-pinchflat.html
[Vaultwarden]: services-vaultwarden.html
```{=include=} chapters html:into-file=//services-arr.html
modules/services/arr/docs/default.md
```
```{=include=} chapters html:into-file=//services-forgejo.html
modules/services/forgejo/docs/default.md
```
```{=include=} chapters html:into-file=//services-home-assistant.html
modules/services/home-assistant/docs/default.md
```
```{=include=} chapters html:into-file=//services-jellyfin.html
modules/services/jellyfin/docs/default.md
```
```{=include=} chapters html:into-file=//services-karakeep.html
modules/services/karakeep/docs/default.md
```
## Documents {#services-category-documents}
```{=include=} chapters html:into-file=//services-nextcloud.html
modules/services/nextcloud-server/docs/default.md
```
```{=include=} chapters html:into-file=//services-pinchflat.html
modules/services/pinchflat/docs/default.md
## Passwords {#services-category-passwords}
```{=include=} chapters html:into-file=//services-vaultwarden.html
modules/services/vaultwarden/docs/default.md
```
## Automation {#services-category-automation}
```{=include=} chapters html:into-file=//services-home-assistant.html
modules/services/home-assistant/docs/default.md
```
## AI {#services-category-ai}
```{=include=} chapters html:into-file=//services-karakeep.html
modules/services/karakeep/docs/default.md
```
```{=include=} chapters html:into-file=//services-open-webui.html
modules/services/open-webui/docs/default.md
```
```{=include=} chapters html:into-file=//services-vaultwarden.html
modules/services/vaultwarden/docs/default.md
## Code {#services-category-code}
```{=include=} chapters html:into-file=//services-forgejo.html
modules/services/forgejo/docs/default.md
```
## Media {#services-category-media}
```{=include=} chapters html:into-file=//services-arr.html
modules/services/arr/docs/default.md
```
```{=include=} chapters html:into-file=//services-jellyfin.html
modules/services/jellyfin/docs/default.md
```
```{=include=} chapters html:into-file=//services-pinchflat.html
modules/services/pinchflat/docs/default.md
```

View file

@ -27,6 +27,7 @@
shbPatches = originPkgs.lib.optionals (system == "x86_64-linux") [
# Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged.
./patches/lldap.patch
./patches/0001-nixos-borgbackup-add-option-to-override-state-direct.patch
# Leaving commented out as an example.
# (originPkgs.fetchpatch {
@ -102,6 +103,7 @@
];
modules = {
"blocks/authelia" = ./modules/blocks/authelia.nix;
"blocks/borgbackup" = ./modules/blocks/borgbackup.nix;
"blocks/lldap" = ./modules/blocks/lldap.nix;
"blocks/ssl" = {
module = ./modules/blocks/ssl.nix;
@ -294,6 +296,7 @@
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
// (vm_test "authelia" ./test/blocks/authelia.nix)
// (vm_test "borgbackup" ./test/blocks/borgbackup.nix)
// (vm_test "lldap" ./test/blocks/lldap.nix)
// (vm_test "lib" ./test/blocks/lib.nix)
// (vm_test "mitmdump" ./test/blocks/mitmdump.nix)
@ -368,6 +371,7 @@
imports = [
# blocks
self.nixosModules.authelia
self.nixosModules.borgbackup
self.nixosModules.davfs
self.nixosModules.hardcodedsecret
self.nixosModules.lldap
@ -400,6 +404,7 @@
};
nixosModules.authelia = modules/blocks/authelia.nix;
nixosModules.borgbackup = modules/blocks/borgbackup.nix;
nixosModules.davfs = modules/blocks/davfs.nix;
nixosModules.hardcodedsecret = modules/blocks/hardcodedsecret.nix;
nixosModules.lldap = modules/blocks/lldap.nix;

View file

@ -9,54 +9,92 @@
let
cfg = config.shb.borgbackup;
instanceOptions = {
enable = lib.mkEnableOption "shb borgbackup";
contracts = pkgs.callPackage ../contracts { };
keySopsFile = lib.mkOption {
description = "Sops file that holds this instance's repository key and passphrase.";
type = lib.types.path;
example = "secrets/backup.yaml";
};
inherit (lib)
concatStringsSep
filterAttrs
flatten
literalExpression
optionals
listToAttrs
mapAttrsToList
mkOption
mkMerge
;
inherit (lib)
mkIf
nameValuePair
optionalAttrs
removePrefix
;
inherit (lib.types)
attrsOf
int
oneOf
nonEmptyStr
nullOr
str
submodule
;
encryptionKeyFile = lib.mkOption {
description = "Encryption key for the backup.";
type = lib.types.path;
};
commonOptions =
{
name,
prefix,
config,
...
}:
{
enable = lib.mkEnableOption ''
SelfHostBlocks' BorgBackup block;
encryption_passcommand = "cat /run/secrets/borgmatic/passphrases/${
if isNull instance.secretName then name else instance.secretName
}";
borg_keys_directory = "/run/secrets/borgmatic/keys";
A disabled instance will not backup data anymore
but still provides the helper tool to restore snapshots
'';
sourceDirectories = lib.mkOption {
description = "Source directories.";
type = lib.types.nonEmptyListOf lib.types.str;
};
passphrase = lib.mkOption {
description = "Encryption key for the backup repository.";
type = lib.types.submodule {
options = contracts.secret.mkRequester {
mode = "0400";
owner = config.request.user;
ownerText = "[shb.borgbackup.${prefix}.<name>.request.user](#blocks-borgbackup-options-shb.borgbackup.${prefix}._name_.request.user)";
restartUnits = [ (fullName name config.settings.repository) ];
restartUnitsText = "[ [shb.borgbackup.${prefix}.<name>.settings.repository](#blocks-borgbackup-options-shb.borgbackup.${prefix}._name_.settings.repository) ]";
};
};
};
excludePatterns = lib.mkOption {
description = "Exclude patterns.";
type = lib.types.listOf lib.types.str;
default = [ ];
};
secretName = lib.mkOption {
description = "Secret name, if null use the name of the backup instance.";
type = lib.types.nullOr lib.types.str;
default = null;
};
repositories = lib.mkOption {
description = "Repositories to back this instance to.";
type = lib.types.nonEmptyListOf (
lib.types.submodule {
repository = lib.mkOption {
description = "Repository to send the backups to.";
type = submodule {
options = {
path = lib.mkOption {
type = lib.types.str;
path = mkOption {
type = str;
description = "Repository location";
};
timerConfig = lib.mkOption {
type = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption;
secrets = mkOption {
type = attrsOf lib.shb.secretFileType;
default = { };
description = ''
Secrets needed to access the repository where the backups will be stored.
See [s3 config](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#amazon-s3) for an example
and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets.
'';
example = literalExpression ''
{
AWS_ACCESS_KEY_ID.source = <path/to/secret>;
AWS_SECRET_ACCESS_KEY.source = <path/to/secret>;
}
'';
};
timerConfig = mkOption {
type = attrsOf utils.systemdUtils.unitOptions.unitOption;
default = {
OnCalendar = "daily";
Persistent = true;
@ -69,95 +107,127 @@ let
};
};
};
}
);
};
retention = lib.mkOption {
description = "Retention options.";
type = lib.types.attrsOf (
lib.types.oneOf [
lib.types.int
lib.types.nonEmptyStr
]
);
default = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
};
};
consistency = lib.mkOption {
description = "Consistency frequency options.";
type = lib.types.attrsOf lib.types.nonEmptyStr;
default = { };
example = {
repository = "2 weeks";
archives = "1 month";
};
};
hooks = lib.mkOption {
description = "Hooks to run before or after the backup.";
default = { };
type = lib.types.submodule {
options = {
beforeBackup = lib.mkOption {
description = "Hooks to run before backup";
type = lib.types.listOf lib.types.str;
default = [ ];
};
afterBackup = lib.mkOption {
description = "Hooks to run after backup";
type = lib.types.listOf lib.types.str;
default = [ ];
};
};
};
retention = lib.mkOption {
description = "Retention options. See {command}`borg help prune` for the available options.";
type = attrsOf (oneOf [
int
nonEmptyStr
]);
default = {
within = "1d";
hourly = 24;
daily = 7;
weekly = 4;
monthly = 6;
};
};
consistency = lib.mkOption {
description = "Consistency frequency options.";
type = lib.types.attrsOf lib.types.nonEmptyStr;
default = { };
example = {
repository = "2 weeks";
archives = "1 month";
};
};
limitUploadKiBs = mkOption {
type = nullOr int;
description = "Limit upload bandwidth to the given KiB/s amount.";
default = null;
example = 8000;
};
stateDir = mkOption {
type = nullOr lib.types.str;
description = ''
Override the directory in which {command}`borg` stores its
configuration and cache. By default it uses the user's
home directory but is some cases this can cause conflicts.
'';
default = null;
};
};
environmentFile = lib.mkOption {
type = lib.types.bool;
description = "Add environment file to be read by the systemd service.";
default = false;
example = true;
};
};
repoSlugName =
name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (lib.strings.removePrefix "/" name);
repoSlugName = name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (removePrefix "/" name);
fullName = name: repository: "borgbackup-job-${name}_${repoSlugName repository.path}";
in
{
options.shb.borgbackup = {
user = lib.mkOption {
description = "Unix user doing the backups.";
type = lib.types.str;
default = "backup";
};
group = lib.mkOption {
description = "Unix group doing the backups.";
type = lib.types.str;
default = "backup";
};
instances = lib.mkOption {
description = "Each instance is a backup setting";
instances = mkOption {
description = "Files to backup following the [backup contract](./contracts-backup.html).";
default = { };
type = lib.types.attrsOf (
lib.types.submodule {
options = instanceOptions;
}
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = contracts.backup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the BorgBackup provider.
'';
type = submodule {
options = commonOptions {
inherit name config;
prefix = "instances";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}
)
);
};
databases = mkOption {
description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html).";
default = { };
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = contracts.databasebackup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the BorgBackup provider.
'';
type = submodule {
options = commonOptions {
inherit name config;
prefix = "databases";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}
)
);
};
borgServer = lib.mkOption {
description = "Add borgbackup package so external backups can use this server as a remote.";
description = "Add borgbackup package to `environment.systemPackages` so external backups can use this server as a remote.";
default = false;
example = true;
type = lib.types.bool;
@ -193,153 +263,262 @@ in
};
};
config = lib.mkIf (cfg.instances != { }) (
config = lib.mkIf (cfg.instances != { } || cfg.databases != { }) (
let
enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances;
enabledInstances = filterAttrs (k: i: i.settings.enable) cfg.instances;
enabledDatabases = filterAttrs (k: i: i.settings.enable) cfg.databases;
in
lib.mkMerge [
# Secrets configuration
{
users.users = {
${cfg.user} = {
name = cfg.user;
group = cfg.group;
home = "/var/lib/backup";
createHome = true;
isSystemUser = true;
extraGroups = [ "keys" ];
};
};
users.groups = {
${cfg.group} = {
name = cfg.group;
};
};
sops.secrets =
let
mkSopsSecret =
name: instance:
(
[
{
"${instance.backend}/passphrases/${
if isNull instance.secretName then name else instance.secretName
}" =
{
sopsFile = instance.keySopsFile;
mode = "0440";
owner = cfg.user;
group = cfg.group;
};
}
]
++
lib.optional
((lib.filter ({ path, ... }: lib.strings.hasPrefix "s3" path) instance.repositories) != [ ])
{
"${instance.backend}/environmentfiles/${
if isNull instance.secretName then name else instance.secretName
}" =
{
sopsFile = instance.keySopsFile;
mode = "0440";
owner = cfg.user;
group = cfg.group;
};
}
++ lib.optionals (instance.backend == "borgmatic") (
lib.flatten (
map (
{ path, ... }:
{
"${instance.backend}/keys/${repoSlugName path}" = {
key = "${instance.backend}/keys/${
if isNull instance.secretName then name else instance.secretName
}";
sopsFile = instance.keySopsFile;
mode = "0440";
owner = cfg.user;
group = cfg.group;
};
}
) instance.repositories
)
)
);
in
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSopsSecret enabledInstances));
}
# Borgmatic configuration
{
systemd.timers.borgmatic = lib.mkIf (enabledInstances != { }) {
timerConfig = {
OnCalendar = "hourly";
};
};
systemd.services.borgmatic = lib.mkIf (enabledInstances != { }) {
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStartPre = [ "" ]; # Do not sleep before starting.
ExecStart = [
""
"${pkgs.borgmatic}/bin/borgmatic --verbosity -1 --syslog-verbosity 1"
environment.systemPackages =
optionals (cfg.borgServer || enabledInstances != { } || enabledDatabases != { })
[
pkgs.borgbackup
];
# For borgmatic, since we have only one service, we need to merge all environmentFile
# from all instances.
EnvironmentFile = lib.mapAttrsToList (name: value: value.environmentFile) enabledInstances;
};
};
systemd.packages = lib.mkIf (enabledInstances != { }) [ pkgs.borgmatic ];
environment.systemPackages = (
lib.optionals cfg.borgServer [ pkgs.borgbackup ]
++ lib.optionals (enabledInstances != { }) [
pkgs.borgbackup
pkgs.borgmatic
]
);
environment.etc =
}
{
services.borgbackup.jobs =
let
mkSettings = name: instance: {
"borgmatic.d/${name}.yaml".text = lib.generators.toYAML { } {
location = {
source_directories = instance.sourceDirectories;
repositories = map ({ path, ... }: path) instance.repositories;
}
// (lib.attrsets.optionalAttrs (builtins.length instance.excludePatterns > 0) {
excludePatterns = instance.excludePatterns;
});
mkJob = name: instance: {
"${name}_${repoSlugName instance.settings.repository.path}" = {
inherit (instance.request) user;
storage = {
encryption_passcommand = "cat ${instance.encryptionKeyFile}";
borg_keys_directory = "/run/secrets/borgmatic/keys";
};
repo = instance.settings.repository.path;
retention = instance.retention;
consistency.checks =
let
mkCheck = name: frequency: {
inherit name frequency;
};
in
lib.attrsets.mapAttrsToList mkCheck instance.consistency;
paths = instance.request.sourceDirectories;
# hooks = lib.mkMerge [
# lib.optionalAttrs (builtins.length instance.hooks.beforeBackup > 0) {
# inherit (instance.hooks) beforeBackup;
# }
# lib.optionalAttrs (builtins.length instance.hooks.afterBackup > 0) {
# inherit (instance.hooks) afterBackup;
# }
# ];
encryption.mode = "repokey-blake2";
# We do not set encryption.passphrase here, we set BORG_PASSPHRASE_FD further down.
encryption.passCommand = "cat ${instance.settings.passphrase.result.path}";
doInit = true;
failOnWarnings = true;
stateDir = instance.settings.stateDir;
persistentTimer = instance.settings.repository.timerConfig.Persistent or false;
startAt = ""; # Some non-empty string value tricks the upstream module in creating the systemd timer.
prune.keep = instance.settings.retention;
preHook = concatStringsSep "\n" instance.request.hooks.beforeBackup;
postHook = concatStringsSep "\n" instance.request.hooks.afterBackup;
extraArgs = (
optionals (instance.settings.limitUploadKiBs != null) [
"--upload-ratelimit=${toString instance.settings.limitUploadKiBs}"
]
);
exclude = instance.request.excludePatterns;
};
};
in
lib.mkMerge (lib.attrsets.mapAttrsToList mkSettings enabledInstances);
mkMerge (mapAttrsToList mkJob enabledInstances);
}
{
services.borgbackup.jobs =
let
mkJob = name: instance: {
"${name}_${repoSlugName instance.settings.repository.path}" = {
inherit (instance.request) user;
repo = instance.settings.repository.path;
dumpCommand = lib.getExe (pkgs.writeShellApplication {
name = "dump-command";
text = instance.request.backupCmd;
});
encryption.mode = "repokey-blake2";
# We do not set encryption.passphrase here, we set BORG_PASSPHRASE_FD further down.
encryption.passCommand = "cat ${instance.settings.passphrase.result.path}";
doInit = true;
failOnWarnings = true;
stateDir = instance.settings.stateDir;
persistentTimer = instance.settings.repository.timerConfig.Persistent or false;
startAt = ""; # Some non-empty list value that tricks upstream in creating the systemd timer.
prune.keep = instance.settings.retention;
extraArgs = (
optionals (instance.settings.limitUploadKiBs != null) [
"--upload-ratelimit=${toString instance.settings.limitUploadKiBs}"
]
);
};
};
in
mkMerge (mapAttrsToList mkJob enabledDatabases);
}
{
systemd.timers =
let
mkTimer = name: instance: {
${fullName name instance.settings.repository} = {
timerConfig = lib.mkForce instance.settings.repository.timerConfig;
};
};
in
mkMerge (mapAttrsToList mkTimer (enabledInstances // enabledDatabases));
}
{
systemd.services =
let
mkSettings =
name: instance:
let
serviceName = fullName name instance.settings.repository;
in
{
${serviceName} = mkMerge [
{
serviceConfig = {
# Makes the systemd service wait for the backup to be done before changing state to inactive.
Type = "oneshot";
Nice = lib.mkForce cfg.performance.niceness;
IOSchedulingClass = lib.mkForce cfg.performance.ioSchedulingClass;
IOSchedulingPriority = lib.mkForce cfg.performance.ioPriority;
# BindReadOnlyPaths = instance.sourceDirectories;
};
}
(optionalAttrs (instance.settings.repository.secrets != { }) {
serviceConfig.EnvironmentFile = [
"/run/secrets_borgbackup/${serviceName}"
];
after = [ "${serviceName}-pre.service" ];
requires = [ "${serviceName}-pre.service" ];
})
];
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != { }) (
let
script = lib.shb.genConfigOutOfBandSystemd {
config = instance.settings.repository.secrets;
configLocation = "/run/secrets_borgbackup/${serviceName}";
generator = lib.shb.toEnvVar;
user = instance.request.user;
};
in
{
script = script.preStart;
serviceConfig.Type = "oneshot";
serviceConfig.LoadCredential = script.loadCredentials;
}
);
};
in
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
}
{
systemd.services =
let
mkEnv =
name: instance:
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
enable = true;
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = (
lib.shb.replaceSecrets {
userConfig = instance.settings.repository.secrets // {
BORG_PASSCOMMAND = ''"cat ${instance.settings.passphrase.result.path}"'';
BORG_REPO = instance.settings.repository.path;
};
resultPath = "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}";
generator = lib.shb.toEnvVar;
user = instance.request.user;
}
);
};
in
listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases)));
}
{
environment.systemPackages =
let
mkBorgBackupBinary =
name: instance:
pkgs.writeShellApplication {
name = fullName name instance.settings.repository;
text = ''
usage() {
echo "$0 restore latest"
}
if ! [ "$1" = "restore" ]; then
usage
exit 1
fi
shift
if ! [ "$1" = "latest" ]; then
usage
exit 1
fi
shift
sudocmd() {
sudo --preserve-env=BORG_REPO,BORG_PASSCOMMAND -u ${instance.request.user} "$@"
}
set -a
# shellcheck disable=SC1090
source <(sudocmd cat "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}")
set +a
archive="$(sudocmd borg list --short "$BORG_REPO" | tail -n 1)"
echo "Will restore archive $archive"
(cd / && sudocmd ${pkgs.borgbackup}/bin/borg extract "$BORG_REPO"::"$archive")
'';
};
in
flatten (mapAttrsToList mkBorgBackupBinary cfg.instances);
}
{
environment.systemPackages =
let
mkBorgBackupBinary =
name: instance:
pkgs.writeShellApplication {
name = fullName name instance.settings.repository;
text = ''
usage() {
echo "$0 restore latest"
}
if ! [ "$1" = "restore" ]; then
usage
exit 1
fi
shift
if ! [ "$1" = "latest" ]; then
usage
exit 1
fi
shift
sudocmd() {
sudo --preserve-env=BORG_REPO,BORG_PASSCOMMAND -u ${instance.request.user} "$@"
}
set -a
# shellcheck disable=SC1090
source <(sudocmd cat "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}")
set +a
archive="$(sudocmd borg list --short "$BORG_REPO" | tail -n 1)"
echo "Will restore archive $archive"
sudocmd sh -c "${pkgs.borgbackup}/bin/borg extract $BORG_REPO::$archive --stdout | ${instance.request.restoreCmd}"
'';
};
in
flatten (mapAttrsToList mkBorgBackupBinary cfg.databases);
}
]
);

View file

@ -0,0 +1,270 @@
# Borgbackup Block {#blocks-borgbackup}
Defined in [`/modules/blocks/borgbackup.nix`](@REPO@/modules/blocks/borgbackup.nix).
This block sets up a backup job using [BorgBackup][].
[borgbackup]: https://www.borgbackup.org/
## Provider Contracts {#blocks-borgbackup-contract-provider}
This block provides the following contracts:
- [backup contract](contracts-backup.html) under the [`shb.borgbackup.instances`][instances] option.
It is tested with [contract tests][backup contract tests].
- [database backup contract](contracts-databasebackup.html) under the [`shb.borgbackup.databases`][databases] option.
It is tested with [contract tests][database backup contract tests].
[instances]: #blocks-borgbackup-options-shb.borgbackup.instances
[databases]: #blocks-borgbackup-options-shb.borgbackup.databases
[backup contract tests]: @REPO@/test/contracts/backup.nix
[database backup contract tests]: @REPO@/test/contracts/databasebackup.nix
As requested by those two contracts, when setting up a backup with BorgBackup,
a backup Systemd service and a [restore script](#blocks-borgbackup-maintenance) are provided.
## Usage {#blocks-borgbackup-usage}
The following examples assume usage of the [sops block][] to provide secrets
although any blocks providing the [secrets contract][] works too.
[sops block]: ./blocks-sops.html
[secrets contract]: ./contracts-secrets.html
### One folder backed up manually {#blocks-borgbackup-usage-provider-manual}
The following snippet shows how to configure
the backup of 1 folder to 1 repository.
We assume that the folder `/var/lib/myfolder` of the service `myservice` must be backed up.
```nix
shb.borgbackup.instances."myservice" = {
request = {
user = "myservice";
sourceDirectories = [
"/var/lib/myfolder"
];
};
settings = {
enable = true;
passphrase.result = shb.sops.secret."passphrase".result;
repository = {
path = "/srv/backups/myservice";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
};
retention = {
within = "1d";
hourly = 24;
daily = 7;
weekly = 4;
monthly = 6;
};
};
};
shb.sops.secret."passphrase".request =
shb.borgbackup.instances."myservice".settings.passphrase.request;
```
### One folder backed up with contract {#blocks-borgbackup-usage-provider-contract}
With the same example as before but assuming the `myservice` service
has a `myservice.backup` option that is a requester for the backup contract,
the snippet above becomes:
```nix
shb.borgbackup.instances."myservice" = {
request = config.myservice.backup;
settings = {
enable = true;
passphrase.result = shb.sops.secret."passphrase".result;
repository = {
path = "/srv/backups/myservice";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
};
retention = {
within = "1d";
hourly = 24;
daily = 7;
weekly = 4;
monthly = 6;
};
};
};
shb.sops.secret."passphrase".request =
shb.borgbackup.instances."myservice".settings.passphrase.request;
```
### One folder backed up to S3 {#blocks-borgbackup-usage-provider-remote}
Here we will only highlight the differences with the previous configuration.
This assumes you have access to such a remote S3 store, for example by using [Backblaze](https://www.backblaze.com/).
```diff
shb.backup.instances.myservice = {
repository = {
- path = "/srv/pool1/backups/myfolder";
+ path = "s3:s3.us-west-000.backblazeb2.com/backups/myfolder";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
+ extraSecrets = {
+ AWS_ACCESS_KEY_ID.source="<path/to/access_key_id>";
+ AWS_SECRET_ACCESS_KEY.source="<path/to/secret_access_key>";
+ };
};
}
```
### Multiple directories to multiple destinations {#blocks-borgbackup-usage-multiple}
The following snippet shows how to configure backup of any number of folders to 3 repositories,
each happening at different times to avoid I/O contention.
We will also make sure to be able to re-use as much as the configuration as possible.
A few assumptions:
- 2 hard drive pools used for backup are mounted respectively on `/srv/pool1` and `/srv/pool2`.
- You have a backblaze account.
First, let's define a variable to hold all the repositories we want to back up to:
```nix
repos = [
{
path = "/srv/pool1/backups";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "3h";
};
}
{
path = "/srv/pool2/backups";
timerConfig = {
OnCalendar = "08:00:00";
RandomizedDelaySec = "3h";
};
}
{
path = "s3:s3.us-west-000.backblazeb2.com/backups";
timerConfig = {
OnCalendar = "16:00:00";
RandomizedDelaySec = "3h";
};
}
];
```
Compared to the previous examples, we do not include the name of what we will back up in the
repository paths.
Now, let's define a function to create a backup configuration. It will take a list of repositories,
a name identifying the backup and a list of folders to back up.
```nix
backupcfg = repositories: name: sourceDirectories {
enable = true;
backend = "borgbackup";
keySopsFile = ../secrets/backup.yaml;
repositories = builtins.map (r: {
path = "${r.path}/${name}";
inherit (r) timerConfig;
}) repositories;
inherit sourceDirectories;
retention = {
within = "1d";
hourly = 24;
daily = 7;
weekly = 4;
monthly = 6;
};
environmentFile = true;
};
```
Now, we can define multiple backup jobs to backup different folders:
```nix
shb.backup.instances.myfolder1 = backupcfg repos ["/var/lib/myfolder1"];
shb.backup.instances.myfolder2 = backupcfg repos ["/var/lib/myfolder2"];
```
The difference between the above snippet and putting all the folders into one configuration (shown
below) is the former splits the backups into sub-folders on the repositories.
```nix
shb.backup.instances.all = backupcfg repos ["/var/lib/myfolder1" "/var/lib/myfolder2"];
```
## Monitoring {#blocks-borgbackup-monitoring}
[WIP](https://github.com/ibizaman/selfhostblocks/issues/151)
## Maintenance {#blocks-borgbackup-maintenance}
One command-line helper is provided per backup instance and repository pair to automatically supply the needed secrets.
The restore script has all the secrets needed to access the repo,
it will run `sudo` automatically
and the user running it needs to have correct permissions for privilege escalation
In the [multiple directories example](#blocks-borgbackup-usage-multiple) above, the following 6 helpers are provided in the `$PATH`:
```bash
borgbackup-job-myfolder1_srv_pool1_backups
borgbackup-job-myfolder1_srv_pool2_backups
borgbackup-job-myfolder1_s3_s3.us-west-000.backblazeb2.com_backups
borgbackup-job-myfolder2_srv_pool1_backups
borgbackup-job-myfolder2_srv_pool2_backups
borgbackup-job-myfolder2_s3_s3.us-west-000.backblazeb2.com_backups
```
Discovering those is easy thanks to tab-completion.
One can then restore a backup from a given repository with:
```bash
borgbackup-job-myfolder1_srv_pool1_backups restore latest
```
### Troubleshooting {#blocks-borgbackup-maintenance-troubleshooting}
In case something bad happens with a backup, the [official documentation](https://borgbackup.readthedocs.io/en/stable/077_troubleshooting.html) has a lot of tips.
## Tests {#blocks-borgbackup-tests}
Specific integration tests are defined in [`/test/blocks/borgbackup.nix`](@REPO@/test/blocks/borgbackup.nix).
## Options Reference {#blocks-borgbackup-options}
```{=include=} options
id-prefix: blocks-borgbackup-options-
list-id: selfhostblocks-block-borgbackup-options
source: @OPTIONS_JSON@
```

View file

@ -51,7 +51,7 @@ let
}:
{
enable = mkEnableOption ''
this backup intance.
SelfHostBlocks' Restic block
A disabled instance will not backup data anymore
but still provides the helper tool to restore snapshots
@ -426,19 +426,39 @@ in
let
mkResticBinary =
name: instance:
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail
pkgs.writeShellApplication {
name = fullName name instance.settings.repository;
text = ''
usage() {
echo "$0 restore latest"
}
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
| xargs -d '\n')
if ! [ "$1" = "restore" ]; then
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
else
if ! [ "$1" = "restore" ]; then
usage
exit 1
fi
shift
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic restore $@ --target /"
fi
'';
if ! [ "$1" = "latest" ]; then
usage
exit 1
fi
shift
sudocmd() {
sudo --preserve-env=RESTIC_REPOSITORY,RESTIC_PASSWORD_FILE -u ${instance.request.user} "$@"
}
set -a
# shellcheck disable=SC1090
source <(sudocmd cat "/run/secrets_restic_env/${fullName name instance.settings.repository}")
set +a
echo "Will restore archive 'latest'"
sudocmd ${pkgs.restic}/bin/restic restore latest --target /
'';
};
in
flatten (mapAttrsToList mkResticBinary cfg.instances);
}
@ -447,19 +467,39 @@ in
let
mkResticBinary =
name: instance:
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail
pkgs.writeShellApplication {
name = fullName name instance.settings.repository;
text = ''
usage() {
echo "$0 restore latest"
}
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
| xargs -d '\n')
if ! [ "$1" = "restore" ]; then
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
else
if ! [ "$1" = "restore" ]; then
usage
exit 1
fi
shift
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupName} | ${instance.request.restoreCmd}"
fi
'';
if ! [ "$1" = "latest" ]; then
usage
exit 1
fi
shift
sudocmd() {
sudo --preserve-env=RESTIC_REPOSITORY,RESTIC_PASSWORD_FILE -u ${instance.request.user} "$@"
}
set -a
# shellcheck disable=SC1090
source <(sudocmd cat "/run/secrets_restic_env/${fullName name instance.settings.repository}")
set +a
echo "Will restore archive 'latest'"
sudocmd sh -c "${pkgs.restic}/bin/restic dump latest ${instance.request.backupName} | ${instance.request.restoreCmd}"
'';
};
in
flatten (mapAttrsToList mkResticBinary cfg.databases);
}

View file

@ -6,10 +6,6 @@ This block sets up a backup job using [Restic][].
[restic]: https://restic.net/
## Tests {#blocks-restic-tests}
Specific integration tests are defined in [`/test/blocks/restic.nix`](@REPO@/test/blocks/restic.nix).
## Provider Contracts {#blocks-restic-contract-provider}
This block provides the following contracts:
@ -25,9 +21,7 @@ This block provides the following contracts:
[database backup contract tests]: @REPO@/test/contracts/databasebackup.nix
As requested by those two contracts, when setting up a backup with Restic,
a backup Systemd service and restore script are provided.
The restore script has all the secrets needed to access the repo,
the only requirement to run it is to be able to `sudo` in the expected user.
a backup Systemd service and a [restore script](#blocks-restic-maintenance) are provided.
## Usage {#blocks-restic-usage}
@ -228,18 +222,18 @@ below) is the former splits the backups into sub-folders on the repositories.
shb.backup.instances.all = backupcfg repos ["/var/lib/myfolder1" "/var/lib/myfolder2"];
```
## Demo {#blocks-restic-demo}
[WIP]
## Monitoring {#blocks-restic-monitoring}
[WIP]
[WIP](https://github.com/ibizaman/selfhostblocks/issues/151)
## Maintenance {#blocks-restic-maintenance}
One command-line helper is provided per backup instance and repository pair to automatically supply the needed secrets.
The restore script has all the secrets needed to access the repo,
it will run `sudo` automatically
and the user running it needs to have correct permissions for privilege escalation
In the [multiple directories example](#blocks-restic-usage-multiple) above, the following 6 helpers are provided in the `$PATH`:
```bash
@ -263,6 +257,10 @@ restic-myfolder1_srv_pool1_backups restore latest
In case something bad happens with a backup, the [official documentation](https://restic.readthedocs.io/en/stable/077_troubleshooting.html) has a lot of tips.
## Tests {#blocks-restic-tests}
Specific integration tests are defined in [`/test/blocks/restic.nix`](@REPO@/test/blocks/restic.nix).
## Options Reference {#blocks-restic-options}
```{=include=} options

View file

@ -0,0 +1,78 @@
From dda895b551c7cf56476ac8892904e289a4d8b920 Mon Sep 17 00:00:00 2001
From: ibizaman <ibizaman@tiserbox.com>
Date: Sat, 1 Nov 2025 13:49:20 +0100
Subject: [PATCH] nixos/borgbackup: add option to override state directory
---
nixos/modules/services/backup/borgbackup.nix | 23 +++++++++++++++-----
1 file changed, 18 insertions(+), 5 deletions(-)
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
index adabb2ce0f8b..82baeb928398 100644
--- a/nixos/modules/services/backup/borgbackup.nix
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -136,7 +136,7 @@ let
mkBackupService =
name: cfg:
let
- userHome = config.users.users.${cfg.user}.home;
+ userHome = if cfg.stateDir != null then cfg.stateDir else config.users.users.${cfg.user}.home;
backupJobName = "borgbackup-job-${name}";
backupScript = mkBackupScript backupJobName cfg;
in
@@ -177,6 +177,7 @@ let
environment = {
BORG_REPO = cfg.repo;
}
+ // (lib.optionalAttrs (cfg.stateDir != null) { BORG_BASE_DIR = cfg.stateDir; })
// (mkPassEnv cfg)
// cfg.environment;
};
@@ -223,6 +224,7 @@ let
set = {
BORG_REPO = cfg.repo;
}
+ // (lib.optionalAttrs (cfg.stateDir != null) { BORG_BASE_DIR = cfg.stateDir; })
// (mkPassEnv cfg)
// cfg.environment;
});
@@ -232,14 +234,15 @@ let
name: cfg:
let
settings = { inherit (cfg) user group; };
+ userHome = if cfg.stateDir != null then cfg.stateDir else config.users.users.${cfg.user}.home;
in
lib.nameValuePair "borgbackup-job-${name}" (
{
# Create parent dirs separately, to ensure correct ownership.
- "${config.users.users."${cfg.user}".home}/.config".d = settings;
- "${config.users.users."${cfg.user}".home}/.cache".d = settings;
- "${config.users.users."${cfg.user}".home}/.config/borg".d = settings;
- "${config.users.users."${cfg.user}".home}/.cache/borg".d = settings;
+ "${userHome}/.config".d = settings;
+ "${userHome}/.cache".d = settings;
+ "${userHome}/.config/borg".d = settings;
+ "${userHome}/.cache/borg".d = settings;
}
// lib.optionalAttrs (isLocalPath cfg.repo && !cfg.removableDevice) {
"${cfg.repo}".d = settings;
@@ -487,6 +490,16 @@ in
default = "root";
};
+ stateDir = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ description = ''
+ Override the directory in which {command}`borg` stores its
+ configuration and cache. By default it uses the user's
+ home directory but is some cases this can cause conflicts.
+ '';
+ default = null;
+ };
+
wrapper = lib.mkOption {
type = with lib.types; nullOr str;
description = ''
--
2.50.1

190
test/blocks/borgbackup.nix Normal file
View file

@ -0,0 +1,190 @@
{ pkgs, lib, ... }:
let
testLib = pkgs.callPackage ../common.nix { };
commonTest =
user:
lib.shb.runNixOSTest {
name = "borgbackup_backupAndRestore_${user}";
nodes.machine =
{ config, ... }:
{
imports = [
testLib.baseImports
../../modules/blocks/hardcodedsecret.nix
../../modules/blocks/borgbackup.nix
];
shb.hardcodedsecret.A = {
request = {
owner = "root";
group = "keys";
mode = "0440";
};
settings.content = "secretA";
};
shb.hardcodedsecret.B = {
request = {
owner = "root";
group = "keys";
mode = "0440";
};
settings.content = "secretB";
};
shb.hardcodedsecret.passphrase = {
request = config.shb.borgbackup.instances."testinstance".settings.passphrase.request;
settings.content = "passphrase";
};
shb.borgbackup.instances."testinstance" = {
settings = {
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = "/opt/repos/A";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
};
# Those are not needed by the repository but are still included
# so we can test them in the hooks section.
secrets = {
A.source = config.shb.hardcodedsecret.A.result.path;
B.source = config.shb.hardcodedsecret.B.result.path;
};
};
};
request = {
inherit user;
sourceDirectories = [
"/opt/files/A"
"/opt/files/B"
];
hooks.beforeBackup = [
''
echo $RUNTIME_DIRECTORY
if [ "$RUNTIME_DIRECTORY" = /run/borgbackup-backups-testinstance_opt_repos_A ]; then
if ! [ -f /run/secrets_borgbackup/borgbackup-backups-testinstance_opt_repos_A ]; then
exit 10
fi
if [ -z "$A" ] || ! [ "$A" = "secretA" ]; then
echo "A:$A"
exit 11
fi
if [ -z "$B" ] || ! [ "$B" = "secretB" ]; then
echo "B:$B"
exit 12
fi
fi
''
];
};
};
};
extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true;
testScript =
{ nodes, ... }:
let
provider = nodes.machine.shb.borgbackup.instances."testinstance";
backupService = provider.result.backupService;
restoreScript = provider.result.restoreScript;
in
''
from dictdiffer import diff
def list_files(dir):
files_and_content = {}
files = machine.succeed(f"""
find {dir} -type f
""").split("\n")[:-1]
for f in files:
content = machine.succeed(f"""
cat {f}
""").strip()
files_and_content[f] = content
return files_and_content
def assert_files(dir, files):
result = list(diff(list_files(dir), files))
if len(result) > 0:
raise Exception("Unexpected files:", result)
with subtest("Create initial content"):
machine.succeed("""
mkdir -p /opt/files/A
mkdir -p /opt/files/B
echo repoA_fileA_1 > /opt/files/A/fileA
echo repoA_fileB_1 > /opt/files/A/fileB
echo repoB_fileA_1 > /opt/files/B/fileA
echo repoB_fileB_1 > /opt/files/B/fileB
chown ${user}: -R /opt/files
chmod go-rwx -R /opt/files
""")
assert_files("/opt/files", {
'/opt/files/B/fileA': 'repoB_fileA_1',
'/opt/files/B/fileB': 'repoB_fileB_1',
'/opt/files/A/fileA': 'repoA_fileA_1',
'/opt/files/A/fileB': 'repoA_fileB_1',
})
with subtest("First backup in repo A"):
machine.succeed("systemctl start ${backupService}")
with subtest("New content"):
machine.succeed("""
echo repoA_fileA_2 > /opt/files/A/fileA
echo repoA_fileB_2 > /opt/files/A/fileB
echo repoB_fileA_2 > /opt/files/B/fileA
echo repoB_fileB_2 > /opt/files/B/fileB
""")
assert_files("/opt/files", {
'/opt/files/B/fileA': 'repoB_fileA_2',
'/opt/files/B/fileB': 'repoB_fileB_2',
'/opt/files/A/fileA': 'repoA_fileA_2',
'/opt/files/A/fileB': 'repoA_fileB_2',
})
with subtest("Delete content"):
machine.succeed("""
rm -r /opt/files/A /opt/files/B
""")
assert_files("/opt/files", {})
with subtest("Restore initial content from repo A"):
machine.succeed("""
${restoreScript} restore latest
""")
assert_files("/opt/files", {
'/opt/files/B/fileA': 'repoB_fileA_1',
'/opt/files/B/fileB': 'repoB_fileB_1',
'/opt/files/A/fileA': 'repoA_fileA_1',
'/opt/files/A/fileB': 'repoA_fileB_1',
})
'';
};
in
{
backupAndRestoreRoot = commonTest "root";
backupAndRestoreUser = commonTest "nobody";
}

View file

@ -72,4 +72,74 @@ in
};
};
};
borgbackup_root = contracts.test.backup {
name = "borgbackup_root";
username = "root";
providerRoot = [
"shb"
"borgbackup"
"instances"
"mytest"
];
modules = [
../../modules/blocks/borgbackup.nix
../../modules/blocks/hardcodedsecret.nix
];
settings =
{ repository, config, ... }:
{
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = repository;
timerConfig = {
OnCalendar = "00:00:00";
};
};
};
extraConfig =
{ username, config, ... }:
{
shb.hardcodedsecret.passphrase = {
request = config.shb.borgbackup.instances."mytest".settings.passphrase.request;
settings.content = "passphrase";
};
};
};
borgbackup_nonroot = contracts.test.backup {
name = "borgbackup_nonroot";
username = "me";
providerRoot = [
"shb"
"borgbackup"
"instances"
"mytest"
];
modules = [
../../modules/blocks/borgbackup.nix
../../modules/blocks/hardcodedsecret.nix
];
settings =
{ repository, config, ... }:
{
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = repository;
timerConfig = {
OnCalendar = "00:00:00";
};
};
};
extraConfig =
{ username, config, ... }:
{
shb.hardcodedsecret.passphrase = {
request = config.shb.borgbackup.instances."mytest".settings.passphrase.request;
settings.content = "passphrase";
};
};
};
}

View file

@ -48,4 +48,51 @@ in
};
};
};
borgbackup_postgres = contracts.test.databasebackup {
name = "borgbackup_postgres";
requesterRoot = [
"shb"
"postgresql"
"databasebackup"
];
providerRoot = [
"shb"
"borgbackup"
"databases"
"postgresql"
];
modules = [
../../modules/blocks/postgresql.nix
../../modules/blocks/borgbackup.nix
../../modules/blocks/hardcodedsecret.nix
];
settings =
{ repository, config, ... }:
{
enable = true;
stateDir = "/var/lib/borgbackup_postgres";
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = repository;
timerConfig = {
OnCalendar = "00:00:00";
};
};
};
extraConfig =
{ config, database, ... }:
{
shb.postgresql.ensures = [
{
inherit database;
username = database;
}
];
shb.hardcodedsecret.passphrase = {
request = config.shb.borgbackup.databases.postgresql.settings.passphrase.request;
settings.content = "passphrase";
};
};
};
}