diff --git a/README.md b/README.md new file mode 100644 index 0000000..aee4521 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# NixOS Config + +Personal Nix flake for four machines: + +- `michael` - x86_64 Linux server +- `tahani` - x86_64 Linux home server / workstation +- `chidi` - aarch64 Darwin work laptop +- `jason` - aarch64 Darwin personal laptop + +## Repository Map + +- `modules/` - flake-parts modules, auto-imported via `import-tree` +- `modules/_hosts/` - host-specific submodules like hardware, disks, and services +- `modules/_lib/` - local helper functions +- `apps/` - Nushell apps exposed through the flake +- `secrets/` - SOPS-encrypted secrets +- `flake.nix` - generated flake entrypoint +- `modules/dendritic.nix` - source of truth for flake inputs and `flake.nix` generation + +## How It Is Structured + +This repo uses `den` and organizes configuration around aspects instead of putting everything directly in host files. + +- shared behavior lives in `den.aspects..` modules +- hosts are declared in `modules/hosts.nix` +- host composition happens in `modules/.nix` +- user-level config mostly lives in Home Manager aspects + +Common examples: + +- `modules/core.nix` - shared Nix and shell foundation +- `modules/dev-tools.nix` - VCS, language, and developer tooling +- `modules/network.nix` - SSH, fail2ban, and tailscale aspects +- `modules/michael.nix` - server composition for `michael` +- `modules/tahani.nix` - server/workstation composition for `tahani` + +## Common Commands + +```bash +nix run .#build +nix run .#build -- michael +nix run .#apply +nix run .#deploy -- .#tahani +nix flake check +alejandra . +``` + +## Updating The Flake + +`flake.nix` is generated. Update inputs in `modules/dendritic.nix`, then regenerate: + +```bash +nix run .#write-flake +alejandra . +``` diff --git a/modules/_darwin/dock.nix b/modules/_darwin/dock.nix index 00c04bb..3928abb 100644 --- a/modules/_darwin/dock.nix +++ b/modules/_darwin/dock.nix @@ -45,7 +45,7 @@ in { {path = "/System/Applications/Music.app/";} {path = "/System/Applications/System Settings.app/";} { - path = "${config.users.users.cschmatzler.home}/Downloads"; + path = "/Users/cschmatzler/Downloads"; section = "others"; options = "--sort name --view grid --display stack"; } diff --git a/modules/_hosts/michael/backups.nix b/modules/_hosts/michael/backups.nix new file mode 100644 index 0000000..38444ad --- /dev/null +++ b/modules/_hosts/michael/backups.nix @@ -0,0 +1,58 @@ +{ + config, + lib, + pkgs, + ... +}: { + services.restic.backups.gitea = { + repository = "s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories"; + paths = ["/var/lib/gitea"]; + exclude = [ + "/var/lib/gitea/log" + "/var/lib/gitea/data/gitea.db" + "/var/lib/gitea/data/gitea.db-shm" + "/var/lib/gitea/data/gitea.db-wal" + ]; + passwordFile = config.sops.secrets.michael-gitea-restic-password.path; + environmentFile = config.sops.secrets.michael-gitea-restic-env.path; + pruneOpts = [ + "--keep-daily 7" + "--keep-weekly 4" + "--keep-monthly 6" + ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + RandomizedDelaySec = "1h"; + }; + }; + + systemd.services.restic-backups-gitea = { + wants = ["restic-init-gitea.service"]; + after = ["restic-init-gitea.service"]; + serviceConfig = { + User = lib.mkForce "gitea"; + Group = lib.mkForce "gitea"; + }; + }; + + systemd.services.restic-init-gitea = { + description = "Initialize Restic repository for Gitea backups"; + wantedBy = ["multi-user.target"]; + after = ["network-online.target"]; + wants = ["network-online.target"]; + path = [pkgs.restic]; + serviceConfig = { + Type = "oneshot"; + User = "gitea"; + Group = "gitea"; + RemainAfterExit = true; + EnvironmentFile = config.sops.secrets.michael-gitea-restic-env.path; + }; + script = '' + export RESTIC_PASSWORD=$(cat ${config.sops.secrets.michael-gitea-restic-password.path}) + restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories snapshots &>/dev/null || \ + restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories init + ''; + }; +} diff --git a/modules/_hosts/michael/gitea.nix b/modules/_hosts/michael/gitea.nix new file mode 100644 index 0000000..d9c93bc --- /dev/null +++ b/modules/_hosts/michael/gitea.nix @@ -0,0 +1,114 @@ +{ + config, + lib, + ... +}: { + sops.secrets = { + michael-gitea-litestream = { + sopsFile = ../../../secrets/michael-gitea-litestream; + format = "binary"; + owner = "gitea"; + group = "gitea"; + path = "/run/secrets/michael-gitea-litestream"; + }; + michael-gitea-restic-password = { + sopsFile = ../../../secrets/michael-gitea-restic-password; + format = "binary"; + owner = "gitea"; + group = "gitea"; + path = "/run/secrets/michael-gitea-restic-password"; + }; + michael-gitea-restic-env = { + sopsFile = ../../../secrets/michael-gitea-restic-env; + format = "binary"; + owner = "gitea"; + group = "gitea"; + path = "/run/secrets/michael-gitea-restic-env"; + }; + }; + + networking.firewall.allowedTCPPorts = [80 443]; + + services.redis.servers.gitea = { + enable = true; + port = 6380; + bind = "127.0.0.1"; + settings = { + maxmemory = "64mb"; + maxmemory-policy = "allkeys-lru"; + }; + }; + + services.gitea = { + enable = true; + database = { + type = "sqlite3"; + path = "/var/lib/gitea/data/gitea.db"; + }; + settings = { + server = { + ROOT_URL = "https://git.schmatzler.com/"; + DOMAIN = "git.schmatzler.com"; + HTTP_ADDR = "127.0.0.1"; + HTTP_PORT = 3000; + LANDING_PAGE = "explore"; + }; + service.DISABLE_REGISTRATION = true; + security.INSTALL_LOCK = true; + cache = { + ADAPTER = "redis"; + HOST = "redis://127.0.0.1:6380/0?pool_size=100&idle_timeout=180s"; + ITEM_TTL = "16h"; + }; + "cache.last_commit" = { + ITEM_TTL = "8760h"; + COMMITS_COUNT = 100; + }; + session = { + PROVIDER = "redis"; + PROVIDER_CONFIG = "redis://127.0.0.1:6380/1?pool_size=100&idle_timeout=180s"; + COOKIE_SECURE = true; + SAME_SITE = "strict"; + }; + api.ENABLE_SWAGGER = false; + }; + }; + + services.litestream = { + enable = true; + environmentFile = config.sops.secrets.michael-gitea-litestream.path; + settings = { + dbs = [ + { + path = "/var/lib/gitea/data/gitea.db"; + replicas = [ + { + type = "s3"; + bucket = "michael-gitea-litestream"; + path = "gitea"; + endpoint = "s3.eu-central-003.backblazeb2.com"; + } + ]; + } + ]; + }; + }; + + systemd.services.litestream.serviceConfig = { + User = lib.mkForce "gitea"; + Group = lib.mkForce "gitea"; + }; + + services.caddy = { + enable = true; + virtualHosts."git.schmatzler.com".extraConfig = '' + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + } + reverse_proxy localhost:3000 + ''; + }; +} diff --git a/modules/_hosts/tahani/adguardhome.nix b/modules/_hosts/tahani/adguardhome.nix index 213c267..89f167f 100644 --- a/modules/_hosts/tahani/adguardhome.nix +++ b/modules/_hosts/tahani/adguardhome.nix @@ -1,7 +1,7 @@ -{ +{config, ...}: { services.adguardhome = { enable = true; - host = "0.0.0.0"; + host = "127.0.0.1"; port = 10000; settings = { dhcp = { @@ -57,4 +57,13 @@ ]; }; }; + + services.caddy.virtualHosts."adguard.manticore-hippocampus.ts.net" = { + extraConfig = '' + tls { + get_certificate tailscale + } + reverse_proxy localhost:${toString config.services.adguardhome.port} + ''; + }; } diff --git a/modules/_hosts/tahani/paperless.nix b/modules/_hosts/tahani/paperless.nix index 616dd2d..10e0dcb 100644 --- a/modules/_hosts/tahani/paperless.nix +++ b/modules/_hosts/tahani/paperless.nix @@ -25,8 +25,11 @@ virtualisation.oci-containers = { backend = "docker"; containers.paperless-ai = { - image = "clusterzx/paperless-ai:latest"; + image = "clusterzx/paperless-ai:v3.0.9"; autoStart = true; + ports = [ + "127.0.0.1:3000:3000" + ]; volumes = [ "paperless-ai-data:/app/data" ]; @@ -36,11 +39,10 @@ PAPERLESS_AI_PORT = "3000"; # Initial setup wizard will configure the rest PAPERLESS_AI_INITIAL_SETUP = "yes"; - # Paperless-ngx API URL accessible from container (using host network) - PAPERLESS_API_URL = "http://127.0.0.1:${toString config.services.paperless.port}/api"; + PAPERLESS_API_URL = "http://host.docker.internal:${toString config.services.paperless.port}/api"; }; extraOptions = [ - "--network=host" + "--add-host=host.docker.internal:host-gateway" ]; }; }; @@ -57,7 +59,7 @@ services.paperless = { enable = true; - address = "0.0.0.0"; + address = "127.0.0.1"; passwordFile = config.sops.secrets.tahani-paperless-password.path; settings = { PAPERLESS_DBENGINE = "sqlite"; diff --git a/modules/_lib/constants.nix b/modules/_lib/constants.nix deleted file mode 100644 index 8495a95..0000000 --- a/modules/_lib/constants.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ - user = "cschmatzler"; - - sshKeys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHfRZQ+7ejD3YHbyMTrV0gN1Gc0DxtGgl5CVZSupo5ws" - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/I+/2QT47raegzMIyhwMEPKarJP/+Ox9ewA4ZFJwk/" - ]; - - stateVersions = { - darwin = 6; - nixos = "25.11"; - homeManager = "25.11"; - }; -} diff --git a/modules/_lib/open-project.nix b/modules/_lib/open-project.nix deleted file mode 100644 index c3de0f8..0000000 --- a/modules/_lib/open-project.nix +++ /dev/null @@ -1,10 +0,0 @@ -{pkgs}: -pkgs.writeShellScriptBin "open-project" '' - TARGET=$(fd -t d --exact-depth 1 . $HOME/Projects | - sed "s~$HOME/Projects/~~" | - fzf --prompt "project > ") - - if [ -n "$TARGET" ]; then - echo "$HOME/Projects/$TARGET" - fi -'' diff --git a/modules/ai-tools.nix b/modules/ai-tools.nix index 45823b2..0c46a5c 100644 --- a/modules/ai-tools.nix +++ b/modules/ai-tools.nix @@ -79,7 +79,7 @@ ExecStart = "${inputs'.llm-agents.packages.opencode}/bin/opencode serve --port 18822"; Restart = "on-failure"; RestartSec = 5; - Environment = "PATH=${config.home.profileDirectory}/bin:/run/current-system/sw/bin"; + Environment = "PATH=${pkgs.lib.makeBinPath [inputs'.llm-agents.packages.opencode pkgs.coreutils pkgs.nodejs_24 pkgs.nushell]}:/run/current-system/sw/bin"; }; Install = { WantedBy = ["default.target"]; diff --git a/modules/darwin.nix b/modules/darwin.nix index e9e0ecd..ec2685e 100644 --- a/modules/darwin.nix +++ b/modules/darwin.nix @@ -105,7 +105,7 @@ }; nix = { - settings.trusted-users = ["@admin" "cschmatzler"]; + settings.trusted-users = ["cschmatzler"]; gc.interval = { Weekday = 0; Hour = 2; diff --git a/modules/defaults.nix b/modules/defaults.nix index 5a952ca..fc978e4 100644 --- a/modules/defaults.nix +++ b/modules/defaults.nix @@ -23,16 +23,19 @@ config = { flake.flakeModules = { + # Shared system foundations + core = ./core.nix; + network = ./network.nix; + nixos-system = ./nixos-system.nix; + + # User environment ai-tools = ./ai-tools.nix; atuin = ./atuin.nix; - core = ./core.nix; desktop = ./desktop.nix; dev-tools = ./dev-tools.nix; email = ./email.nix; finance = ./finance.nix; neovim = ./neovim.nix; - network = ./network.nix; - nixos-system = ./nixos-system.nix; shell = ./shell.nix; ssh-client = ./ssh-client.nix; terminal = ./terminal.nix; diff --git a/modules/dev-tools.nix b/modules/dev-tools.nix index a392e98..083f4e3 100644 --- a/modules/dev-tools.nix +++ b/modules/dev-tools.nix @@ -378,21 +378,23 @@ ast-grep bun delta + deadnix devenv docker docker-compose gh - git gnumake hyperfine jj-ryu jj-starship + nil nodejs_24 nurl pnpm postgresql_17 serie sqlite + statix tea tokei tree-sitter diff --git a/modules/michael.nix b/modules/michael.nix index ddd259e..0cf40b1 100644 --- a/modules/michael.nix +++ b/modules/michael.nix @@ -11,180 +11,16 @@ den.aspects.tailscale ]; - den.aspects.michael.nixos = { - config, - pkgs, - lib, - modulesPath, - ... - }: { + den.aspects.michael.nixos = {modulesPath, ...}: { imports = [ (modulesPath + "/installer/scan/not-detected.nix") + ./_hosts/michael/backups.nix ./_hosts/michael/disk-config.nix + ./_hosts/michael/gitea.nix ./_hosts/michael/hardware-configuration.nix inputs.disko.nixosModules.default ]; networking.hostName = "michael"; - - sops.secrets = { - michael-gitea-litestream = { - sopsFile = ../secrets/michael-gitea-litestream; - format = "binary"; - owner = "gitea"; - group = "gitea"; - }; - michael-gitea-restic-password = { - sopsFile = ../secrets/michael-gitea-restic-password; - format = "binary"; - owner = "gitea"; - group = "gitea"; - }; - michael-gitea-restic-env = { - sopsFile = ../secrets/michael-gitea-restic-env; - format = "binary"; - owner = "gitea"; - group = "gitea"; - }; - }; - - networking.firewall.allowedTCPPorts = [80 443]; - - services.redis.servers.gitea = { - enable = true; - port = 6380; - bind = "127.0.0.1"; - settings = { - maxmemory = "64mb"; - maxmemory-policy = "allkeys-lru"; - }; - }; - - services.gitea = { - enable = true; - database = { - type = "sqlite3"; - path = "/var/lib/gitea/data/gitea.db"; - }; - settings = { - server = { - ROOT_URL = "https://git.schmatzler.com/"; - DOMAIN = "git.schmatzler.com"; - HTTP_ADDR = "127.0.0.1"; - HTTP_PORT = 3000; - LANDING_PAGE = "explore"; - }; - service.DISABLE_REGISTRATION = true; - security.INSTALL_LOCK = true; - cache = { - ADAPTER = "redis"; - HOST = "redis://127.0.0.1:6380/0?pool_size=100&idle_timeout=180s"; - ITEM_TTL = "16h"; - }; - "cache.last_commit" = { - ITEM_TTL = "8760h"; - COMMITS_COUNT = 100; - }; - session = { - PROVIDER = "redis"; - PROVIDER_CONFIG = "redis://127.0.0.1:6380/1?pool_size=100&idle_timeout=180s"; - COOKIE_SECURE = true; - SAME_SITE = "strict"; - }; - api.ENABLE_SWAGGER = false; - }; - }; - - services.litestream = { - enable = true; - environmentFile = config.sops.secrets.michael-gitea-litestream.path; - settings = { - dbs = [ - { - path = "/var/lib/gitea/data/gitea.db"; - replicas = [ - { - type = "s3"; - bucket = "michael-gitea-litestream"; - path = "gitea"; - endpoint = "s3.eu-central-003.backblazeb2.com"; - } - ]; - } - ]; - }; - }; - - systemd.services.litestream = { - serviceConfig = { - User = lib.mkForce "gitea"; - Group = lib.mkForce "gitea"; - }; - }; - - services.caddy = { - enable = true; - virtualHosts."git.schmatzler.com".extraConfig = '' - header { - Strict-Transport-Security "max-age=31536000; includeSubDomains" - X-Content-Type-Options "nosniff" - X-Frame-Options "DENY" - Referrer-Policy "strict-origin-when-cross-origin" - } - reverse_proxy localhost:3000 - ''; - }; - - services.restic.backups.gitea = { - repository = "s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories"; - paths = ["/var/lib/gitea"]; - exclude = [ - "/var/lib/gitea/log" - "/var/lib/gitea/data/gitea.db" - "/var/lib/gitea/data/gitea.db-shm" - "/var/lib/gitea/data/gitea.db-wal" - ]; - passwordFile = config.sops.secrets.michael-gitea-restic-password.path; - environmentFile = config.sops.secrets.michael-gitea-restic-env.path; - pruneOpts = [ - "--keep-daily 7" - "--keep-weekly 4" - "--keep-monthly 6" - ]; - timerConfig = { - OnCalendar = "daily"; - Persistent = true; - RandomizedDelaySec = "1h"; - }; - }; - - systemd.services.restic-backups-gitea = { - wants = ["restic-init-gitea.service"]; - after = ["restic-init-gitea.service"]; - serviceConfig = { - User = lib.mkForce "gitea"; - Group = lib.mkForce "gitea"; - }; - }; - - systemd.services.restic-init-gitea = { - description = "Initialize Restic repository for Gitea backups"; - wantedBy = ["multi-user.target"]; - after = ["network-online.target"]; - wants = ["network-online.target"]; - path = [pkgs.restic]; - serviceConfig = { - Type = "oneshot"; - User = "gitea"; - Group = "gitea"; - RemainAfterExit = true; - EnvironmentFile = config.sops.secrets.michael-gitea-restic-env.path; - }; - script = '' - export RESTIC_PASSWORD=$(cat ${config.sops.secrets.michael-gitea-restic-password.path}) - restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories snapshots &>/dev/null || \ - restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories init - ''; - }; }; } diff --git a/modules/shell.nix b/modules/shell.nix index 42a5899..ec21c31 100644 --- a/modules/shell.nix +++ b/modules/shell.nix @@ -284,7 +284,6 @@ home.packages = with pkgs; [ vivid - (callPackage ./_lib/open-project.nix {}) ]; }; } diff --git a/modules/ssh-client.nix b/modules/ssh-client.nix index db14d60..62672bc 100644 --- a/modules/ssh-client.nix +++ b/modules/ssh-client.nix @@ -1,29 +1,30 @@ {...}: { den.aspects.ssh-client.homeManager = { config, - lib, pkgs, ... - }: { + }: let + homeDir = "${ + if pkgs.stdenv.hostPlatform.isDarwin + then "/Users" + else "/home" + }/${config.home.username}"; + in { programs.ssh = { enable = true; enableDefaultConfig = false; includes = [ - (lib.mkIf pkgs.stdenv.hostPlatform.isLinux "/home/${config.home.username}/.ssh/config_external") - (lib.mkIf pkgs.stdenv.hostPlatform.isDarwin "/Users/${config.home.username}/.ssh/config_external") + "${homeDir}/.ssh/config_external" ]; matchBlocks = { "*" = {}; "github.com" = { identitiesOnly = true; identityFile = [ - (lib.mkIf pkgs.stdenv.hostPlatform.isLinux "/home/${config.home.username}/.ssh/id_ed25519") - (lib.mkIf pkgs.stdenv.hostPlatform.isDarwin "/Users/${config.home.username}/.ssh/id_ed25519") + "${homeDir}/.ssh/id_ed25519" ]; }; }; }; - - home.packages = [pkgs.openssh]; }; } diff --git a/modules/tahani.nix b/modules/tahani.nix index 4929109..fbddbef 100644 --- a/modules/tahani.nix +++ b/modules/tahani.nix @@ -28,11 +28,13 @@ tahani-paperless-password = { sopsFile = ../secrets/tahani-paperless-password; format = "binary"; + path = "/run/secrets/tahani-paperless-password"; }; tahani-email-password = { sopsFile = ../secrets/tahani-email-password; format = "binary"; owner = "cschmatzler"; + path = "/run/secrets/tahani-email-password"; }; }; virtualisation.docker.enable = true;