From 4a4de4eb0a3a838cde3d391a00e7bb39e8d5080a Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Fri, 31 Jul 2026 11:17:25 +0300 Subject: [PATCH] add yisroelbaum web stack module --- .gitignore | 3 + README.md | 41 ++++++++++ flake.lock | 103 ++++++++++++++++++++++++++ flake.nix | 54 ++++++++++++++ nix/module.nix | 173 +++++++++++++++++++++++++++++++++++++++++++ nix/tests/module.nix | 82 ++++++++++++++++++++ 6 files changed, 456 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/module.nix create mode 100644 nix/tests/module.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2718357 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +result +/.direnv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3855902 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# yisroelbaum.com web stack + +This flake owns the production NixOS configuration for the web services under +`yisroelbaum.com`: + +- the MyResume package at `yisroelbaum.com`; +- Jellyfin at `jellyfin.yisroelbaum.com`; +- Forgejo at `git.yisroelbaum.com`, including its Borg backup; +- Rabbi Gerzi at `rabbigerzi.yisroelbaum.com` and + `rabbigerziapi.yisroelbaum.com`; +- nginx and per-host ACME certificates for those endpoints. + +The importing host remains responsible for provisioning secrets. Import +`nixosModules.yisroelbaum-web` or `nixosModules.default`, then pass the +resulting runtime paths: + +```nix +services.yisroelbaum-web = { + enable = true; + secretFiles = { + borgPassphrase = "/run/secrets/borg-passphrase"; + borgPrivateKey = "/run/secrets/borg-private-key"; + forgejoMailerUser = "/run/secrets/forgejo-mailer-user"; + forgejoMailerPassword = "/run/secrets/forgejo-mailer-password"; + rabbiGerziEnvironment = "/run/secrets/rabbi-gerzi.env"; + }; +}; +``` + +Run the checks with: + +```shell +nix flake check +``` + +## First deployment + +This repository is hosted by the Forgejo instance that it configures. For the +initial migration, publish and push this flake while the old host configuration +is still active. Only then add the remote flake input to the host configuration +and update its lock file. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9662262 --- /dev/null +++ b/flake.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1785318670, + "narHash": "sha256-dN6Ou5x/+23FZLEpYP3IffO+NyJFzUlGumt1uu3MMaY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0954f7ee2f6bb3dc7d4e3d0d8bcb8fd4bde4cfc5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "rabbi-gerzi": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "utils": "utils" + }, + "locked": { + "lastModified": 1784294454, + "narHash": "sha256-oPxPCk4wMf0qvarGyZqixQI44RyirQA0ZBYs7J8liv4=", + "ref": "refs/heads/master", + "rev": "3e02a83dd48de378d3763fb27df3a665cca6cb18", + "revCount": 379, + "type": "git", + "url": "https://git.yisroelbaum.com/yisroelbaum/Rabbi_Gerzi" + }, + "original": { + "type": "git", + "url": "https://git.yisroelbaum.com/yisroelbaum/Rabbi_Gerzi" + } + }, + "resume": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1779106432, + "narHash": "sha256-EsVQ1SAEIbgiTDA8b5ygm904vDJd5xUQWld/KJRrJvM=", + "ref": "refs/heads/master", + "rev": "4ae8a76a478b5c1f3abd45ad3cbdaaa3439b4635", + "revCount": 11, + "type": "git", + "url": "https://git.yisroelbaum.com/yisroelbaum/MyResume" + }, + "original": { + "type": "git", + "url": "https://git.yisroelbaum.com/yisroelbaum/MyResume" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "rabbi-gerzi": "rabbi-gerzi", + "resume": "resume" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c5b2a35 --- /dev/null +++ b/flake.nix @@ -0,0 +1,54 @@ +{ + description = "NixOS module for the yisroelbaum.com web stack"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + resume = { + url = "git+https://git.yisroelbaum.com/yisroelbaum/MyResume"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + rabbi-gerzi = { + url = "git+https://git.yisroelbaum.com/yisroelbaum/Rabbi_Gerzi"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + resume, + rabbi-gerzi, + ... + }: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + module = import ./nix/module.nix { inherit resume; }; + in + { + nixosModules.yisroelbaum-web = { + imports = [ + rabbi-gerzi.nixosModules.default + module + ]; + }; + nixosModules.default = self.nixosModules.yisroelbaum-web; + + checks.${system}.module-eval = import ./nix/tests/module.nix { + inherit + nixpkgs + self + system + ; + }; + + formatter.${system} = pkgs.nixfmt-tree; + + devShells.${system}.default = pkgs.mkShell { + packages = [ pkgs.nixfmt-tree ]; + }; + }; +} diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..7a0e404 --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,173 @@ +{ resume }: +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.yisroelbaum-web; + domainName = "yisroelbaum.com"; + resumePackage = resume.packages.${pkgs.stdenv.hostPlatform.system}.default; + secretFileOption = + description: + lib.mkOption { + type = lib.types.path; + inherit description; + }; +in +{ + options.services.yisroelbaum-web = { + enable = lib.mkEnableOption "the yisroelbaum.com web stack"; + + secretFiles = { + borgPassphrase = secretFileOption "Borg repository passphrase file."; + borgPrivateKey = secretFileOption "Borg repository SSH private key file."; + forgejoMailerUser = secretFileOption "Forgejo SMTP username file."; + forgejoMailerPassword = secretFileOption "Forgejo SMTP password file."; + rabbiGerziEnvironment = secretFileOption "Rabbi Gerzi runtime dotenv file."; + }; + }; + + config = lib.mkIf cfg.enable { + users.users.${config.services.nginx.user}.extraGroups = [ "acme" ]; + + services = { + jellyfin.enable = true; + + forgejo = { + enable = true; + settings = { + server = { + ROOT_URL = "https://git.${domainName}/"; + PROTOCOL = "http"; + HTTP_ADDR = "127.0.0.1"; + HTTP_PORT = 3000; + DOMAIN = "git.${domainName}"; + SSH_PORT = 2222; + START_SSH_SERVER = true; + }; + session.COOKIE_SECURE = true; + service.DISABLE_REGISTRATION = true; + mailer = { + ENABLED = true; + SMTP_ADDR = "in-v3.mailjet.com"; + SMTP_PORT = 587; + FROM = "me@${domainName}"; + }; + }; + secrets.mailer = { + USER = cfg.secretFiles.forgejoMailerUser; + PASSWD = cfg.secretFiles.forgejoMailerPassword; + }; + }; + + rabbi-gerzi = { + enable = true; + frontend = { + hostName = "rabbigerzi.${domainName}"; + nginx = { + forceSSL = true; + enableACME = true; + }; + }; + backend = { + hostName = "rabbigerziapi.${domainName}"; + environmentFile = cfg.secretFiles.rabbiGerziEnvironment; + nginx = { + forceSSL = true; + enableACME = true; + }; + }; + }; + + nginx = { + enable = true; + virtualHosts = { + "${domainName}" = { + forceSSL = true; + enableACME = true; + locations."/".root = "${resumePackage}"; + }; + + "jellyfin.${domainName}" = { + forceSSL = true; + enableACME = true; + locations."/".proxyPass = "http://localhost:8096"; + }; + + "git.${domainName}" = { + forceSSL = true; + enableACME = true; + extraConfig = '' + client_max_body_size 512M; + ''; + locations."/" = { + proxyPass = "http://127.0.0.1:3000"; + recommendedProxySettings = false; + extraConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + ''; + }; + }; + }; + }; + + borgbackup.jobs.forgejo = { + paths = [ config.services.forgejo.stateDir ]; + repo = "ssh://n5lniyfd@n5lniyfd.repo.borgbase.com/./repo"; + user = "root"; + group = "root"; + encryption = { + mode = "repokey-blake2"; + passCommand = "${pkgs.coreutils}/bin/cat ${cfg.secretFiles.borgPassphrase}"; + }; + doInit = true; + compression = "auto,zstd"; + startAt = "*-*-* 03:15:00"; + persistentTimer = true; + prune.keep = { + daily = 7; + weekly = 4; + monthly = 6; + }; + environment.BORG_RSH = "ssh -i ${cfg.secretFiles.borgPrivateKey} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.config/borg/known_hosts"; + preHook = '' + forgejoWasActive=/root/.cache/borg/forgejo-was-active + if ${pkgs.systemd}/bin/systemctl is-active --quiet forgejo.service; then + touch "$forgejoWasActive" + ${pkgs.systemd}/bin/systemctl stop forgejo.service + else + rm -f "$forgejoWasActive" + fi + ''; + postHook = '' + forgejoWasActive=/root/.cache/borg/forgejo-was-active + if [ -e "$forgejoWasActive" ]; then + ${pkgs.systemd}/bin/systemctl start forgejo.service + rm -f "$forgejoWasActive" + fi + ''; + }; + }; + + security.acme = { + defaults = { + webroot = "/var/lib/acme/acme-challenge/"; + email = "yisroel.d.baum@gmail.com"; + }; + acceptTerms = true; + }; + + systemd.services.borgbackup-job-forgejo = { + after = [ + "sops-install-secrets.service" + "network-online.target" + ]; + wants = [ "network-online.target" ]; + }; + }; +} diff --git a/nix/tests/module.nix b/nix/tests/module.nix new file mode 100644 index 0000000..25695c2 --- /dev/null +++ b/nix/tests/module.nix @@ -0,0 +1,82 @@ +{ + nixpkgs, + self, + system, +}: +let + pkgs = nixpkgs.legacyPackages.${system}; + evaluatedSystem = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + self.nixosModules.default + { + system.stateVersion = "25.11"; + services.yisroelbaum-web = { + enable = true; + secretFiles = { + borgPassphrase = "/run/secrets/borg-passphrase"; + borgPrivateKey = "/run/secrets/borg-private-key"; + forgejoMailerUser = "/run/secrets/forgejo-mailer-user"; + forgejoMailerPassword = "/run/secrets/forgejo-mailer-password"; + rabbiGerziEnvironment = "/run/secrets/rabbi-gerzi.env"; + }; + }; + } + ]; + }; + evaluatedConfig = evaluatedSystem.config; + virtualHosts = evaluatedConfig.services.nginx.virtualHosts; + certificates = evaluatedConfig.security.acme.certs; + forgejoBackup = evaluatedConfig.services.borgbackup.jobs.forgejo; + forgejoBackupService = evaluatedConfig.systemd.services.borgbackup-job-forgejo; + publicHostNames = [ + "yisroelbaum.com" + "jellyfin.yisroelbaum.com" + "git.yisroelbaum.com" + "rabbigerzi.yisroelbaum.com" + "rabbigerziapi.yisroelbaum.com" + ]; +in +assert evaluatedConfig.services.nginx.enable; +assert evaluatedConfig.services.jellyfin.enable; +assert evaluatedConfig.services.forgejo.enable; +assert evaluatedConfig.services.rabbi-gerzi.enable; +assert + virtualHosts."yisroelbaum.com".locations."/".root + == "${self.inputs.resume.packages.${system}.default}"; +assert virtualHosts."jellyfin.yisroelbaum.com".locations."/".proxyPass == "http://localhost:8096"; +assert virtualHosts."git.yisroelbaum.com".locations."/".proxyPass == "http://127.0.0.1:3000"; +assert !virtualHosts."git.yisroelbaum.com".locations."/".recommendedProxySettings; +assert nixpkgs.lib.hasInfix "proxy_set_header X-Forwarded-Proto $scheme;" + virtualHosts."git.yisroelbaum.com".locations."/".extraConfig; +assert builtins.all ( + hostName: + virtualHosts.${hostName}.forceSSL + && virtualHosts.${hostName}.enableACME + && certificates.${hostName}.domain == hostName +) publicHostNames; +assert builtins.all (certificate: !(nixpkgs.lib.hasPrefix "*." certificate.domain)) ( + builtins.attrValues certificates +); +assert evaluatedConfig.services.forgejo.settings.server.ROOT_URL == "https://git.yisroelbaum.com/"; +assert evaluatedConfig.services.forgejo.settings.server.DOMAIN == "git.yisroelbaum.com"; +assert evaluatedConfig.services.forgejo.settings.server.SSH_PORT == 2222; +assert evaluatedConfig.services.forgejo.settings.mailer.FROM == "me@yisroelbaum.com"; +assert evaluatedConfig.services.forgejo.secrets.mailer.USER == "/run/secrets/forgejo-mailer-user"; +assert + evaluatedConfig.services.forgejo.secrets.mailer.PASSWD == "/run/secrets/forgejo-mailer-password"; +assert evaluatedConfig.services.rabbi-gerzi.frontend.hostName == "rabbigerzi.yisroelbaum.com"; +assert evaluatedConfig.services.rabbi-gerzi.backend.hostName == "rabbigerziapi.yisroelbaum.com"; +assert + evaluatedConfig.services.rabbi-gerzi.backend.environmentFile == "/run/secrets/rabbi-gerzi.env"; +assert forgejoBackup.repo == "ssh://n5lniyfd@n5lniyfd.repo.borgbase.com/./repo"; +assert forgejoBackup.startAt == "*-*-* 03:15:00"; +assert forgejoBackup.persistentTimer; +assert forgejoBackup.prune.keep.daily == 7; +assert forgejoBackup.prune.keep.weekly == 4; +assert forgejoBackup.prune.keep.monthly == 6; +assert builtins.elem evaluatedConfig.services.forgejo.stateDir forgejoBackup.paths; +assert builtins.elem "sops-install-secrets.service" forgejoBackupService.after; +pkgs.runCommand "yisroelbaum-web-module-eval" { } '' + touch "$out" +''