tighten service boundaries and clean up config structure

This commit is contained in:
2026-03-11 17:21:08 +00:00
parent eae286c5ab
commit 6569d7d4d8
16 changed files with 271 additions and 214 deletions

55
README.md Normal file
View File

@@ -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.<name>.<class>` modules
- hosts are declared in `modules/hosts.nix`
- host composition happens in `modules/<host>.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 .
```

View File

@@ -45,7 +45,7 @@ in {
{path = "/System/Applications/Music.app/";} {path = "/System/Applications/Music.app/";}
{path = "/System/Applications/System Settings.app/";} {path = "/System/Applications/System Settings.app/";}
{ {
path = "${config.users.users.cschmatzler.home}/Downloads"; path = "/Users/cschmatzler/Downloads";
section = "others"; section = "others";
options = "--sort name --view grid --display stack"; options = "--sort name --view grid --display stack";
} }

View File

@@ -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
'';
};
}

View File

@@ -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
'';
};
}

View File

@@ -1,7 +1,7 @@
{ {config, ...}: {
services.adguardhome = { services.adguardhome = {
enable = true; enable = true;
host = "0.0.0.0"; host = "127.0.0.1";
port = 10000; port = 10000;
settings = { settings = {
dhcp = { 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}
'';
};
} }

View File

@@ -25,8 +25,11 @@
virtualisation.oci-containers = { virtualisation.oci-containers = {
backend = "docker"; backend = "docker";
containers.paperless-ai = { containers.paperless-ai = {
image = "clusterzx/paperless-ai:latest"; image = "clusterzx/paperless-ai:v3.0.9";
autoStart = true; autoStart = true;
ports = [
"127.0.0.1:3000:3000"
];
volumes = [ volumes = [
"paperless-ai-data:/app/data" "paperless-ai-data:/app/data"
]; ];
@@ -36,11 +39,10 @@
PAPERLESS_AI_PORT = "3000"; PAPERLESS_AI_PORT = "3000";
# Initial setup wizard will configure the rest # Initial setup wizard will configure the rest
PAPERLESS_AI_INITIAL_SETUP = "yes"; PAPERLESS_AI_INITIAL_SETUP = "yes";
# Paperless-ngx API URL accessible from container (using host network) PAPERLESS_API_URL = "http://host.docker.internal:${toString config.services.paperless.port}/api";
PAPERLESS_API_URL = "http://127.0.0.1:${toString config.services.paperless.port}/api";
}; };
extraOptions = [ extraOptions = [
"--network=host" "--add-host=host.docker.internal:host-gateway"
]; ];
}; };
}; };
@@ -57,7 +59,7 @@
services.paperless = { services.paperless = {
enable = true; enable = true;
address = "0.0.0.0"; address = "127.0.0.1";
passwordFile = config.sops.secrets.tahani-paperless-password.path; passwordFile = config.sops.secrets.tahani-paperless-password.path;
settings = { settings = {
PAPERLESS_DBENGINE = "sqlite"; PAPERLESS_DBENGINE = "sqlite";

View File

@@ -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";
};
}

View File

@@ -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
''

View File

@@ -79,7 +79,7 @@
ExecStart = "${inputs'.llm-agents.packages.opencode}/bin/opencode serve --port 18822"; ExecStart = "${inputs'.llm-agents.packages.opencode}/bin/opencode serve --port 18822";
Restart = "on-failure"; Restart = "on-failure";
RestartSec = 5; 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 = { Install = {
WantedBy = ["default.target"]; WantedBy = ["default.target"];

View File

@@ -105,7 +105,7 @@
}; };
nix = { nix = {
settings.trusted-users = ["@admin" "cschmatzler"]; settings.trusted-users = ["cschmatzler"];
gc.interval = { gc.interval = {
Weekday = 0; Weekday = 0;
Hour = 2; Hour = 2;

View File

@@ -23,16 +23,19 @@
config = { config = {
flake.flakeModules = { flake.flakeModules = {
# Shared system foundations
core = ./core.nix;
network = ./network.nix;
nixos-system = ./nixos-system.nix;
# User environment
ai-tools = ./ai-tools.nix; ai-tools = ./ai-tools.nix;
atuin = ./atuin.nix; atuin = ./atuin.nix;
core = ./core.nix;
desktop = ./desktop.nix; desktop = ./desktop.nix;
dev-tools = ./dev-tools.nix; dev-tools = ./dev-tools.nix;
email = ./email.nix; email = ./email.nix;
finance = ./finance.nix; finance = ./finance.nix;
neovim = ./neovim.nix; neovim = ./neovim.nix;
network = ./network.nix;
nixos-system = ./nixos-system.nix;
shell = ./shell.nix; shell = ./shell.nix;
ssh-client = ./ssh-client.nix; ssh-client = ./ssh-client.nix;
terminal = ./terminal.nix; terminal = ./terminal.nix;

View File

@@ -378,21 +378,23 @@
ast-grep ast-grep
bun bun
delta delta
deadnix
devenv devenv
docker docker
docker-compose docker-compose
gh gh
git
gnumake gnumake
hyperfine hyperfine
jj-ryu jj-ryu
jj-starship jj-starship
nil
nodejs_24 nodejs_24
nurl nurl
pnpm pnpm
postgresql_17 postgresql_17
serie serie
sqlite sqlite
statix
tea tea
tokei tokei
tree-sitter tree-sitter

View File

@@ -11,180 +11,16 @@
den.aspects.tailscale den.aspects.tailscale
]; ];
den.aspects.michael.nixos = { den.aspects.michael.nixos = {modulesPath, ...}: {
config,
pkgs,
lib,
modulesPath,
...
}: {
imports = [ imports = [
(modulesPath + "/installer/scan/not-detected.nix") (modulesPath + "/installer/scan/not-detected.nix")
./_hosts/michael/backups.nix
./_hosts/michael/disk-config.nix ./_hosts/michael/disk-config.nix
./_hosts/michael/gitea.nix
./_hosts/michael/hardware-configuration.nix ./_hosts/michael/hardware-configuration.nix
inputs.disko.nixosModules.default inputs.disko.nixosModules.default
]; ];
networking.hostName = "michael"; 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
'';
};
}; };
} }

View File

@@ -284,7 +284,6 @@
home.packages = with pkgs; [ home.packages = with pkgs; [
vivid vivid
(callPackage ./_lib/open-project.nix {})
]; ];
}; };
} }

View File

@@ -1,29 +1,30 @@
{...}: { {...}: {
den.aspects.ssh-client.homeManager = { den.aspects.ssh-client.homeManager = {
config, config,
lib,
pkgs, pkgs,
... ...
}: { }: let
homeDir = "${
if pkgs.stdenv.hostPlatform.isDarwin
then "/Users"
else "/home"
}/${config.home.username}";
in {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false; enableDefaultConfig = false;
includes = [ includes = [
(lib.mkIf pkgs.stdenv.hostPlatform.isLinux "/home/${config.home.username}/.ssh/config_external") "${homeDir}/.ssh/config_external"
(lib.mkIf pkgs.stdenv.hostPlatform.isDarwin "/Users/${config.home.username}/.ssh/config_external")
]; ];
matchBlocks = { matchBlocks = {
"*" = {}; "*" = {};
"github.com" = { "github.com" = {
identitiesOnly = true; identitiesOnly = true;
identityFile = [ identityFile = [
(lib.mkIf pkgs.stdenv.hostPlatform.isLinux "/home/${config.home.username}/.ssh/id_ed25519") "${homeDir}/.ssh/id_ed25519"
(lib.mkIf pkgs.stdenv.hostPlatform.isDarwin "/Users/${config.home.username}/.ssh/id_ed25519")
]; ];
}; };
}; };
}; };
home.packages = [pkgs.openssh];
}; };
} }

View File

@@ -28,11 +28,13 @@
tahani-paperless-password = { tahani-paperless-password = {
sopsFile = ../secrets/tahani-paperless-password; sopsFile = ../secrets/tahani-paperless-password;
format = "binary"; format = "binary";
path = "/run/secrets/tahani-paperless-password";
}; };
tahani-email-password = { tahani-email-password = {
sopsFile = ../secrets/tahani-email-password; sopsFile = ../secrets/tahani-email-password;
format = "binary"; format = "binary";
owner = "cschmatzler"; owner = "cschmatzler";
path = "/run/secrets/tahani-email-password";
}; };
}; };
virtualisation.docker.enable = true; virtualisation.docker.enable = true;