nixos/librechat: init (#462287)

This commit is contained in:
Gergő Gutyina
2025-12-13 00:45:15 +00:00
committed by GitHub
7 changed files with 323 additions and 0 deletions
+6
View File
@@ -22688,6 +22688,12 @@
keys = [ { fingerprint = "7DCA 5615 8AB2 621F 2F32 9FF4 1C7C E491 479F A273"; } ];
name = "Rahul Butani";
};
rrvsh = {
email = "rafiq@rrv.sh";
github = "rrvsh";
githubId = 20300874;
name = "Mohammad Rafiq";
};
rseichter = {
email = "nixos.org@seichter.de";
github = "rseichter";
@@ -18,6 +18,8 @@
- [ImmichFrame](https://immichframe.dev/), display your photos from Immich as a digital photo frame. Available as `services.immichframe`.
- [LibreChat](https://www.librechat.ai/), open-source self-hostable ChatGPT clone with Agents and RAG APIs. Available as [services.librechat](#opt-services.librechat.enable).
- [udp-over-tcp](https://github.com/mullvad/udp-over-tcp), a tunnel for proxying UDP traffic over a TCP stream. Available as `services.udp-over-tcp`.
## Backward Incompatibilities {#sec-release-26.05-incompatibilities}
+1
View File
@@ -1655,6 +1655,7 @@
./services/web-apps/lasuite-docs.nix
./services/web-apps/lasuite-meet.nix
./services/web-apps/lemmy.nix
./services/web-apps/librechat.nix
./services/web-apps/librespeed.nix
./services/web-apps/libretranslate.nix
./services/web-apps/limesurvey.nix
@@ -0,0 +1,259 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.librechat;
format = pkgs.formats.yaml { };
configFile = format.generate "librechat.yaml" cfg.settings;
exportCredentials = n: _: ''export ${n}="$(${pkgs.systemd}/bin/systemd-creds cat ${n}_FILE)"'';
exportAllCredentials = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList exportCredentials vars);
getLoadCredentialList = lib.mapAttrsToList (n: v: "${n}_FILE:${v}") cfg.credentials;
in
{
options.services.librechat = {
enable = lib.mkEnableOption "the LibreChat server";
package = lib.mkPackageOption pkgs "librechat" { };
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open the port in the firewall.
'';
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/librechat";
example = "/persist/librechat";
description = "Absolute path for where the LibreChat server will use as its data directory to store logs, user uploads, and generated images.";
};
user = lib.mkOption {
type = lib.types.str;
default = "librechat";
example = "alice";
description = "The user to run the service as.";
};
group = lib.mkOption {
type = lib.types.str;
default = "librechat";
example = "users";
description = "The group to run the service as.";
};
credentials = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
default = { };
example = {
CREDS_KEY = "/run/secrets/creds_key";
};
description = ''
Environment variables which are loaded from the contents of files at a file paths, mainly used for secrets.
See [LibreChat environment variables](https://www.librechat.ai/docs/configuration/dotenv).
Alternatively you can use `services.librechat.credentialsFile` to define all the variables in a single file.
'';
};
env = lib.mkOption {
type = lib.types.submodule {
freeformType =
with lib.types;
attrsOf (oneOf [
str
path
(coercedTo int toString str)
(coercedTo float toString str)
(coercedTo port toString str)
(coercedTo bool (x: if x then "true" else "false") str)
]);
options = {
CONFIG_PATH = lib.mkOption {
default = configFile;
internal = true;
readOnly = true;
};
PORT = lib.mkOption {
type = with lib.types; coercedTo port toString str;
default = 3080;
example = 2309;
description = "The value that will be passed to the PORT environment variable, telling LibreChat what to listen on.";
};
};
};
example = {
ALLOW_REGISTRATION = true;
HOST = "0.0.0.0";
PORT = 2309;
CONSOLE_JSON_STRING_LENGTH = 255;
};
description = ''
Environment variables that will be set for the service.
See [LibreChat environment variables](https://www.librechat.ai/docs/configuration/dotenv).
'';
};
credentialsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
description = ''
Path to a file that contains environment variables.
See [LibreChat environment variables](https://www.librechat.ai/docs/configuration/dotenv).
Example content of the file:
```
CREDS_KEY=6d6deb03cdfb27ea454f6b9ddd42494bdce4af25d50d8aee454ddce583690cc5
```
Alternatively you can use `services.librechat.credentials` to define the value of each variable in a separate file.
'';
default = "/dev/null";
example = "/run/secrets/librechat";
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
};
default = { };
example = {
version = "1.0.8";
cache = true;
interface = {
privacyPolicy = {
externalUrl = "https://librechat.ai/privacy-policy";
openNewTab = true;
};
};
endpoints = {
custom = [
{
name = "OpenRouter";
apiKey = "\${OPENROUTER_KEY}";
baseURL = "https://openrouter.ai/api/v1";
models = {
default = [ "meta-llama/llama-3-70b-instruct" ];
fetch = true;
};
titleConvo = true;
titleModule = "meta-llama/llama-3-70b-instruct";
dropParams = [ "stop" ];
modelDisplayLabel = "OpenRouter";
}
];
};
};
description = ''
A free-form attribute set that will be written to librechat.yaml.
See the [LibreChat configuration options](https://www.librechat.ai/docs/configuration/librechat_yaml).
You can use environment variables by wrapping them in $\{}. Take care to escape the \$ character.
'';
};
enableLocalDB = lib.mkEnableOption "a local mongodb instance";
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.env ? MONGO_URI || cfg.credentials ? MONGO_URI;
message = "MongoDB is not configured, either set `services.librechat.enableLocalDB = true` or provide your own MongoDB instance by setting `services.librechat.env.MONGO_URI` or `services.credentials.MONGO_URI`.";
}
{
assertion =
cfg.credentialsFile != "/dev/null"
|| (
(cfg.env ? CREDS_KEY || cfg.credentials ? CREDS_KEY)
&& (cfg.env ? CREDS_IV || cfg.credentials ? CREDS_IV)
&& (cfg.env ? JWT_SECRET || cfg.credentials ? JWT_SECRET)
&& (cfg.env ? JWT_REFRESH_SECRET || cfg.credentials ? JWT_REFRESH_SECRET)
);
message = ''
CREDS_KEY, CREDS_IV, JWT_SECRET, and JWT_REFRESH_SECRET must be defined in `services.librechat.credentials` and point to locations of files on the host or in a file that `services.credentialsFile` is pointing to.
Alternatively it can be defined in `services.librechat.env` with literal values but they will be saved within the world-readable nix store.;
You can use https://www.librechat.ai/toolkit/creds_generator to generate these.
'';
}
];
networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.port;
systemd.tmpfiles.settings."10-librechat"."${cfg.dataDir}".d = {
mode = "0755";
inherit (cfg) user group;
};
systemd.services.librechat = {
wantedBy = [ "multi-user.target" ];
after = [
"tmpfiles.target"
]
++ lib.optional cfg.enableLocalDB "mongodb.service";
description = "Open-source app for all your AI conversations, fully customizable and compatible with any AI provider";
environment = cfg.env;
script = # sh
''
${exportAllCredentials cfg.credentials}
cd ${cfg.dataDir}
${lib.getExe cfg.package}
'';
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
StateDirectory = builtins.baseNameOf cfg.dataDir;
WorkingDirectory = cfg.dataDir;
LoadCredential = getLoadCredentialList;
EnvironmentFile = cfg.credentialsFile;
Restart = "on-failure";
RestartSec = 10;
# Hardening
CapabilityBoundingSet = "";
NoNewPrivileges = true;
PrivateUsers = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateMounts = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
UMask = "0077";
};
};
users.users.librechat = lib.mkIf (cfg.user == "librechat") {
name = "librechat";
isSystemUser = true;
group = "librechat";
description = "LibreChat server user";
};
users.groups.librechat = lib.mkIf (cfg.user == "librechat") { };
services.librechat.env.MONGO_URI = lib.mkIf cfg.enableLocalDB "mongodb://localhost:27017";
services.mongodb.enable = lib.mkIf cfg.enableLocalDB true;
};
meta.maintainers = with lib.maintainers; [
gepbird
niklaskorz
rrvsh
];
}
+1
View File
@@ -857,6 +857,7 @@ in
lemurs-xorg = runTest ./lemurs/lemurs-xorg.nix;
lemurs-xorg-script = runTest ./lemurs/lemurs-xorg-script.nix;
libinput = runTest ./libinput.nix;
librechat = runTest ./librechat.nix;
librenms = runTest ./librenms.nix;
libresprite = runTest ./libresprite.nix;
libreswan = runTest ./libreswan.nix;
+50
View File
@@ -0,0 +1,50 @@
# Run this test with NIXPKGS_ALLOW_UNFREE=1
{ lib, ... }:
{
name = "librechat";
meta.maintainers = with lib.maintainers; [
gepbird
niklaskorz
rrvsh
];
nodes.machine =
{ pkgs, ... }:
let
# !!! Don't do this with real keys. The /nix store is world-readable!
credsKeyFile = pkgs.writeText "librechat-creds-key" "6d6deb03cdfb27ea454f6b9ddd42494bdce4af25d50d8aee454ddce583690cc5";
credsIvFile = pkgs.writeText "librechat-creds-iv" "7c09a571f65ac793611685cc9ab1dbe7";
jwtSecret = pkgs.writeText "librechat-jwt-secret" "29c4dc7f7de15306accf5eddb4cb8a70eb233d9fba4301f8f47f14c8c047ac81";
jwtRefreshSecret = pkgs.writeText "librechat-jwt-refresh-secret" "f2c1685561f2f570b3e7955df267b5c602ee099f14dc5caa0dacc320580ea180";
in
{
services.librechat = {
enable = true;
env = {
ALLOW_REGISTRATION = true;
};
credentials = {
# The following were randomly generated with https://www.librechat.ai/toolkit/creds_generator
CREDS_KEY = credsKeyFile;
CREDS_IV = credsIvFile;
JWT_SECRET = jwtSecret;
JWT_REFRESH_SECRET = jwtRefreshSecret;
};
enableLocalDB = true;
};
};
testScript = ''
machine.start()
machine.succeed("grep -qF 'ALLOW_REGISTRATION=true' /etc/systemd/system/librechat.service")
machine.wait_for_unit("librechat.service")
machine.wait_for_open_port(3080)
machine.succeed("curl --fail http://localhost:3080/")
machine.shutdown()
'';
}
+4
View File
@@ -7,6 +7,7 @@
node-gyp,
vips,
nix-update-script,
nixosTests,
}:
buildNpmPackage rec {
@@ -84,6 +85,9 @@ buildNpmPackage rec {
"^v(\\d+\\.\\d+\\.\\d+)$"
];
};
tests = {
inherit (nixosTests) librechat;
};
};
meta = {