diff --git a/modules/nixos/librechat-oci/default.nix b/modules/nixos/librechat-oci/default.nix index 7dde06a..625df85 100644 --- a/modules/nixos/librechat-oci/default.nix +++ b/modules/nixos/librechat-oci/default.nix @@ -8,85 +8,223 @@ let cfg = config.services.librechat-oci; - image = pkgs.dockerTools.pullImage { - imageName = "ghcr.io/danny-avila/librechat"; - imageDigest = "sha256:a46254938507971e0d4f7ed3f9d116bd9b118f4810b5b75eb716baf575645068"; - hash = "sha256-zevUN6vrs3hymwCGFmk/YXlUzYjN37H+EO5aLxYchyc="; - finalImageName = "ghcr.io/danny-avila/librechat"; - finalImageTag = "v0.8.5"; - }; + defaultImages = { + librechat = pkgs.dockerTools.pullImage { + imageName = "ghcr.io/danny-avila/librechat"; + imageDigest = "sha256:a46254938507971e0d4f7ed3f9d116bd9b118f4810b5b75eb716baf575645068"; + hash = "sha256-zevUN6vrs3hymwCGFmk/YXlUzYjN37H+EO5aLxYchyc="; + finalImageName = "ghcr.io/danny-avila/librechat"; + finalImageTag = "v0.8.5"; + }; - meiliImage = pkgs.dockerTools.pullImage { - imageName = "getmeili/meilisearch"; - imageDigest = "sha256:b839a48d008d4e67e57f78dcff5b21d5e8b8fa066bacd11f97824d6307abb0dd"; - hash = "sha256-+hypvjFDSdnnWAO4tARTnjNd/6HlF6pMg1L6UUffdYM="; - finalImageName = "getmeili/meilisearch"; - finalImageTag = "v1.44.0"; + meilisearch = pkgs.dockerTools.pullImage { + imageName = "getmeili/meilisearch"; + imageDigest = "sha256:b839a48d008d4e67e57f78dcff5b21d5e8b8fa066bacd11f97824d6307abb0dd"; + hash = "sha256-+hypvjFDSdnnWAO4tARTnjNd/6HlF6pMg1L6UUffdYM="; + finalImageName = "getmeili/meilisearch"; + finalImageTag = "v1.44.0"; + }; + + mongodb = pkgs.dockerTools.pullImage { + imageName = "mongo"; + imageDigest = "sha256:098862b1339f031900ca66cf8fef799e616d6324fa41b9a263f2ec899552c1ef"; + hash = "sha256-XuWnvcqAAkAGshdQtnngKOOJP2Bd33FXTbGHTRX3nUc="; + finalImageName = "mongo"; + finalImageTag = "8.0.20"; + }; + + vectordb = pkgs.dockerTools.pullImage { + imageName = "pgvector/pgvector"; + imageDigest = "sha256:8809cfffff0082cf260c9ac752f1dd1afc77f6f0a55c4e6411321e78efc3d9a5"; + hash = "sha256-rc6gQLMzv8UOZVLmWKGUESNIo+iPf5DR7T79AmbzWc4="; + finalImageName = "pgvector/pgvector"; + finalImageTag = "0.8.0-pg15-trixie"; + }; + + ragApi = pkgs.dockerTools.pullImage { + imageName = "registry.librechat.ai/danny-avila/librechat-rag-api-dev-lite"; + imageDigest = "sha256:6dfb6832661ff9c26fa329c823ce266059e33567670a763e9ecb9b566b8daa68"; + hash = "sha256-k8pkEgbqT4NU2+2ZjdRFlfFvMUk/1p+pkysbELh95pM="; + finalImageName = "registry.librechat.ai/danny-avila/librechat-rag-api-dev-lite"; + finalImageTag = "latest"; + }; }; defaultEnv = { + # Server HOST = "0.0.0.0"; - PORT = "3080"; + PORT = toString cfg.port; NO_INDEX = "true"; - DEBUG_LOGGING = "false"; + TRUST_PROXY = "1"; + + # Logging CONSOLE_JSON = "false"; - ALLOW_REGISTRATION = "true"; - ALLOW_EMAIL_LOGIN = "true"; + DEBUG_LOGGING = "true"; + DEBUG_CONSOLE = "false"; + AGENT_DEBUG_LOGGING = "false"; + DEBUG_OPENAI = "false"; + + # Node + NODE_MAX_OLD_SPACE_SIZE = "6144"; + + # Search SEARCH = "true"; MEILI_NO_ANALYTICS = "true"; + + # Moderation + OPENAI_MODERATION = "false"; + BAN_VIOLATIONS = "true"; + BAN_DURATION = "1000 * 60 * 60 * 2"; + BAN_INTERVAL = "20"; + LOGIN_VIOLATION_SCORE = "1"; + REGISTRATION_VIOLATION_SCORE = "1"; + CONCURRENT_VIOLATION_SCORE = "1"; + MESSAGE_VIOLATION_SCORE = "1"; + NON_BROWSER_VIOLATION_SCORE = "20"; + TTS_VIOLATION_SCORE = "0"; + STT_VIOLATION_SCORE = "0"; + FORK_VIOLATION_SCORE = "0"; + IMPORT_VIOLATION_SCORE = "0"; + FILE_UPLOAD_VIOLATION_SCORE = "0"; + LOGIN_MAX = "7"; + LOGIN_WINDOW = "5"; + REGISTER_MAX = "5"; + REGISTER_WINDOW = "60"; + LIMIT_CONCURRENT_MESSAGES = "true"; + CONCURRENT_MESSAGE_MAX = "2"; + LIMIT_MESSAGE_IP = "true"; + MESSAGE_IP_MAX = "40"; + MESSAGE_IP_WINDOW = "1"; + LIMIT_MESSAGE_USER = "false"; + MESSAGE_USER_MAX = "40"; + MESSAGE_USER_WINDOW = "1"; + ILLEGAL_MODEL_REQ_SCORE = "5"; + + # Registration and login + ALLOW_EMAIL_LOGIN = "true"; + ALLOW_REGISTRATION = "false"; + ALLOW_SOCIAL_LOGIN = "false"; + ALLOW_SOCIAL_REGISTRATION = "false"; + ALLOW_PASSWORD_RESET = "false"; + ALLOW_UNVERIFIED_EMAIL_LOGIN = "true"; + SESSION_EXPIRY = "1000 * 60 * 15"; + REFRESH_TOKEN_EXPIRY = "(1000 * 60 * 60 * 24) * 7"; + + # OpenID + OPENID_SCOPE = "openid profile email"; + OPENID_CALLBACK_URL = "/oauth/openid/callback"; + OPENID_AUTO_REDIRECT = "false"; + OPENID_USE_PKCE = "false"; + OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE = "user.read"; + + # OAuth callback URLs + DISCORD_CALLBACK_URL = "/oauth/discord/callback"; + FACEBOOK_CALLBACK_URL = "/oauth/facebook/callback"; + GITHUB_CALLBACK_URL = "/oauth/github/callback"; + GOOGLE_CALLBACK_URL = "/oauth/google/callback"; + APPLE_CALLBACK_URL = "/oauth/apple/callback"; + SAML_CALLBACK_URL = "/oauth/saml/callback"; + + # Entra ID + USE_ENTRA_ID_FOR_PEOPLE_SEARCH = "false"; + ENTRA_ID_INCLUDE_OWNERS_AS_MEMBERS = "false"; + OPENID_GRAPH_SCOPES = "User.Read,People.Read,GroupMember.Read.All"; + + # Shared links + ALLOW_SHARED_LINKS = "true"; + ALLOW_SHARED_LINKS_PUBLIC = "false"; + + # UI + APP_TITLE = "LibreChat"; + HELP_AND_FAQ_URL = "https://librechat.ai"; + + # Flux + FLUX_API_BASE_URL = "https://api.us1.bfl.ai"; + + # Email + EMAIL_PORT = "25"; + EMAIL_FROM = "noreply@librechat.ai"; + + # Azure Blob Storage + AZURE_STORAGE_PUBLIC_ACCESS = "false"; + AZURE_CONTAINER_NAME = "files"; }; + mkImageOption = + name: description: + mkOption { + type = types.package; + default = defaultImages.${name}; + description = description; + }; + inherit (lib) literalExpression mkEnableOption mkIf mkOption mkOverride + optional types ; in { options.services.librechat-oci = { enable = mkEnableOption "LibreChat container with Podman."; - image = mkOption { - type = types.package; - default = image; - description = "The Docker image to use (`pkgs.dockerTools.pullImage`)."; - }; - meiliImage = mkOption { - type = types.package; - default = meiliImage; - description = "The Meilisearch Docker image to use (`pkgs.dockerTools.pullImage`)."; + + images = { + librechat = mkImageOption "librechat" "The LibreChat Docker image (`pkgs.dockerTools.pullImage`)."; + meilisearch = mkImageOption "meilisearch" "The Meilisearch Docker image (`pkgs.dockerTools.pullImage`)."; + mongodb = mkImageOption "mongodb" "The MongoDB Docker image (`pkgs.dockerTools.pullImage`)."; + vectordb = mkImageOption "vectordb" "The pgvector Docker image (`pkgs.dockerTools.pullImage`)."; + ragApi = mkImageOption "ragApi" "The LibreChat RAG API Docker image (`pkgs.dockerTools.pullImage`)."; }; + externalUrl = mkOption { type = types.nullOr types.str; default = null; - example = literalExpression "http://${config.networking.domain}"; - description = "Public URL to configure for LibreChat."; + example = literalExpression ''"https://chat.example.com"''; + description = "Public URL to configure for LibreChat (sets DOMAIN_CLIENT and DOMAIN_SERVER)."; }; + port = mkOption { type = types.port; default = 3080; - description = "Which port the LibreChat server listens to."; + description = "Which port the LibreChat server listens on."; }; + meiliPort = mkOption { type = types.port; default = 7700; - description = "Which port Meilisearch listens to."; + description = "Which port Meilisearch listens on."; }; + + ragPort = mkOption { + type = types.port; + default = 8000; + description = "Which port the RAG API listens on."; + }; + environment = mkOption { default = { }; type = types.attrsOf types.str; description = '' Extra environment variables for LibreChat. - For more details see + These are merged on top of the defaults and can override them. + For secrets use instead. + See . ''; }; + environmentFile = mkOption { - description = "Environment file to be passed to the LibreChat container."; type = types.nullOr types.path; default = null; - example = "config.sops.templates.librechat-env.path"; + example = literalExpression "config.sops.templates.librechat-env.path"; + description = '' + Environment file passed to the LibreChat, Meilisearch, and RAG API + containers. Use this for secrets such as JWT_SECRET, CREDS_KEY, + MEILI_MASTER_KEY, and API keys. + ''; }; }; @@ -107,209 +245,273 @@ in virtualisation.oci-containers.backend = "podman"; - virtualisation.oci-containers.containers."librechat-mongodb" = { - image = "mongo:7.0"; - environment = { - MONGO_INITDB_ROOT_USERNAME = "root"; - MONGO_INITDB_ROOT_PASSWORD = "librechat"; - MONGO_INITDB_DATABASE = "LibreChat"; - }; + virtualisation.oci-containers.containers.librechat-mongodb = { + image = with cfg.images.mongodb; "${imageName}:${imageTag}"; + imageFile = cfg.images.mongodb; + cmd = [ + "mongod" + "--noauth" + ]; volumes = [ "librechat_mongodb_data:/data/db:rw" ]; log-driver = "journald"; extraOptions = [ - "--network=host" + "--network-alias=mongodb" + "--network=librechat_default" ]; }; - virtualisation.oci-containers.containers."librechat-meilisearch" = { - image = with cfg.meiliImage; imageName + ":" + imageTag; - imageFile = cfg.meiliImage; + virtualisation.oci-containers.containers.librechat-meilisearch = { + image = with cfg.images.meilisearch; "${imageName}:${imageTag}"; + imageFile = cfg.images.meilisearch; environment = { MEILI_NO_ANALYTICS = "true"; + MEILI_HOST = "http://meilisearch:${toString cfg.meiliPort}"; }; + environmentFiles = optional (cfg.environmentFile != null) cfg.environmentFile; volumes = [ "librechat_meili_data:/meili_data:rw" ]; log-driver = "journald"; extraOptions = [ - "--network=host" + "--network-alias=meilisearch" + "--network=librechat_default" + ]; + }; + + virtualisation.oci-containers.containers.librechat-vectordb = { + image = with cfg.images.vectordb; "${imageName}:${imageTag}"; + imageFile = cfg.images.vectordb; + environment = { + POSTGRES_DB = "mydatabase"; + POSTGRES_USER = "myuser"; + POSTGRES_PASSWORD = "mypassword"; + }; + volumes = [ + "librechat_pgdata:/var/lib/postgresql/data:rw" + ]; + log-driver = "journald"; + extraOptions = [ + "--network-alias=vectordb" + "--network=librechat_default" + ]; + }; + + virtualisation.oci-containers.containers.librechat-rag-api = { + image = with cfg.images.ragApi; "${imageName}:${imageTag}"; + imageFile = cfg.images.ragApi; + environment = { + DB_HOST = "vectordb"; + RAG_PORT = toString cfg.ragPort; + }; + environmentFiles = optional (cfg.environmentFile != null) cfg.environmentFile; + dependsOn = [ "librechat-vectordb" ]; + log-driver = "journald"; + extraOptions = [ + "--network-alias=rag_api" + "--network=librechat_default" ]; }; virtualisation.oci-containers.containers.librechat = { - image = with cfg.image; imageName + ":" + imageTag; - imageFile = cfg.image; + image = with cfg.images.librechat; "${imageName}:${imageTag}"; + imageFile = cfg.images.librechat; environment = defaultEnv // { - MONGO_URI = "mongodb://root:librechat@localhost:27017/LibreChat?authSource=admin"; - MEILI_HOST = "http://localhost:${toString cfg.meiliPort}"; + MONGO_URI = "mongodb://mongodb:27017/LibreChat"; + MEILI_HOST = "http://meilisearch:${toString cfg.meiliPort}"; + RAG_PORT = toString cfg.ragPort; + RAG_API_URL = "http://rag_api:${toString cfg.ragPort}"; + DOMAIN_CLIENT = + if cfg.externalUrl != null then cfg.externalUrl else "http://localhost:${toString cfg.port}"; + DOMAIN_SERVER = + if cfg.externalUrl != null then cfg.externalUrl else "http://localhost:${toString cfg.port}"; } // cfg.environment; - environmentFiles = lib.optional (cfg.environmentFile != null) cfg.environmentFile; + environmentFiles = optional (cfg.environmentFile != null) cfg.environmentFile; volumes = [ - "librechat_data:/app/client/data:rw" "librechat_images:/app/client/public/images:rw" - "librechat_uploads:/app/api/server/files/uploads:rw" + "librechat_uploads:/app/uploads:rw" "librechat_logs:/app/logs:rw" ]; ports = [ - "${toString cfg.port}:${toString cfg.port}" + "127.0.0.1:${toString cfg.port}:${toString cfg.port}/tcp" + ]; + dependsOn = [ + "librechat-mongodb" + "librechat-rag-api" ]; log-driver = "journald"; extraOptions = [ - "--network=host" + "--network-alias=api" + "--network=librechat_default" ]; }; - systemd.services."podman-librechat-mongodb" = { - serviceConfig = { - Restart = mkOverride 90 "always"; - }; + systemd.services.podman-librechat-mongodb = { + serviceConfig.Restart = mkOverride 90 "always"; after = [ + "podman-network-librechat_default.service" "podman-volume-librechat_mongodb_data.service" ]; requires = [ + "podman-network-librechat_default.service" "podman-volume-librechat_mongodb_data.service" ]; - partOf = [ - "podman-compose-librechat-root.target" - ]; - wantedBy = [ - "podman-compose-librechat-root.target" - ]; + partOf = [ "podman-compose-librechat-root.target" ]; + wantedBy = [ "podman-compose-librechat-root.target" ]; }; - systemd.services."podman-librechat-meilisearch" = { - serviceConfig = { - Restart = mkOverride 90 "always"; - }; + systemd.services.podman-librechat-meilisearch = { + serviceConfig.Restart = mkOverride 90 "always"; after = [ + "podman-network-librechat_default.service" "podman-volume-librechat_meili_data.service" ]; requires = [ + "podman-network-librechat_default.service" "podman-volume-librechat_meili_data.service" ]; - partOf = [ - "podman-compose-librechat-root.target" - ]; - wantedBy = [ - "podman-compose-librechat-root.target" - ]; + partOf = [ "podman-compose-librechat-root.target" ]; + wantedBy = [ "podman-compose-librechat-root.target" ]; }; - systemd.services."podman-librechat" = { - serviceConfig = { - Restart = mkOverride 90 "always"; - }; + systemd.services.podman-librechat-vectordb = { + serviceConfig.Restart = mkOverride 90 "always"; after = [ - "podman-volume-librechat_data.service" + "podman-network-librechat_default.service" + "podman-volume-librechat_pgdata.service" + ]; + requires = [ + "podman-network-librechat_default.service" + "podman-volume-librechat_pgdata.service" + ]; + partOf = [ "podman-compose-librechat-root.target" ]; + wantedBy = [ "podman-compose-librechat-root.target" ]; + }; + + systemd.services.podman-librechat-rag-api = { + serviceConfig.Restart = mkOverride 90 "always"; + after = [ + "podman-network-librechat_default.service" + "podman-librechat-vectordb.service" + ]; + requires = [ + "podman-network-librechat_default.service" + "podman-librechat-vectordb.service" + ]; + partOf = [ "podman-compose-librechat-root.target" ]; + wantedBy = [ "podman-compose-librechat-root.target" ]; + }; + + systemd.services.podman-librechat = { + serviceConfig.Restart = mkOverride 90 "always"; + after = [ + "podman-network-librechat_default.service" "podman-volume-librechat_images.service" "podman-volume-librechat_uploads.service" "podman-volume-librechat_logs.service" "podman-librechat-mongodb.service" "podman-librechat-meilisearch.service" + "podman-librechat-rag-api.service" ]; requires = [ - "podman-volume-librechat_data.service" + "podman-network-librechat_default.service" "podman-volume-librechat_images.service" "podman-volume-librechat_uploads.service" "podman-volume-librechat_logs.service" "podman-librechat-mongodb.service" "podman-librechat-meilisearch.service" + "podman-librechat-rag-api.service" ]; - partOf = [ - "podman-compose-librechat-root.target" - ]; - wantedBy = [ - "podman-compose-librechat-root.target" - ]; + partOf = [ "podman-compose-librechat-root.target" ]; + wantedBy = [ "podman-compose-librechat-root.target" ]; }; - systemd.services."podman-volume-librechat_data" = { + systemd.services.podman-network-librechat_default = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; + ExecStop = "podman network rm -f librechat_default"; }; script = '' - podman volume inspect librechat_data || podman volume create librechat_data + podman network inspect librechat_default || podman network create librechat_default ''; partOf = [ "podman-compose-librechat-root.target" ]; wantedBy = [ "podman-compose-librechat-root.target" ]; }; - systemd.services."podman-volume-librechat_images" = { + systemd.services.podman-volume-librechat_mongodb_data = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; - script = '' - podman volume inspect librechat_images || podman volume create librechat_images - ''; + script = "podman volume inspect librechat_mongodb_data || podman volume create librechat_mongodb_data"; partOf = [ "podman-compose-librechat-root.target" ]; wantedBy = [ "podman-compose-librechat-root.target" ]; }; - systemd.services."podman-volume-librechat_uploads" = { + systemd.services.podman-volume-librechat_meili_data = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; - script = '' - podman volume inspect librechat_uploads || podman volume create librechat_uploads - ''; + script = "podman volume inspect librechat_meili_data || podman volume create librechat_meili_data"; partOf = [ "podman-compose-librechat-root.target" ]; wantedBy = [ "podman-compose-librechat-root.target" ]; }; - systemd.services."podman-volume-librechat_logs" = { + systemd.services.podman-volume-librechat_pgdata = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; - script = '' - podman volume inspect librechat_logs || podman volume create librechat_logs - ''; + script = "podman volume inspect librechat_pgdata || podman volume create librechat_pgdata"; partOf = [ "podman-compose-librechat-root.target" ]; wantedBy = [ "podman-compose-librechat-root.target" ]; }; - systemd.services."podman-volume-librechat_mongodb_data" = { + systemd.services.podman-volume-librechat_images = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; - script = '' - podman volume inspect librechat_mongodb_data || podman volume create librechat_mongodb_data - ''; + script = "podman volume inspect librechat_images || podman volume create librechat_images"; partOf = [ "podman-compose-librechat-root.target" ]; wantedBy = [ "podman-compose-librechat-root.target" ]; }; - systemd.services."podman-volume-librechat_meili_data" = { + systemd.services.podman-volume-librechat_uploads = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; - script = '' - podman volume inspect librechat_meili_data || podman volume create librechat_meili_data - ''; + script = "podman volume inspect librechat_uploads || podman volume create librechat_uploads"; partOf = [ "podman-compose-librechat-root.target" ]; wantedBy = [ "podman-compose-librechat-root.target" ]; }; - systemd.targets."podman-compose-librechat-root" = { - unitConfig = { - Description = "Root target generated by compose2nix."; + systemd.services.podman-volume-librechat_logs = { + path = [ pkgs.podman ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; }; + script = "podman volume inspect librechat_logs || podman volume create librechat_logs"; + partOf = [ "podman-compose-librechat-root.target" ]; + wantedBy = [ "podman-compose-librechat-root.target" ]; + }; + + systemd.targets.podman-compose-librechat-root = { + unitConfig.Description = "Root target generated by compose2nix."; wantedBy = [ "multi-user.target" ]; }; };