1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat(nix): add NixOS module for archivr-server

modules/nixos/archivr-server.nix — services.archivr-server NixOS module:
- enable/bind/archives/user/group/openFirewall options
- generates archivr-server.toml from options via pkgs.writeText
- pins auth DB to /var/lib/archivr-server/ (StateDirectory)
- dedicated archivr system user + group
- hardened systemd unit: ProtectSystem=strict, NoNewPrivileges,
  PrivateTmp, RestrictNamespaces, etc.; archive paths whitelisted
- openFirewall parses the port from the bind string automatically

flake.nix:
- add self to outputs args
- add aarch64-linux to systems (Raspberry Pi / ARM servers)
- expose nixosModules.archivr-server + nixosModules.default

.gitignore: whitelist modules/ directory

docs/README.md: add 'Hosting on NixOS' section with full example
This commit is contained in:
TheGeneralist 2026-06-29 16:51:20 +02:00
parent 4311e85f95
commit f9de282b0e
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
4 changed files with 218 additions and 1 deletions

3
.gitignore vendored
View file

@ -19,6 +19,9 @@
!ARCHIVR-MENTAL-MODEL.md !ARCHIVR-MENTAL-MODEL.md
!NEXT.md !NEXT.md
!modules/
!modules/**
!frontend/ !frontend/
!frontend/** !frontend/**
frontend/node_modules/ frontend/node_modules/

View file

@ -137,6 +137,58 @@ Auth and session handling will be designed when remote or public hosting becomes
- X/Twitter Tweet content scrape: [Tweet and Thread shorthands](#supported-shorthand-inputs). (These are saved as JSON files in `raw_tweets/`) - X/Twitter Tweet content scrape: [Tweet and Thread shorthands](#supported-shorthand-inputs). (These are saved as JSON files in `raw_tweets/`)
- Instagram, Facebook, TikTok, Reddit, Snapchat: direct URLs or platform-prefixed shorthand passed through to `yt-dlp` - Instagram, Facebook, TikTok, Reddit, Snapchat: direct URLs or platform-prefixed shorthand passed through to `yt-dlp`
### Hosting on NixOS
The flake exposes a `nixosModules.default` output. Add it to your system flake and
enable the service:
```nix
# flake.nix (your system flake)
{
inputs.archivr.url = "github:thegeneralist/archivr";
outputs = { nixpkgs, archivr, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
archivr.nixosModules.default
{
services.archivr-server = {
enable = true;
bind = "127.0.0.1:8080"; # loopback only; put nginx/caddy in front for TLS
archives = [
{ id = "personal"; label = "Personal"; path = "/srv/archivr/personal/.archivr"; }
{ id = "work"; label = "Work"; path = "/srv/archivr/work/.archivr"; }
];
};
}
];
};
};
}
```
The module:
- Creates an `archivr` system user and group.
- Generates the TOML config from your options and stores the auth database under
`/var/lib/archivr-server/` (persists across upgrades).
- Runs under a hardened systemd unit (`ProtectSystem = strict`, `NoNewPrivileges`,
`PrivateTmp`, etc.). Archive directories are whitelisted for read-write access.
- Restarts automatically on failure.
**`openFirewall`** — set to `true` to open the TCP port derived from `bind`.
Only needed when binding to a non-loopback address:
```nix
services.archivr-server = {
bind = "0.0.0.0:8080";
openFirewall = true;
};
```
**Archive directories** must be readable and writable by the `archivr` user.
Initialise them with `archivr init` first, then `chown -R archivr:archivr /srv/archivr`.
### Supported Shorthand Inputs ### Supported Shorthand Inputs
- YouTube video/short media: - YouTube video/short media:

View file

@ -16,11 +16,12 @@
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
outputs = outputs =
{ nixpkgs, ... }: { nixpkgs, self, ... }:
let let
lib = nixpkgs.lib; lib = nixpkgs.lib;
systems = [ systems = [
"x86_64-linux" "x86_64-linux"
"aarch64-linux"
"aarch64-darwin" "aarch64-darwin"
]; ];
in in
@ -171,6 +172,11 @@
} }
); );
nixosModules = {
archivr-server = import ./modules/nixos/archivr-server.nix { inherit self; };
default = self.nixosModules.archivr-server;
};
devShells = lib.genAttrs systems ( devShells = lib.genAttrs systems (
system: system:
let let

View file

@ -0,0 +1,156 @@
# NixOS module for archivr-server.
# Consumed by flake.nix as:
# nixosModules.archivr-server = import ./modules/nixos/archivr-server.nix { inherit self; };
{ self }:
{ config, lib, pkgs, ... }:
let
cfg = config.services.archivr-server;
# Generate the TOML registry file from NixOS options.
# The auth DB is pinned to the StateDirectory so it survives upgrades.
configFile = pkgs.writeText "archivr-server.toml" ''
bind = "${cfg.bind}"
auth_db_path = "/var/lib/archivr-server/archivr-auth.sqlite"
${lib.concatMapStrings (a: ''
[[archives]]
id = "${a.id}"
label = "${a.label}"
archive_path = "${a.path}"
'') cfg.archives}
'';
in
{
options.services.archivr-server = {
enable = lib.mkEnableOption "archivr web server";
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.system}.archivr-server;
defaultText = lib.literalExpression "archivr-server from the archivr flake";
description = "The archivr-server package to use.";
};
bind = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1:8080";
example = "0.0.0.0:8080";
description = ''
Address and port to listen on. Defaults to loopback only.
Binding to a non-loopback address exposes the server on the network;
put a reverse proxy with TLS and authentication in front before doing so.
'';
};
archives = lib.mkOption {
type = lib.types.listOf (lib.types.submodule {
options = {
id = lib.mkOption {
type = lib.types.str;
example = "personal";
description = "Unique archive identifier used in the config and API URLs.";
};
label = lib.mkOption {
type = lib.types.str;
example = "Personal";
description = "Display name shown in the UI archive switcher.";
};
path = lib.mkOption {
type = lib.types.str;
example = "/srv/archivr/personal/.archivr";
description = "Absolute path to the .archivr directory created by {command}`archivr init`.";
};
};
});
default = [];
description = ''
Archives to mount. Each entry maps to one {code}`[[archives]]` block in the
generated config file. The list must contain at least one entry.
'';
example = lib.literalExpression ''
[
{ id = "personal"; label = "Personal"; path = "/srv/archivr/personal/.archivr"; }
{ id = "work"; label = "Work"; path = "/srv/archivr/work/.archivr"; }
]
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "archivr";
description = "User account under which archivr-server runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "archivr";
description = "Group under which archivr-server runs.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Open the firewall TCP port derived from
{option}`services.archivr-server.bind`. Only meaningful when binding
to a non-loopback address.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.archives != [];
message = "services.archivr-server.archives must contain at least one archive.";
}
];
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
description = "archivr-server service user";
home = "/var/lib/archivr-server";
createHome = false; # StateDirectory creates it
};
users.groups.${cfg.group} = { };
systemd.services.archivr-server = {
description = "archivr web server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/archivr-server ${configFile}";
User = cfg.user;
Group = cfg.group;
# State directory — auth SQLite lives here across upgrades/restarts.
StateDirectory = "archivr-server";
StateDirectoryMode = "0750";
Restart = "on-failure";
RestartSec = "5s";
# Hardening — make the entire FS read-only except for the state
# directory and the mounted archive directories.
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ReadWritePaths = [ "/var/lib/archivr-server" ] ++ (map (a: a.path) cfg.archives);
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
LockPersonality = true;
RestrictNamespaces = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
(lib.toInt (lib.last (lib.splitString ":" cfg.bind)))
];
};
};
}