From 78e8aff34f61bcadf81ba9d8a3c373dc40fc8901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Gro=C3=9Fmann?= Date: Fri, 26 Jan 2024 01:04:08 +0100 Subject: [PATCH] chore: flake update feat: ollama server --- flake.lock | 126 ++++++++++++------------ hosts/desktopnix/default.nix | 2 +- hosts/elisabeth/guests.nix | 26 ++++- hosts/elisabeth/secrets/ollama/host.pub | 1 + modules/config/nix.nix | 2 +- modules/optional/printing.nix | 8 +- modules/services/ollama.nix | 13 +++ pkgs/ollama-webui.nix | 62 ++++++++++++ secrets/secrets.nix.age | Bin 5169 -> 5151 bytes 9 files changed, 173 insertions(+), 67 deletions(-) create mode 100644 hosts/elisabeth/secrets/ollama/host.pub create mode 100644 modules/services/ollama.nix create mode 100644 pkgs/ollama-webui.nix diff --git a/flake.lock b/flake.lock index fd8dcbd..0d82e7c 100644 --- a/flake.lock +++ b/flake.lock @@ -324,11 +324,11 @@ ] }, "locked": { - "lastModified": 1705540973, - "narHash": "sha256-kNt/qAEy7ueV7NKbVc8YMHWiQAAgrir02MROYNI8fV0=", + "lastModified": 1706145859, + "narHash": "sha256-+iGHKwzKVW6aGAWfUmUSJW1KiE6WLYhKyTyWZMTw/cg=", "owner": "nix-community", "repo": "disko", - "rev": "0033adc6e3f1ed076f3ed1c637ef1dfe6bef6733", + "rev": "5a2dc95464080764b9ca1b82b5d6d981157522be", "type": "github" }, "original": { @@ -403,11 +403,11 @@ "flake-compat_5": { "flake": false, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "owner": "edolstra", "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { @@ -627,11 +627,11 @@ "systems": "systems_8" }, "locked": { - "lastModified": 1685518550, - "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", - "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { @@ -767,11 +767,11 @@ ] }, "locked": { - "lastModified": 1660459072, - "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", + "lastModified": 1703887061, + "narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=", "owner": "hercules-ci", "repo": "gitignore.nix", - "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", + "rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5", "type": "github" }, "original": { @@ -808,11 +808,11 @@ ] }, "locked": { - "lastModified": 1705535278, - "narHash": "sha256-V5+XKfNbiY0bLKLQlH+AXyhHttEL7XcZBH9iSbxxexA=", + "lastModified": 1706221476, + "narHash": "sha256-T4b8YafVjHXvtDY8ARec1WrXO8uyyNZOpNgv9yoQy2M=", "owner": "nix-community", "repo": "home-manager", - "rev": "b84191db127c16a92cbdf7f7b9969d58bb456699", + "rev": "c7ce343d9bf1a329056a4dd5b32ea8cc43b55e15", "type": "github" }, "original": { @@ -829,11 +829,11 @@ ] }, "locked": { - "lastModified": 1705104164, - "narHash": "sha256-pllCu3Hcm1wP/B0SUxgUXvHeEd4w8s2aVrEQRdIL1yo=", + "lastModified": 1705879479, + "narHash": "sha256-ZIohbyly1KOe+8I3gdyNKgVN/oifKdmeI0DzMfytbtg=", "owner": "nix-community", "repo": "home-manager", - "rev": "0912d26b30332ae6a90e1b321ff88e80492127dd", + "rev": "2d47379ad591bcb14ca95a90b6964b8305f6c913", "type": "github" }, "original": { @@ -850,11 +850,11 @@ ] }, "locked": { - "lastModified": 1700847865, - "narHash": "sha256-uWaOIemGl9LF813MW0AEgCBpKwFo2t1Wv3BZc6e5Frw=", + "lastModified": 1706001011, + "narHash": "sha256-J7Bs9LHdZubgNHZ6+eE/7C18lZ1P6S5/zdJSdXFItI4=", "owner": "nix-community", "repo": "home-manager", - "rev": "8cedd63eede4c22deb192f1721dd67e7460e1ebe", + "rev": "3df2a80f3f85f91ea06e5e91071fa74ba92e5084", "type": "github" }, "original": { @@ -911,11 +911,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1705423846, - "narHash": "sha256-PULm77CvMZ9cQ4MaTXgvJom2ePB9c38p39JB4TFXEdw=", + "lastModified": 1705838953, + "narHash": "sha256-bu00HScTFCapBq6r1U5QXPO7yDZhzNkGCbGfYKOHRDM=", "owner": "nix-community", "repo": "lib-aggregate", - "rev": "1d0951ca1b3721ff4e6049c3a37df56c78c60c65", + "rev": "aca52761b7d82325fadfec11ea78e01fff8f06e8", "type": "github" }, "original": { @@ -948,11 +948,11 @@ "spectrum": "spectrum" }, "locked": { - "lastModified": 1705592620, - "narHash": "sha256-97/yDm6n9C6fma0pSM/mMQeMLfmEOZPGbpKARNoKeG4=", + "lastModified": 1706214321, + "narHash": "sha256-42FZWeJQNYgz0ZkclMzShuvjT9TvJNRN78Iu3SEyD4M=", "owner": "astro", "repo": "microvm.nix", - "rev": "ccf44d60393a571b549448167fa03882693a5a3d", + "rev": "186b8bf6dbacc1ab55fe8ac8d5a2bbf76a1a70e1", "type": "github" }, "original": { @@ -969,11 +969,11 @@ ] }, "locked": { - "lastModified": 1704277720, - "narHash": "sha256-meAKNgmh3goankLGWqqpw73pm9IvXjEENJloF0coskE=", + "lastModified": 1705915768, + "narHash": "sha256-+Jlz8OAqkOwJlioac9wtpsCnjgGYUhvLpgJR/5tP9po=", "owner": "lnl7", "repo": "nix-darwin", - "rev": "0dd382b70c351f528561f71a0a7df82c9d2be9a4", + "rev": "1e706ef323de76236eb183d7784f3bd57255ec0b", "type": "github" }, "original": { @@ -1032,11 +1032,11 @@ ] }, "locked": { - "lastModified": 1705282324, - "narHash": "sha256-LnURMA7yCM5t7et9O2+2YfGQh0FKAfE5GyahNDDzJVM=", + "lastModified": 1705806513, + "narHash": "sha256-FcOmNjhHFfPz2udZbRpZ1sfyhVMr+C2O8kOxPj+HDDk=", "owner": "nix-community", "repo": "nix-index-database", - "rev": "49aaeecf41ae0a0944e2c627cb515bcde428a1d1", + "rev": "f8e04fbcebcc24cebc91989981bd45f69b963ed7", "type": "github" }, "original": { @@ -1092,11 +1092,11 @@ ] }, "locked": { - "lastModified": 1705400161, - "narHash": "sha256-0MFaNIwwpVWB1N9m7cfHAM2pSVtYESQ7tlHxnDTOhM4=", + "lastModified": 1706085261, + "narHash": "sha256-7PgpHRHyShINcqgevPP1fJ6N8kM5ZSOJnk3QZBrOCQ0=", "owner": "nix-community", "repo": "nixos-generators", - "rev": "521fb4cdd8a2e1a00d1adf0fea7135d1faf04234", + "rev": "896f6589db5b25023b812bbb6c1f5d3a499b1132", "type": "github" }, "original": { @@ -1107,11 +1107,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1705312285, - "narHash": "sha256-rd+dY+v61Y8w3u9bukO/hB55Xl4wXv4/yC8rCGVnK5U=", + "lastModified": 1706182238, + "narHash": "sha256-Ti7CerGydU7xyrP/ow85lHsOpf+XMx98kQnPoQCSi1g=", "owner": "nixos", "repo": "nixos-hardware", - "rev": "bee2202bec57e521e3bd8acd526884b9767d7fa0", + "rev": "f84eaffc35d1a655e84749228cde19922fcf55f1", "type": "github" }, "original": { @@ -1122,11 +1122,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1705496572, - "narHash": "sha256-rPIe9G5EBLXdBdn9ilGc0nq082lzQd0xGGe092R/5QE=", + "lastModified": 1705856552, + "narHash": "sha256-JXfnuEf5Yd6bhMs/uvM67/joxYKoysyE3M2k6T3eWbg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "842d9d80cfd4560648c785f8a4e6f3b096790e19", + "rev": "612f97239e2cc474c13c9dafa0df378058c5ad8d", "type": "github" }, "original": { @@ -1138,11 +1138,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1705193289, - "narHash": "sha256-oL5EAaZHiA3ABLdyKag/DgT+457vmELv8A+eaox2xsI=", + "lastModified": 1705798119, + "narHash": "sha256-WPVKxYMcvGW/2X16pfF1ef05EQ0Ql5XPCxqoCDlQSrY=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "da839f74dc77c9826fa333b1bc2c8258fd6ffcbe", + "rev": "a26fc04e3d43acfa1dc52065a4ce39ca7a2ec91c", "type": "github" }, "original": { @@ -1243,11 +1243,11 @@ ] }, "locked": { - "lastModified": 1705585910, - "narHash": "sha256-5pvcEdTiVn5F+6gpyQbTxeLhcRlV/oN8nNiwjgLqigs=", + "lastModified": 1706195865, + "narHash": "sha256-yJ++qYtmG6zeVLMJ7RzlADCq7F2tdoTPYMEN9hv3TKE=", "owner": "nix-community", "repo": "nixpkgs-wayland", - "rev": "5b2b874c87882a5fc7f30be353410432e685ca0d", + "rev": "f64c8b95825425c9bdfdc76cf200aacaaf403873", "type": "github" }, "original": { @@ -1331,11 +1331,11 @@ "pre-commit-hooks": "pre-commit-hooks_3" }, "locked": { - "lastModified": 1705581923, - "narHash": "sha256-ms+6X+Sbx7Je8vMzux4ricuUR6JNHGoMZJLqhjGLxn8=", + "lastModified": 1706198703, + "narHash": "sha256-7INiYw039cf5202QxnIlOVXx+QMI8qsUGzbg5mnFSF4=", "owner": "nix-community", "repo": "nixvim", - "rev": "df7a90127b079a39bfaba3eae1885ce6ab3a062a", + "rev": "7164a89f72c28305e9ee7833220913d27aca9bd4", "type": "github" }, "original": { @@ -1446,11 +1446,11 @@ ] }, "locked": { - "lastModified": 1705072518, - "narHash": "sha256-90dERRuG781f0EWjn2AOtScZqsTcpIFLpY8TN2VbkL8=", + "lastModified": 1705757126, + "narHash": "sha256-Eksr+n4Q8EYZKAN0Scef5JK4H6FcHc+TKNHb95CWm+c=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "274ae3979a0eacae422e1bbcf63b8b7a335e1114", + "rev": "f56597d53fd174f796b5a7d3ee0b494f9e2285cc", "type": "github" }, "original": { @@ -1472,11 +1472,11 @@ "nixpkgs-stable": "nixpkgs-stable_4" }, "locked": { - "lastModified": 1705229514, - "narHash": "sha256-itILy0zimR/iyUGq5Dgg0fiW8plRDyxF153LWGsg3Cw=", + "lastModified": 1705757126, + "narHash": "sha256-Eksr+n4Q8EYZKAN0Scef5JK4H6FcHc+TKNHb95CWm+c=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "ffa9a5b90b0acfaa03b1533b83eaf5dead819a05", + "rev": "f56597d53fd174f796b5a7d3ee0b494f9e2285cc", "type": "github" }, "original": { @@ -1605,11 +1605,11 @@ "nixpkgs": "nixpkgs_4" }, "locked": { - "lastModified": 1705504375, - "narHash": "sha256-oRVxuJ6sCljsgfoWb+SsIK2MvUjsxrXQHRoVTUDVC40=", + "lastModified": 1706172305, + "narHash": "sha256-9VXEpF+wFyVNmUAMyGFPqXCSTAa+oXEkwm2Fe0Oq/JM=", "owner": "danth", "repo": "stylix", - "rev": "2d59480b4531ce8d062d20a42560a266cb42b9d0", + "rev": "1a5dee1957dc45e125013ae3919ff284cfb83cdc", "type": "github" }, "original": { @@ -1814,11 +1814,11 @@ "rust-overlay": "rust-overlay_2" }, "locked": { - "lastModified": 1705487953, - "narHash": "sha256-6oh1H7/74v57m3AtK8jQLvN9LtKqyeT862krjJasOJs=", + "lastModified": 1706214060, + "narHash": "sha256-P2AyxPfn8+nfFB3xKUikd6fTN8jVl5/ZLV1gsz5eiT0=", "owner": "Toqozz", "repo": "wired-notify", - "rev": "fe0f02af93b09e5fe689c948a557e466b99d9a58", + "rev": "d65f227779061299842b4e1891954c4843ee2750", "type": "github" }, "original": { diff --git a/hosts/desktopnix/default.nix b/hosts/desktopnix/default.nix index f8f5a47..8b1f8f9 100644 --- a/hosts/desktopnix/default.nix +++ b/hosts/desktopnix/default.nix @@ -40,7 +40,7 @@ xkbVariant = "bone"; }; virtualisation.podman = { - enable = false; + enable = true; dockerCompat = true; }; diff --git a/hosts/elisabeth/guests.nix b/hosts/elisabeth/guests.nix index bd0f7f4..58849a5 100644 --- a/hosts/elisabeth/guests.nix +++ b/hosts/elisabeth/guests.nix @@ -13,6 +13,7 @@ vaultwardendomain = "pw.${config.secrets.secrets.global.domains.web}"; paperlessdomain = "ppl.${config.secrets.secrets.global.domains.web}"; immichdomain = "immich.${config.secrets.secrets.global.domains.web}"; + ollamadomain = "ollama.${config.secrets.secrets.global.domains.web}"; ipOf = hostName: lib.net.cidr.host config.secrets.secrets.global.net.ips."${config.guests.${hostName}.nodeName}" config.secrets.secrets.global.net.privateSubnet; in { services.nginx = { @@ -79,6 +80,27 @@ in { ''; }; + upstreams.ollama = { + servers."${ipOf "ollama"}:3000" = {}; + + extraConfig = '' + zone ollama 64k ; + keepalive 5 ; + ''; + }; + virtualHosts.${ollamadomain} = { + forceSSL = true; + useACMEHost = "web"; + locations."/" = { + proxyPass = "http://ollama"; + proxyWebsockets = true; + }; + extraConfig = '' + allow ${config.secrets.secrets.global.net.privateSubnet}; + deny all; + ''; + }; + upstreams.adguardhome = { servers."${ipOf "adguardhome"}:3000" = {}; @@ -95,10 +117,11 @@ in { proxyWebsockets = true; }; extraConfig = '' - allow 192.168.178.0/24; + allow ${config.secrets.secrets.global.net.privateSubnet}; deny all; ''; }; + upstreams.paperless = { servers."${ipOf "paperless"}:3000" = {}; @@ -223,6 +246,7 @@ in { // mkContainer "adguardhome" {} // mkContainer "vaultwarden" {} // mkContainer "ddclient" {} + // mkContainer "ollama" {} // mkContainer "nextcloud" { enablePanzer = true; } diff --git a/hosts/elisabeth/secrets/ollama/host.pub b/hosts/elisabeth/secrets/ollama/host.pub new file mode 100644 index 0000000..c94f7fb --- /dev/null +++ b/hosts/elisabeth/secrets/ollama/host.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHD5D88M/eb/uW/i8vYn3pkVbn3rLBJiO/qsckKA6ALJ diff --git a/modules/config/nix.nix b/modules/config/nix.nix index d9e757e..c148189 100644 --- a/modules/config/nix.nix +++ b/modules/config/nix.nix @@ -42,7 +42,7 @@ optimise.automatic = true; gc = { automatic = true; - dates = "daily"; + dates = "monthly"; }; registry = { diff --git a/modules/optional/printing.nix b/modules/optional/printing.nix index 4dfaf9c..17ee20f 100644 --- a/modules/optional/printing.nix +++ b/modules/optional/printing.nix @@ -1,6 +1,12 @@ {pkgs, ...}: { services.printing = { enable = true; - drivers = [pkgs.hplipWithPlugin]; + drivers = [pkgs.hplipWithPlugin pkgs.hplip]; }; + environment.persistence."/state".directories = [ + { + directory = "/var/lib/cups"; + mode = "755"; + } + ]; } diff --git a/modules/services/ollama.nix b/modules/services/ollama.nix new file mode 100644 index 0000000..eafdf7b --- /dev/null +++ b/modules/services/ollama.nix @@ -0,0 +1,13 @@ +{ + networking.firewall.allowedTCPPorts = [11434]; + services.ollama = { + listenAddress = "0.0.0.0:11434"; + enable = true; + }; + environment.persistence."/state".directories = [ + { + directory = "/var/lib/private/ollama"; + mode = "0700"; + } + ]; +} diff --git a/pkgs/ollama-webui.nix b/pkgs/ollama-webui.nix new file mode 100644 index 0000000..5330959 --- /dev/null +++ b/pkgs/ollama-webui.nix @@ -0,0 +1,62 @@ +{ + lib, + buildNpmPackage, + nodePackages, + fetchFromGitHub, + runtimeShell, +}: +# We just package the JS frontend part, not the Python reverse-proxy backend. +# NixOS can provide any another reverse proxy such as nginx. +buildNpmPackage rec { + pname = "ollama-webui"; + # ollama-webui doesn't tag versions yet. + version = "0.0.0-unstable-2023-12-22"; + + src = fetchFromGitHub { + owner = "ollama-webui"; + repo = "ollama-webui"; + rev = "77c1a77fccb04337ff95440030cd051fd16c2cd8"; + hash = "sha256-u7h2tpHgtQwYXornslY3CZjKjigqBK2mHmaiK1EoEgk="; + }; + # dependencies are downloaded into a separate node_modules Nix package + npmDepsHash = "sha256-SI2dPn1SwbGwl8093VBtcDsA2eHSxr3UUC+ta68w2t8="; + + # We have to bake in the default URL it will use for ollama webserver here, + # but it can be overriden in the UI later. + PUBLIC_API_BASE_URL = "http://localhost:11434/api"; + + # The path '/ollama/api' will be redirected to the specified backend URL + OLLAMA_API_BASE_URL = PUBLIC_API_BASE_URL; + # "npm run build" creates a static page in the "build" folder. + installPhase = '' + mkdir -p $out/lib + cp -R ./build/. $out/lib + + mkdir -p $out/bin + cat <>$out/bin/${pname} + #!${runtimeShell} + ${nodePackages.http-server}/bin/http-server $out/lib "\$@" + EOF + chmod +x $out/bin/${pname} + ''; + + meta = with lib; { + description = "ChatGPT-Style Web Interface for Ollama"; + longDescription = '' + Tools like Ollama make open-source large langue models (LLM) accessible and almost + trivial to download and run them locally on a consumer computer. + However, Ollama only runs in a terminal and doesn't store any chat history. + Ollama-WebUI is a web frontend on top of Ollama that looks and behaves similar to ChatGPT's web frontend. + You can have separate chats with different LLMs that are saved in your browser, + automatic Markdown and Latex rendering, upload files etc. + This package contains two parts: + - `/lib` The WebUI as a compiled, static html folder to bundle in your web server + - `/bin/${pname}` A runnable webserver the serves the WebUI for convenience. + ''; + homepage = "https://github.com/ollama-webui/ollama-webui"; + license = licenses.mit; + mainProgram = pname; + maintainers = with maintainers; [malteneuss]; + platforms = platforms.all; + }; +} diff --git a/secrets/secrets.nix.age b/secrets/secrets.nix.age index 96f06fdf0d1d734fab435e5e0203f277c14186f6..1d066a76a4d5546b7cda0aba23be28a3ed5c5ade 100644 GIT binary patch delta 5148 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