From 71fe7b5dd9da0125186907af4d28d9432ae40538 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 31 May 2025 15:04:30 +0800 Subject: [PATCH 1/3] chore(librechat): rm all env vars --- modules/nixos/server/librechat/default.nix | 319 ++++----------------- 1 file changed, 51 insertions(+), 268 deletions(-) diff --git a/modules/nixos/server/librechat/default.nix b/modules/nixos/server/librechat/default.nix index 3f20e60..4bef516 100644 --- a/modules/nixos/server/librechat/default.nix +++ b/modules/nixos/server/librechat/default.nix @@ -1,4 +1,3 @@ -#TODO: add settings option that generates librechat.yaml { config, lib, @@ -7,12 +6,30 @@ }: let cfg = config.server.librechat; + configFile = pkgs.writeTextFile { + name = "librechat.yaml"; + text = lib.generators.toYAML { } cfg.settings; + }; in { options.server.librechat = { enable = lib.mkEnableOption "Whether to enable the LibreChat server."; openFirewall = lib.mkEnableOption "Whether to open the port in the firewall."; - + path = lib.mkOption { + type = lib.types.str; + default = "/var/lib/librechat"; + description = "Absolute path for where the LibreChat server will use as its working directory."; + }; + user = lib.mkOption { + type = lib.types.str; + default = "librechat"; + description = "The user to run the service as."; + }; + group = lib.mkOption { + type = lib.types.str; + default = "librechat"; + description = "The group to run the service as."; + }; settings = lib.mkOption { type = lib.types.attrs; default = { }; @@ -49,273 +66,39 @@ in ''; description = "A free-form attribute set that will be written to librechat.yaml."; }; - - path = lib.mkOption { - type = lib.types.str; - default = "/var/lib/librechat"; - description = "Absolute path for where the LibreChat server will use as its working directory."; - }; - - user = lib.mkOption { - type = lib.types.str; - default = "librechat"; - description = "The user to run the service as."; - }; - - group = lib.mkOption { - type = lib.types.str; - default = "librechat"; - description = "The group to run the service as."; - }; - - prevent-indexing = lib.mkOption { - type = lib.types.bool; - default = true; - example = false; - description = "Prevents public search engines from indexing your website."; - }; - - host = lib.mkOption { - type = lib.types.str; - default = "localhost"; - example = "0.0.0.0"; - description = "Specifies the host."; - }; - - port = lib.mkOption { - type = lib.types.int; - default = 3080; - example = 2309; - description = "Specifies the port."; - }; - - #TODO: Add option to use documentDb. - mongodbURI = lib.mkOption { - type = lib.types.str; - default = ""; - example = "mongodb://127.0.0.1:27017/LibreChat"; - description = "Specifies the MongoDB URI. Must be set or the app will crash on startup."; - }; - - trust_proxy = lib.mkOption { - type = lib.types.int; - default = 1; - example = 0; - description = "Use the address that is at most n number of hops away from the Express application. See https://expressjs.com/en/guide/behind-proxies.html for more information about this."; - }; - - credentials = { - creds_key = lib.mkOption { - type = lib.types.str; - default = ""; - description = "32-byte key (64 characters in hex) for securely storing credentials. Required for app startup. WARNING: If you don't set this or the _file option, the app will crash on startup. You can use this https://www.librechat.ai/toolkit/creds_generator to generate them quickly."; - }; - creds_key_file = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Path to file containing 32-byte key (64 characters in hex) for securely storing credentials. Required for app startup. WARNING: If you don't set this or the _file option, the app will crash on startup. You can use this https://www.librechat.ai/toolkit/creds_generator to generate them quickly."; - }; - creds_iv = lib.mkOption { - type = lib.types.str; - default = ""; - description = "16-byte IV (32 characters in hex) for securely storing credentials. Required for app startup. WARNING: If you don't set this or the _file option, the app will crash on startup. You can use this https://www.librechat.ai/toolkit/creds_generator to generate them quickly."; - }; - creds_iv_file = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Path to file containing 16-byte IV (32 characters in hex) for securely storing credentials. Required for app startup. WARNING: If you don't set this or the _file option, the app will crash on startup. You can use this https://www.librechat.ai/toolkit/creds_generator to generate them quickly."; - }; - jwt_secret = lib.mkOption { - type = lib.types.str; - default = ""; - description = "JWT secret key. Generate with https://www.librechat.ai/toolkit/creds_generator."; - }; - jwt_secret_file = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Absolute path to file containing JWT secret key. Generate with https://www.librechat.ai/toolkit/creds_generator."; - }; - jwt_refresh_secret = lib.mkOption { - type = lib.types.str; - default = ""; - description = "JWT refresh secret key. Generate with https://www.librechat.ai/toolkit/creds_generator."; - }; - jwt_refresh_secret_file = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Absolute path to file containing JWT refresh secret key. Generate with https://www.librechat.ai/toolkit/creds_generator."; - }; - }; - - app_domains = { - client = lib.mkOption { - type = lib.types.str; - default = "http://localhost:${cfg.port}"; - example = "https://librechat.example.com"; - description = "Specifies the client-side domain."; - }; - server = lib.mkOption { - type = lib.types.str; - default = "http://localhost:${cfg.port}"; - example = "https://librechat.example.com"; - description = "Specifies the server-side domain."; - }; - }; - - logging = { - enableDebugLogging = lib.mkOption { - type = lib.types.bool; - default = true; - description = "Keep debug logs active."; - }; - enableConsoleLogging = lib.mkEnableOption "Enable verbose console/stdout logs in the same format as file debug logs."; - enableConsoleJSONLogging = lib.mkEnableOption "Enable verbose JSON console/stdout logs suitable for cloud deployments like GCP/AWS."; - consoleJSONLoggingLength = lib.mkOption { - type = lib.types.int; - default = 255; - description = "Configure the truncation size for console/stdout logs."; - }; - }; - - static_cache = { - max_age = lib.mkOption { - type = lib.types.str; - default = "172800"; - description = "Cache-Control max-age in seconds."; - }; - s_max_age = lib.mkOption { - type = lib.types.str; - default = "86400"; - description = "Cache-Control s-maxage in seconds for shared caches (CDNs and proxies)."; - }; - disable_compression = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Disables compression for static files."; - }; - }; - - index_page_caching = { - cache_control = lib.mkOption { - type = lib.types.str; - default = "no-cache, no-store, must-revalidate"; - description = "Cache-Control header for index.html."; - }; - pragma = lib.mkOption { - type = lib.types.str; - default = "no-cache"; - description = "Pragma header for index.html."; - }; - expires = lib.mkOption { - type = lib.types.str; - default = "0"; - description = "Expires header for index.html."; - }; - }; - - auth = { - allowEmailLogin = lib.mkOption { - type = lib.types.bool; - default = true; - description = "Enable or disable ONLY email login."; - }; - allowEmailRegistration = lib.mkEnableOption "Enable or disable Email registration of new users."; - }; - }; - config = lib.mkIf cfg.enable ( - let - configFile = pkgs.writeTextFile { - name = "librechat.yaml"; - text = lib.generators.toYAML { } cfg.settings; + config = lib.mkIf cfg.enable { + networking.firewall.allowedTCPPorts = if cfg.openFirewall then [ cfg.port ] else [ ]; + systemd.services.librechat = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + description = "Open-source app for all your AI conversations, fully customizable and compatible with any AI provider"; + serviceConfig = { + Type = "simple"; # FIXME + User = cfg.user; + Group = cfg.group; + PermissionsStartOnly = "true"; # run mkdir as root + ExecStartPre = [ + "${pkgs.coreutils}/bin/mkdir -p ${cfg.path}" + "${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${cfg.path}" + ]; + LoadCredential = [ + ]; }; - in - { - - assertions = [ - { - assertion = (cfg.credentials.creds_key != "") || (cfg.credentials.creds_key_file != ""); - message = "You must set either credentials.creds_key or credentials.creds_key_file."; - } - { - assertion = (cfg.credentials.creds_iv != "") || (cfg.credentials.creds_iv_file != ""); - message = "You must set either credentials.creds_iv or credentials.creds_iv_file."; - } - { - assertion = cfg.mongodbURI != ""; - message = "You must set the mongodbURI option."; - } - { - assertion = - cfg.logging.enableDebugLogging - && ( - (cfg.logging.enableConsoleLogging && !cfg.logging.enableConsoleJSONLogging) - || (!cfg.logging.enableConsoleLogging && cfg.logging.enableConsoleJSONLogging) - || (!cfg.logging.enableConsoleLogging && !cfg.logging.enableConsoleJSONLogging) - ); - message = "DEBUG_LOGGING can be used with either DEBUG_CONSOLE or CONSOLE_JSON but not both."; - } - { - assertion = (cfg.credentials.jwt_secret != "") || (cfg.credentials.jwt_secret_file != ""); - message = "You must set either credentials.jwt_secret or credentials.jwt_secret_file."; - } - { - assertion = - (cfg.credentials.jwt_refresh_secret != "") || (cfg.credentials.jwt_refresh_secret_file != ""); - message = "You must set either credentials.jwt_refresh_secret or credentials.jwt_refresh_secret_file."; - } - ]; - - networking.firewall.allowedTCPPorts = if cfg.openFirewall then [ cfg.port ] else [ ]; - systemd.services.librechat = { - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - description = "Open-source app for all your AI conversations, fully customizable and compatible with any AI provider"; - serviceConfig = { - Type = "simple"; # FIXME - User = cfg.user; - Group = cfg.group; - PermissionsStartOnly = "true"; # run mkdir as root - ExecStartPre = [ - "${pkgs.coreutils}/bin/mkdir -p ${cfg.path}" - "${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${cfg.path}" - ]; - LoadCredential = [ - #TODO: Use the creds_* options - "CREDS_KEY_FILE:${cfg.credentials.creds_key_file}" - "CREDS_IV_FILE:${cfg.credentials.creds_iv_file}" - "JWT_SECRET_FILE:${cfg.credentials.jwt_secret_file}" - "JWT_REFRESH_SECRET_FILE:${cfg.credentials.jwt_refresh_secret_file}" - ]; - }; - script = # sh - '' - # Load the systemd credentials - export CREDS_KEY=$(${pkgs.systemd}/bin/systemd-creds cat CREDS_KEY_FILE) - export CREDS_IV=$(${pkgs.systemd}/bin/systemd-creds cat CREDS_IV_FILE) - export JWT_SECRET=$(${pkgs.systemd}/bin/systemd-creds cat JWT_SECRET_FILE) - export JWT_REFRESH_SECRET=$(${pkgs.systemd}/bin/systemd-creds cat JWT_REFRESH_SECRET_FILE) - - export CONFIG_PATH=${configFile} - export HOST=${cfg.host} - export PORT=${builtins.toString cfg.port} - export MONGO_URI="${cfg.mongodbURI}" - export ALLOW_EMAIL_LOGIN=${if cfg.auth.allowEmailLogin then "true" else "false"} - export ALLOW_REGISTRATION=${if cfg.auth.allowEmailRegistration then "true" else "false"} - - cd ${cfg.path} - ${pkgs.librechat}/bin/librechat-server - ''; - }; - - 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") { }; - } - ); + script = # sh + '' + export CONFIG_PATH=${configFile} + cd ${cfg.path} + ${pkgs.librechat}/bin/librechat-server + ''; + }; + 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") { }; + }; } From ff368be300ea90dad4736e80d1cb49f6bc9b2a7e Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 31 May 2025 15:39:25 +0800 Subject: [PATCH 2/3] feat(librechat): add environment variable support --- modules/nixos/server/librechat/default.nix | 50 ++++++++++++++++++++-- systems/x86_64-linux/apollo/default.nix | 15 ++----- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/modules/nixos/server/librechat/default.nix b/modules/nixos/server/librechat/default.nix index 4bef516..cfcea43 100644 --- a/modules/nixos/server/librechat/default.nix +++ b/modules/nixos/server/librechat/default.nix @@ -10,6 +10,24 @@ let name = "librechat.yaml"; text = lib.generators.toYAML { } cfg.settings; }; + # Thanks to https://github.com/nix-community/home-manager/blob/60e4624302d956fe94d3f7d96a560d14d70591b9/modules/lib/shell.nix :) + export = n: v: ''export ${n}="${builtins.toString v}"''; + exportAll = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList export vars); + environmentVariablesFile = pkgs.writeTextFile { + name = "librechat-env-variables.sh"; + text = # sh + '' + # Thanks to https://github.com/nix-community/home-manager/blob/release-25.05/modules/home-environment.nix :) + # Only source this once. + if [ -n "$__LC_ENV_VARS_SOURCED" ]; then return; fi + export __LC_ENV_VARS_SOURCED=1 + + export CONFIG_PATH=${configFile} + ${exportAll cfg.env} + ''; + }; + allowedPorts = + if cfg.openFirewall then (if cfg.env ? PORT then [ cfg.env.port ] else [ 3080 ]) else [ ]; in { options.server.librechat = { @@ -30,6 +48,31 @@ in default = "librechat"; description = "The group to run the service as."; }; + credentials = lib.mkOption { + type = lib.types.lazyAttrsOf lib.types.path; + default = { }; + example = { + CREDS_KEY = /run/secrets/creds_key; + }; + description = "Environment variables that will be loaded in from files at runtime. See https://www.librechat.ai/docs/configuration/dotenv for a full list."; + }; + env = lib.mkOption { + type = + with lib.types; + lazyAttrsOf (oneOf [ + str + path + int + float + ]); + example = { + ALLOW_REGISTRATION = "true"; + HOST = "0.0.0.0"; + CONSOLE_JSON_STRING_LENGTH = 255; + }; + default = { }; + description = "Environment variables that will be set for the service. See https://www.librechat.ai/docs/configuration/dotenv for a full list."; + }; settings = lib.mkOption { type = lib.types.attrs; default = { }; @@ -69,7 +112,7 @@ in }; config = lib.mkIf cfg.enable { - networking.firewall.allowedTCPPorts = if cfg.openFirewall then [ cfg.port ] else [ ]; + networking.firewall.allowedTCPPorts = allowedPorts; systemd.services.librechat = { wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; @@ -83,12 +126,11 @@ in "${pkgs.coreutils}/bin/mkdir -p ${cfg.path}" "${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${cfg.path}" ]; - LoadCredential = [ - ]; + LoadCredential = [ ]; }; script = # sh '' - export CONFIG_PATH=${configFile} + source ${environmentVariablesFile} cd ${cfg.path} ${pkgs.librechat}/bin/librechat-server ''; diff --git a/systems/x86_64-linux/apollo/default.nix b/systems/x86_64-linux/apollo/default.nix index 83c2777..59375f7 100644 --- a/systems/x86_64-linux/apollo/default.nix +++ b/systems/x86_64-linux/apollo/default.nix @@ -28,8 +28,9 @@ librechat = { enable = true; openFirewall = true; - host = "0.0.0.0"; - mongodbURI = "mongodb://apollo:27017"; + env = { + TEST_ENV_VAR = "hello"; + }; settings = { version = "1.0.8"; cache = true; @@ -57,16 +58,6 @@ ]; }; }; - auth = { - allowEmailLogin = true; - allowEmailRegistration = true; - }; - credentials = { - creds_key_file = config.sops.secrets."librechat/creds_key".path; - creds_iv_file = config.sops.secrets."librechat/creds_iv".path; - jwt_secret_file = config.sops.secrets."librechat/jwt_secret".path; - jwt_refresh_secret_file = config.sops.secrets."librechat/jwt_refresh_secret".path; - }; }; }; From c5ac2a86fc30b8fc781f5928140ed6d5fbbdc996 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 31 May 2025 19:43:10 +0800 Subject: [PATCH 3/3] feat(librechat): allow setting arbitrary environment variables from text and file --- modules/nixos/server/librechat/default.nix | 16 +++++++++++----- systems/x86_64-linux/apollo/default.nix | 17 ++++++++--------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/modules/nixos/server/librechat/default.nix b/modules/nixos/server/librechat/default.nix index cfcea43..2e95421 100644 --- a/modules/nixos/server/librechat/default.nix +++ b/modules/nixos/server/librechat/default.nix @@ -13,6 +13,10 @@ let # Thanks to https://github.com/nix-community/home-manager/blob/60e4624302d956fe94d3f7d96a560d14d70591b9/modules/lib/shell.nix :) export = n: v: ''export ${n}="${builtins.toString v}"''; exportAll = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList export vars); + exportCredentials = n: _: ''export ${n}="$(${pkgs.systemd}/bin/systemd-creds cat ${n}_FILE)"''; + exportAllCredentials = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList exportCredentials vars); + transformCredential = n: v: "${n}_FILE:${v}"; + getLoadCredentialList = lib.mapAttrsToList transformCredential cfg.credentials; environmentVariablesFile = pkgs.writeTextFile { name = "librechat-env-variables.sh"; text = # sh @@ -24,6 +28,7 @@ let export CONFIG_PATH=${configFile} ${exportAll cfg.env} + ${exportAllCredentials cfg.credentials} ''; }; allowedPorts = @@ -54,7 +59,7 @@ in example = { CREDS_KEY = /run/secrets/creds_key; }; - description = "Environment variables that will be loaded in from files at runtime. See https://www.librechat.ai/docs/configuration/dotenv for a full list."; + description = "Environment variables which are loaded from the contents of files at a file paths, mainly used for secrets. See https://www.librechat.ai/docs/configuration/dotenv for a full list."; }; env = lib.mkOption { type = @@ -107,7 +112,7 @@ in }; } ''; - description = "A free-form attribute set that will be written to librechat.yaml."; + description = "A free-form attribute set that will be written to librechat.yaml. You can use environment variables by wrapping them in \${}. Take care to escape the \$ character."; }; }; @@ -118,20 +123,21 @@ in after = [ "network.target" ]; description = "Open-source app for all your AI conversations, fully customizable and compatible with any AI provider"; serviceConfig = { - Type = "simple"; # FIXME + Type = "simple"; User = cfg.user; Group = cfg.group; PermissionsStartOnly = "true"; # run mkdir as root ExecStartPre = [ "${pkgs.coreutils}/bin/mkdir -p ${cfg.path}" "${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${cfg.path}" + "${pkgs.coreutils}/bin/chmod 775 ${cfg.path}" ]; - LoadCredential = [ ]; + LoadCredential = getLoadCredentialList; }; script = # sh '' - source ${environmentVariablesFile} cd ${cfg.path} + source ${environmentVariablesFile} ${pkgs.librechat}/bin/librechat-server ''; }; diff --git a/systems/x86_64-linux/apollo/default.nix b/systems/x86_64-linux/apollo/default.nix index 59375f7..10a2988 100644 --- a/systems/x86_64-linux/apollo/default.nix +++ b/systems/x86_64-linux/apollo/default.nix @@ -29,7 +29,14 @@ enable = true; openFirewall = true; env = { - TEST_ENV_VAR = "hello"; + HOST = "0.0.0.0"; + MONGO_URI = "mongodb://apollo:27017"; + }; + credentials = { + CREDS_KEY = config.sops.secrets."librechat/creds_key".path; + CREDS_IV = config.sops.secrets."librechat/creds_iv".path; + JWT_SECRET = config.sops.secrets."librechat/jwt_secret".path; + JWT_REFRESH_SECRET = config.sops.secrets."librechat/jwt_refresh_secret".path; }; settings = { version = "1.0.8"; @@ -61,13 +68,5 @@ }; }; - environment.persistence."/persist".directories = [ - { - directory = config.server.librechat.path; - user = config.server.librechat.user; - group = config.server.librechat.group; - } - ]; - nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; }