nixos/qui: init service (#472934)

This commit is contained in:
Sandro
2026-01-16 17:01:45 +00:00
committed by GitHub
6 changed files with 251 additions and 5 deletions
@@ -20,6 +20,8 @@
- [reaction](https://reaction.ppom.me/), a daemon that scans program outputs for repeated patterns, and takes action. A common usage is to scan ssh and webserver logs, and to ban hosts that cause multiple authentication errors. A modern alternative to fail2ban. Available as [services.reaction](#opt-services.reaction.enable).
- [qui](https://github.com/autobrr/qui), a modern alternative webUI for qBittorrent, with multi-instance support. Written in Go/React. Available as [services.qui](#opt-services.qui.enable).
- [LibreChat](https://www.librechat.ai/), open-source self-hostable ChatGPT clone with Agents and RAG APIs. Available as [services.librechat](#opt-services.librechat.enable).
- [nohang](https://github.com/hakavlad/nohang), a daemon for Linux that prevents out of memory (OOM) situations from affecting system responsiveness. Available as [services.nohang](#opt-services.nohang.enable)
+1
View File
@@ -1551,6 +1551,7 @@
./services/torrent/opentracker.nix
./services/torrent/peerflix.nix
./services/torrent/qbittorrent.nix
./services/torrent/qui.nix
./services/torrent/rtorrent.nix
./services/torrent/torrentstream.nix
./services/torrent/transmission.nix
+191
View File
@@ -0,0 +1,191 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
getExe
maintainers
mkEnableOption
mkIf
mkOption
mkPackageOption
;
inherit (lib.types)
bool
path
port
str
submodule
;
cfg = config.services.qui;
stateDir = "/var/lib/qui";
configFormat = pkgs.formats.toml { };
configFile = configFormat.generate "qui.toml" cfg.settings;
in
{
options = {
services.qui = {
enable = mkEnableOption "qui";
package = mkPackageOption pkgs "qui" { };
user = mkOption {
type = str;
default = "qui";
description = "User to run qui as.";
};
group = mkOption {
type = str;
default = "qui";
example = "torrents";
description = "Group to run qui as.";
};
openFirewall = mkOption {
type = bool;
default = false;
description = "Whether or not to open ports in the firewall for qui.";
};
secretFile = mkOption {
type = path;
example = "/run/secrets/qui-session.txt";
description = ''
Path to a file that contains the session secret. The session secret
can be generated with `openssl rand -hex 32`.
'';
};
settings = mkOption {
default = { };
example = {
port = 7777;
logLevel = "DEBUG";
metricsEnabled = true;
};
type = submodule {
freeformType = configFormat.type;
options = {
host = mkOption {
type = str;
default = "127.0.0.1";
description = "The host address qui listens on.";
};
port = mkOption {
type = port;
default = 7476;
description = "The port qui listens on.";
};
};
};
description = ''
qui configuration options.
Refer to the [template config](https://github.com/autobrr/qui/blob/main/internal/config/config.go)
in the source code for the available options.
The documentation contains the available [environment variables](https://getqui.com/docs/configuration/environment/),
this can be used to get an overview.
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = !(cfg.settings ? sessionSecret);
message = ''
Session secrets should not be passed via settings, as
these are stored in the world-readable nix store.
Use the secretFile option instead.'';
}
];
systemd.services.qui = {
description = "qui: alternative qBittorrent webUI";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
LoadCredential = "sessionSecret:${cfg.secretFile}";
Environment = [ "QUI__SESSION_SECRET_FILE=%d/sessionSecret" ];
StateDirectory = "qui";
ExecStartPre = ''
${pkgs.coreutils}/bin/install -m 600 '${configFile}' '%S/qui/config.toml'
'';
ExecStart = "${getExe cfg.package} serve --config-dir %S/qui";
Restart = "on-failure";
# Based on qbittorrent and nemorosa hardening settings
# Similar to what systemd hardening helper suggests
CapabilityBoundingSet = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateNetwork = false;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = "yes";
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
# This should allow for hardlinking to torrent client files
ProtectSystem = "full";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" ];
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.settings.port ];
};
users = {
users = mkIf (cfg.user == "qui") {
qui = {
group = cfg.group;
description = "qui user";
isSystemUser = true;
home = stateDir;
};
};
groups = mkIf (cfg.group == "qui") {
qui = { };
};
};
};
meta.maintainers = with maintainers; [ undefined-landmark ];
}
+1
View File
@@ -1332,6 +1332,7 @@ in
qtile = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./qtile/default.nix;
qtile-extras = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./qtile-extras/default.nix;
quake3 = runTest ./quake3.nix;
qui = runTest ./qui.nix;
quicktun = runTest ./quicktun.nix;
quickwit = runTest ./quickwit.nix;
rabbitmq = runTest ./rabbitmq.nix;
+47
View File
@@ -0,0 +1,47 @@
{ lib, ... }:
{
name = "qui";
meta.maintainers = with lib.maintainers; [ undefined-landmark ];
nodes.machine =
{ pkgs, ... }:
let
# We create this secret in the Nix store (making it readable by everyone).
# DO NOT DO THIS OUTSIDE OF TESTS!!
testSecretFile = pkgs.writeText "session_secret" "not-secret";
in
{
services.qui = {
enable = true;
secretFile = testSecretFile;
};
# Use port other than default to test if settings options work.
specialisation.settingsPort.configuration = {
services.qui = {
enable = true;
secretFile = testSecretFile;
settings.port = 7777;
};
};
};
testScript =
{ nodes, ... }:
let
settingsPort = "${nodes.machine.system.build.toplevel}/specialisation/settingsPort";
in
# python
''
def test_webui(port):
machine.wait_for_unit("qui.service")
machine.wait_for_open_port(port)
machine.wait_until_succeeds(f"curl --fail http://localhost:{port}")
test_webui(7476)
machine.succeed("${settingsPort}/bin/switch-to-configuration test")
test_webui(7777)
'';
}
+9 -5
View File
@@ -3,6 +3,7 @@
buildGoModule,
fetchFromGitHub,
stdenvNoCC,
nixosTests,
nix-update-script,
nodejs,
pnpm_9,
@@ -72,11 +73,14 @@ buildGoModule (finalAttrs: {
versionCheckProgramArg = "version";
doInstallCheck = true;
passthru.updateScript = nix-update-script {
extraArgs = [
"--subpackage"
"qui-web"
];
passthru = {
updateScript = nix-update-script {
extraArgs = [
"--subpackage"
"qui-web"
];
};
tests.testService = nixosTests.qui;
};
meta = {