From 6a364f5ce9a1d7bc89373bd9138b0f3a0008f7e2 Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 12 Sep 2026 21:17:36 -0400 Subject: [PATCH] feat(nixos): migrate navidrome to pikachu --- configs/nixos/guests/navidrome.nix | 95 ++++++++++++++++++++ configs/nixos/guests/reverse-proxy.nix | 5 -- configs/nixos/hosts/pikachu/default.nix | 16 ++++ configs/nixos/modules/services/navidrome.nix | 64 +++++++++++++ configs/nixos/registry/stable-uid.nix | 2 +- configs/nixos/secrets/pikachu.yaml | 9 +- docs/TODO.md | 2 +- 7 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 configs/nixos/guests/navidrome.nix create mode 100644 configs/nixos/modules/services/navidrome.nix diff --git a/configs/nixos/guests/navidrome.nix b/configs/nixos/guests/navidrome.nix new file mode 100644 index 0000000..fa66667 --- /dev/null +++ b/configs/nixos/guests/navidrome.nix @@ -0,0 +1,95 @@ +{ + config, + lib, + ... +}: +let + guestCfg = config.guests.navidrome; + serviceCfg = config.containers.navidrome.config.modules.services.navidrome; +in +{ + options.guests.navidrome = { + dataPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/srv/navidrome"; + description = "Host path mounted as Navidrome's data folder."; + }; + + libraryPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/srv/music"; + description = "Host path mounted as Navidrome's library folder."; + }; + + lastfmApiKeySecret = lib.mkOption { + type = lib.types.str; + default = "services/navidrome/lastfm/api-key"; + description = "SOPS secret name containing the Navidrome Last.fm API key."; + }; + + lastfmSecretSecret = lib.mkOption { + type = lib.types.str; + default = "services/navidrome/lastfm/secret"; + description = "SOPS secret name containing the Navidrome Last.fm secret."; + }; + }; + + config = lib.mkIf guestCfg.enable { + assertions = [ + { + assertion = guestCfg.dataPath != null; + message = "guests.navidrome.dataPath must point at the host directory that stores Navidrome data."; + } + { + assertion = guestCfg.libraryPath != null; + message = "guests.navidrome.libraryPath must point at the host directory that stores Navidrome's library folder."; + } + ]; + + guests.navidrome = { + interior.modules.services.navidrome.enable = true; + + mounts = { + ${serviceCfg.dataFolder} = { + directory = guestCfg.dataPath; + owner = config.registry."stable-uid".${config.networking.hostName}.navidrome; + group = "storage"; + mode = "2770"; + }; + ${serviceCfg.musicFolder} = { + directory = guestCfg.libraryPath; + owner = "root"; + group = "storage"; + mode = "2770"; + }; + ${serviceCfg.environmentFile} = { + file = config.sops.templates."navidrome.env".path; + readOnly = true; + }; + }; + + limits = { + memory = lib.mkDefault "2G"; + cpu = lib.mkDefault "200%"; + tasksMax = lib.mkDefault 512; + }; + }; + + sops.secrets = { + ${guestCfg.lastfmApiKeySecret}.sopsFile = ../secrets + "/${config.networking.hostName}.yaml"; + ${guestCfg.lastfmSecretSecret}.sopsFile = ../secrets + "/${config.networking.hostName}.yaml"; + }; + + sops.templates."navidrome.env" = { + owner = "root"; + group = "storage"; + mode = "0440"; + content = '' + ND_LASTFM_APIKEY=${config.sops.placeholder.${guestCfg.lastfmApiKeySecret}} + ND_LASTFM_SECRET=${config.sops.placeholder.${guestCfg.lastfmSecretSecret}} + ''; + }; + }; +} diff --git a/configs/nixos/guests/reverse-proxy.nix b/configs/nixos/guests/reverse-proxy.nix index 27eb287..10e378e 100644 --- a/configs/nixos/guests/reverse-proxy.nix +++ b/configs/nixos/guests/reverse-proxy.nix @@ -23,11 +23,6 @@ in enableACME = true; locations."/".proxyPass = "http://10.23.20.32"; }; - "music.alexion.dev" = { - forceSSL = true; - enableACME = true; - locations."/".proxyPass = "http://10.23.20.32"; - }; "photos.alexion.dev" = { forceSSL = true; enableACME = true; diff --git a/configs/nixos/hosts/pikachu/default.nix b/configs/nixos/hosts/pikachu/default.nix index 87da021..cc5b648 100644 --- a/configs/nixos/hosts/pikachu/default.nix +++ b/configs/nixos/hosts/pikachu/default.nix @@ -120,6 +120,22 @@ }; }; + guests.navidrome = { + enable = true; + vlan = 20; + mac = "BC:24:11:C9:17:83"; + dataPath = "/pikachu/data/navidrome"; + libraryPath = "/pikachu/media/music"; + address = "10.23.20.43"; + reverseProxy = { + enable = true; + route = { + domain = "music.alexion.dev"; + port = 4533; + }; + }; + }; + guests.reverse-proxy = { enable = true; vlan = 20; diff --git a/configs/nixos/modules/services/navidrome.nix b/configs/nixos/modules/services/navidrome.nix new file mode 100644 index 0000000..e5bba6c --- /dev/null +++ b/configs/nixos/modules/services/navidrome.nix @@ -0,0 +1,64 @@ +{ + config, + lib, + ... +}: +let + cfg = config.modules.services.navidrome; +in +{ + options.modules.services.navidrome = { + enable = lib.mkEnableOption "Navidrome"; + + dataFolder = lib.mkOption { + type = lib.types.str; + default = "/var/lib/navidrome"; + readOnly = true; + description = "Path for Navidrome's `DataFolder` setting."; + }; + + musicFolder = lib.mkOption { + type = lib.types.str; + default = "/music"; + readOnly = true; + description = "Path for Navidrome's `MusicFolder` setting."; + }; + + environmentFile = lib.mkOption { + type = lib.types.path; + default = "/run/secrets/navidrome.env"; + readOnly = true; + description = "Path to Navidrome's environment file."; + }; + }; + + config = lib.mkMerge [ + { + registry."stable-uid".${config.networking.hostName}.navidrome = 20004; + } + + (lib.mkIf cfg.enable { + users.users.navidrome = { + isSystemUser = true; + uid = config.registry."stable-uid".${config.networking.hostName}.navidrome; + group = "storage"; + home = cfg.dataFolder; + }; + + services.navidrome = { + enable = true; + user = "navidrome"; + group = "storage"; + openFirewall = true; + environmentFile = cfg.environmentFile; + settings = { + Address = "0.0.0.0"; + Port = 4533; + DataFolder = cfg.dataFolder; + MusicFolder = cfg.musicFolder; + EnableSharing = true; + }; + }; + }) + ]; +} diff --git a/configs/nixos/registry/stable-uid.nix b/configs/nixos/registry/stable-uid.nix index 584c84a..7d2b18b 100644 --- a/configs/nixos/registry/stable-uid.nix +++ b/configs/nixos/registry/stable-uid.nix @@ -17,7 +17,7 @@ let in { - # Next stable UID: 20003 + # Next stable UID: 20005 replicate = true; type = lib.types.ints.positive; diff --git a/configs/nixos/secrets/pikachu.yaml b/configs/nixos/secrets/pikachu.yaml index fd13573..a94d646 100644 --- a/configs/nixos/secrets/pikachu.yaml +++ b/configs/nixos/secrets/pikachu.yaml @@ -1,6 +1,11 @@ ssh-host-ed25519-key: ENC[AES256_GCM,data:nxj7nMDzeqr0CitIjns9ugFh7Sur0pVpBr8SgWbxQMpIeNvBp+ugmnAxaUq2HRoYCo68uh/YKJZgMCBPSWrEyIhUX0rXmiZFmusrviJSXrYmy+n2XvLPjuv4vPbJEfjudRdXkHPazB3JJbeVRhGgK7y7smhNZlr8lwPGo0Ui0jVqWB4MPQRP6dLKVfV5shIusOqdhg7y2JSkZLYIjOBOJjknCxGmr5+vF2qwpDJQWEyzuJv2QJ48IlU7QyHF76//6xOchteL70/lyXI4gLiGl2fEczx3KKIXE+DNetKVAQxh4QHwzz9JK8qIJMYVry2qFdGGgF8eN7YEJYA60r1pL++1l8Uq++C1Bsc9jCO91ayQb+spXK0uM3G/ZJka7brRQbvcr54MJEyH6+0uBscXw/wMJ1EBX3Np6dumQhKuALnsAIXE89bKJg1wyPwaCy8uMZMcjQhvNkG0FPSBatHz2bB7bnNuNiXyT+scYyN0huE4Ccq7/H3bHWqbipV51YBH/XZKmjDiCwu3i0t5MHmp,iv:9P6Pa3hBe0/jbacjvzV3VGJJ4+OLRarEJEkLMOftqPw=,tag:zA6ewbj2ovP9+2WcAXa9qA==,type:str] ssh-host-rsa-key: ENC[AES256_GCM,data:C/xmdAeQlQou4wbP1EBqZ8G5/dRpmGYprj/z1g68WfCO7L+gzIgOryaEhBHugv3USAN8E3Ak4Ry+aUZCzDuIM2OeWdUm2AVT6r5o1RPYFtHSdsJUbKKB+9T3bR9GgFBRyRInSOjQhqR3lYuFLeA1xWhV8UG3Vv1tJwjLV1/JTUQf9j/NcYUr8tqjqD6hB8Fa5IEHPAUviX40R50o+EoiPIkB+qKbGru0Eg7p5K+/6d858nZc2lNgXIFnh72D0hXdWFs1BvcwGaY2WL3i7nJmOBUc3zM3/bYlU9EIscl04V6aUSXmF2sqI13Psa8lApV08MdAyUoFSHlYxlFxqe0IJ3nm0ofbJdwM6oDR6E8yQeU9vpirnYOPI6CWx6Y83LFWAVlhOZQKoLPBbiKImjM8oyoBGiIdqUrIXggoZ1u4F2/SwJtd9MwNr0+UDQWAQsYoAoBfI9LSUvEj52C2RiF28MDw2mEE8zmuHWM96KRUB7E8sGXHJrfBzDC5JuVdwmNCAOgck7ACJnHh8ZbKQYv8iGjbwVU/9z52BIUTLjJrdllSmIGWjVqOSVAUZVl7R3AK/xAaUuNNLaNzR4kk/+lQhPTuOLeFBfWbu3kryfeyJNyuHHtS21tgTcwc/bMYVq8LM8ttPn3b/BLQMl49qlBNmMk4TZ4hzBC5BMW6TgSHJRXZ7eajV8x+TM+aGu34xU0TiIy95XgTdi1ktQ0kAzB94iLTsMVhWpwoOguOlGLPGDKuDlym44T4cFRHySSHy0xlroPD1FPSvlSP7uY++55HsxX/88M8HNtCQ9h2P7jFRBYL+CpYbwgWNqtIWq8/daxCxScKZZ2/sAvFL5Ttqe//wYwNp6F3WbIxOk7yS/Ov+ADOt+RnxB2P11dkWNN7kHscGO7/sBNhDbzyEB8kHdfbEYKAl+OwUUsYqiGzdKF8E8aNa/0AAld3whrVgsUmIbFeoAkvJO6TkiZ3uuJAHdF9L8XfpWLEbu+geb6HG4+Bf5QOxxKVkSihMCimTI5vPGHuXj0/vwMihsMvhcaTmfSh2cZRAQhwIPQamEEeC+vqU2Igwcm57Cg81gMJYORLj+SaZSbCbrUxqElE/1OxUtF1KEh9njXjNK/grKnZbDDfRrhZwm3Nri1yxQxk/haHVoZttwXKo2vsWsYvpaHPJuUoQ0r/Wo8RffH2s04qauphLgWi2BUpfs+nNFBGRu6S92o/dktNQYgVeAGo7fYrM4xQX26fjQcOR7TD5bQIR2HcfHzyfH6A4c8MuInUeuf7kZFOoR1DdUFXYbvNcIgbmB/ScImkP3q18KaH+JRsVJbEEB7X4YOQ6lvHTtk0QVLv50V22BA9naFlpKGMWjq0oT5Gf40c6+opDlQWKrgwISwDvm9QJznvquj1EUlFJwVf0SJBHfeomFITZZMma2no2EIV0s/n9LvaU0b1Aim9EAqxUTgMrryLzE16dqSQKugbsn2jWMy7By3Xf8g0kstbyht5EOUZpThrwthEcGRN3xRHHke6ILckAVqEvl2wMuNMV8sd904pdYUL1wQAHKtg+fRbzu5cMY8itEs6YPsWob+HaD+n2tfHf4ByR1KBZ92nCyAWWjW6jrSDCzYCRsxvMSdsBJz9U4szSExC2qm9kQxGeedOl8/qECRZQQArBQZdavbwECGVUT3LRWQOu6bDyFgzoTzUlRxK05wr+Rpir/NmiGURGCZhmy+J2QnejlK0zOidORZAkzFcuGUPiUDGvJ99y43oSYAEYAI4UMKu42zimdVz50PbD2jpSJS6OmxUKfPfyy6CKC1lmYSB7PVGHzQ2aIw0RVMND9JNcntD5qgS2/g1CFhh9z9QyVtkOqiPICXTRXmVEQ5PW04tyzKBRkiSPoXxpC1zq77QPUAz0ZfeElqppdGyfejRiRl6m5JXJ9Pwl3/pgMEqGs0gpM/+TuT3rrYr6v602wPNq6/1XWpizReVPJZNzMrmxQwiIO285w1kvBlZ1VQnbAUiJR+pZuc9PiegSqEbfdkC8SQaGj+2M9tSDRzDGDE1y8pOLDi8Wczbn313JM+QwYDAXGccXxCEq0d3fVjb26u0ZNq2nn5zDy002aMWro+mrsLlWOf4MoAwvnyxgwEewrBEFIk1n14huFd1YpVomZuY3Y9XX1b1XoiTlhbmlP2HopobgQCD4Gt4zFb9RovNxBib+g4j2N94uoiuXOyBTT1B/HWHr6DnMxNvtHIyMOUPzR+6hwO9ntmdhPh+UZIB2b6FWZWLec55yBewzmvWn44dOdo7Z8Tp4d7vfSamyQDO49LA7Bd0CpDan7lbpw5rD4iejRCqZ/yJHTRyExSrZtcLQsScaorGCPlxHL4MUlRMynkyjPSUDRzqdJs+OnJDfUxJ566z8978rCppuYGRAGCqnoEHN9vcGsncMt9LuyUFlDaNKoOFZlMthvfQnUy47JnrP5u18Ph8qjn6A2Wh6MowsMvM67EBSlx8ZdHJC1iWqlHULW7FKPtcvKnDB/GToOqi2y9+EuMVWpoDJmTTyZFr9fmMlA0mL9tXWWG5h3G4LeLrT+LmRmk5tgL1TUJz5XDRQIQZRXxP+X7yt1bDh5PQK0vyupnWbs8KGDM+/pO8GwkwPnehBmJkkfkA6W2DIWzqRZHstdGmk1vtBqE6yAmwBOuXL+vm0vN86ulN+87LNR3OpCBRyorqundmw82vg/RdPNuA+Zxqg+VErsuTTdJmgZxN1rDU+8AiJPC8ZK7mH74lLYX2Ad86ZDriPcAi7wyYLc1qrSSHFCofb2FuM3/cJXcadSjZ//HxZh6qVOC5FW3b/YnFLZeqUgai6PO5rvzgyXusb2qHK60sEyS6pfw+ybOyZxWAc1TYqFXtOhUje8SW3R4cCG4zyCh5kUjZLiCNoNU2MIvVHjSfe9KAvTO2pcmuDIuVv9CKoko8tgoFqS80UDeP0pn99AmnTMXbhUR7eYjR9qAABF0IJJGUUIR1SbwiJrdEXil1qNcgciBI1epIXc1uBRXhcEnHI8VxVwYxF5wISjfuvp4cqJk03AkZVa4E2yydUATOCtk6fkaxVJIkitQaVgc0tqS4pX+9d35NF/njFHXksT/wMBfVJYmcgfIHJFK46igB/rwwC3PpOM+YNM8aJpLyZbbUB9CEWMtT9P5LqLWFoUaC2brIKG7vrfy2OSNOEWxiEDAzn7kY1+zpOb93J8FURpDNV+K4YUznPr1z2BxEGAKQXXx2PHQpAYYvdane5uWsZWMTsK5qQunX2UHPscr3T0A384pzXNdgnR6OrZfbcIpmvTAjxH9qzQLj49kJQ98czuutJWxgrp9J7abKeIdWGObTra5gA6bJ5kw9FZjW0xiyrOLAok/Vbmen8WCIVxToMJnXR91uuq58GxrcuZFke0NpCfEu3bpdPeKlb+sy0FREiFCpaDUNFFn5DqTNwOiW5KjGw/fG6dqC/NUhH5xdeod4dR1C4NEpYKzbWolUFf+rvFtGrRnQ0VQMXav+fpnBFiRA/ez/7oAzzwNv8Y7s3QpCUSUjsPRGWG/4pDlRSaz7rwkCfRxpkbOmPruYx3fcHigBnvIMLtKAPCx4jE7EsS6BoHNhgCMRyzkdl+MZFLKZ7JgFvgPdNcieFlptax2KaEwBIvhgQ4rimLRCv9cCM1qmmNIGB0UovqZ0Ye8tRG6LteFTgsfb3bNQ5sShaHQiuh2m9JGnI41q8CH2Gn6Mz/ftxn4yDWL/SlkjhtnRLoYvD5ro/Tf4q+exobXC0fbA1sLOnaX3atqHeQWK4Sk9L7zrDbwrJHzgmFD2PUPUhpfg9r1QMtJVkUckhQgyjrrIBfyZ4qqPmpYbH1w7cfE3C/rdprNZFKujYlA0ol8Ok1n0gyRFTeNZvd5EdrQ3D+hIKc0ID7/FmfzIccrI6x+wOE1ePQPzHay2mM8qRcjgTiQOTMaFRYjVy28ovqgixs/bylGvw5VNMGJTIqcu6fC54i3HOBhmnH1Ab/5a7PmwXUbAajw+mKddtrr0wQ7cXRncFnoJtld2AIOs7aD51C1Zkr1UqxliWObCsXJmaQoOpBScHdiKNp1mKQ1Oddu3mj0Su1e4gQR+YAwOyQ1zBU5WvNBbudUmRIsjXDS2Y6XaVN50neQ0DZ5V71259xPyawyVvrdiiUg7HOXgGFGcWbFU+p7MZaq0KE3aXgwCC+/mhFgaKgIVWir5pElNaXLUMDF+1jnblaKyHyAk0HPBgegAphVqWF3Hqp3b15jTSdWsmOugGcCO9hLk2OD99nZzuEVBLbDg0yGISPgzVNFg3+Pqcu8FqmGxLwAWNTRc0+ov7VL5Y4fJoD4QOPj19+nZyvXze/FOYo1Hp+U7PBZDIKVswcdauZG+Z2t2bEd0OxXj1NoazpycIVI/TmiTVyDsK+ijXztNnWXGG24WW1KZ3O5+MmDm7F7RDi1vCOfOOg3Y1yPVprOvltyn1uPo5Q/pPTD1FmAGpJkmdT3+dKkb9N5u6RNvRBEzFY8HCqcvKX1HulpJpz72,iv:vfjzz6U6ey9wGs6Ia3Vd5HaGBvpSq4akp4e3eLC/A3M=,tag:ar1+i09oOEusEuQnr44ASw==,type:str] ssh-user-ed25519-key: ENC[AES256_GCM,data:uHZADD2tq8vqlOAOWYniEnj1sl2IUqepf4YvrmRX/0nTksVnoethW6Yb2VNTgu5FsKCxvO65qMZ9KYupDkw/+SCZP2VH4TyhByQSr4SYxrEiNOJLdvmL2VnOWlNNZKaswgBePQR7TBG4mSIiTXCjEAcxeHAhBxLPnwiFxK/Zd6yQhq61x1N+ZtOj0q0QpADP3vioUohdkE5ODoEOUUoQJkYpjdJl2j4PhjCq7yHMEZpeMkqjZhVX5+i8blBu43BM4bKS2tBIJW2IvPV9sHOH8/tsR6p5q0pnpcJAOyOquBSo+eR0egY+liXjSv+Vo/ZDPapl7rKs2SoJ5huUGFbWjxbXY2+t2SYyiVA8lufrVUu94RCwOOJ8UyGYzAGGrChUImVDBsgk/GWwpOvWxcqydF8d6mDdxK9Iek9HtYsS+IKXLrF8GAI9OLB9jKgHVZgyQYSnC146cwCLt99TOFVocE1lfzwpFQTwKvxkep5jBEWiyqRaRUngti0JuF0ih1BWo+LKqxIqq9+V3I39xM8PfTjgHiifnxj4wuhS,iv:32zDKpNPYMiP6Rx7L9TAhxrMwcmphIGni+0jcESoLqg=,tag:YWGE62WQNm2Cer+eG+S5Xg==,type:str] +services: + navidrome: + lastfm: + api-key: ENC[AES256_GCM,data:Zb0I5TpdEqOxLAD6ed2XywFGZp7dfY69vcGiWOUJkJQ=,iv:pO53pHr09jWfzJyTsQbTHtzTmiQ9+RQkDwZJmInIiX8=,tag:io9jkSN7TF2I8+qlSymaYw==,type:str] + secret: ENC[AES256_GCM,data:rKhd9i0/tbJlXhc9Wf81IDydjpD8khSdwJEEMK260E4=,iv:btBXWBKJp9br5a3yZsRVegN8Ytzc45Qbk2iNlPbENkQ=,tag:LUbu+6gxfqQvVpddZto7Tg==,type:str] sops: age: - enc: | @@ -21,7 +26,7 @@ sops: dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA== -----END AGE ENCRYPTED FILE----- recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh - lastmodified: "2026-08-03T01:26:32Z" - mac: ENC[AES256_GCM,data:w4i6+dwN/t/SvFTrP/YiapxOY6ecT7+cJ0v/5lYMe4m1vL2w4mJw9ZrxWifcCcTEfaa766Wc1NE+GxGrBZtlORExbtQCa4tg2GuIQCRqTEc5WSqhYrgMb4elWuYe+r3/SlAf1ldNxz+cmLVtbDpSs96bvkGsKdw/i+q9n2URJYE=,iv:MFWgMtIdpn1v0T6FPhTgBMxi+6kzf2ZAnOGaisRT0d0=,tag:qBX32/zUHSPagXU+u8G8tA==,type:str] + lastmodified: "2026-09-12T22:54:25Z" + mac: ENC[AES256_GCM,data:4R3a0ytf4i8aM8iBQ9vRC/q+hvtjLrCTNgbpCB5DbqBMBYmhtGsBmO/0i1yT0N4tydyLaGOMkNfuuuVi3ASLEOKnViy9w3uD9IN0F79W4f2vguWUVIp/T8skvN9wbV9o9xd0LkJzmeVVgzb1H6HKw+rTGjbquleoIHEBDcDDM+k=,iv:0NlckMXlS+idY6UGBF+F0KFLt+pzD7Q/YEQOdXraAYg=,tag:JBBQduV8uhnKz9gPf6HfAQ==,type:str] unencrypted_suffix: _unencrypted version: 3.13.3 diff --git a/docs/TODO.md b/docs/TODO.md index 647393c..d096a55 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -48,7 +48,6 @@ - lidarr (requires raichu) - immich -- navidrome - romm - gitea - vikunja (?) @@ -101,6 +100,7 @@ # tech debt - better remote deployment +- centralize shared system group names, starting with the `storage` group ## nix refactor ```text