feat(actualbudget): add NixOS guest #42

Merged
alexion merged 1 commits from feat/actualbudget-guest into main 2026-08-11 08:38:42 -04:00
4 changed files with 155 additions and 4 deletions

View File

@@ -18,6 +18,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
## Gotchas
- Actual Budget's NixOS service runs `actual-server` with `WorkingDirectory = services.actual.settings.dataDir`, and the `actual-server` CLI wrapper sets `ACTUAL_CONFIG_PATH=./config.json` when that current directory contains `config.json`, overriding the NixOS module's generated config path before the server loads configuration.
When migrating old community-script state to the normal upstream `/data` shape, move or rewrite a copied `/data/config.json` so it cannot shadow the declarative module settings.
- Subagent completion delivery is non-blocking through immediate spawn, milestone notifications, retained terminal entries, and `subagent_list` or `subagent_result` retrieval.
`subagent_wait` intentionally blocks the parent tool call until its condition or timeout, so do not use it merely to keep background work alive during an interactive workflow.
- Nixvim's flake input following the root nixpkgs source does not make its Home Manager module reuse the host's `pkgs` instance.

50
guests/actualbudget.nix Normal file
View File

@@ -0,0 +1,50 @@
{
config,
lib,
my,
...
}:
let
cfg = config.guests.actualbudget;
dataDir = "/data";
in
{
imports = [
(my.guest {
name = "actualbudget";
interior = {
modules.services.actualbudget = {
enable = true;
inherit dataDir;
};
};
})
];
options.guests.actualbudget = {
dataPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/actualbudget";
description = "Host path bind-mounted as Actual Budget's data directory.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.dataPath != null;
message = "guests.actualbudget.dataPath must point at the host directory that stores Actual Budget data.";
}
];
guests.actualbudget = {
mounts.${dataDir}.hostPath = cfg.dataPath;
limits = {
memory = lib.mkDefault "2G";
cpu = lib.mkDefault "200%";
tasksMax = lib.mkDefault 512;
};
};
};
}

View File

@@ -20,14 +20,45 @@
systemd.network = {
enable = true;
networks."10-uplink" = {
matchConfig.MACAddress = "78:55:36:07:af:49";
networkConfig.DHCP = "yes";
linkConfig.RequiredForOnline = "routable";
netdevs = {
"40-br-vlan20".netdevConfig = {
Name = "br-vlan20";
Kind = "bridge";
};
"40-vlan20" = {
netdevConfig = {
Name = "vlan20";
Kind = "vlan";
};
vlanConfig.Id = 20;
};
};
networks = {
"10-uplink" = {
matchConfig = {
Name = "enp2s0";
MACAddress = "78:55:36:07:af:49";
};
networkConfig.DHCP = "yes";
vlan = [ "vlan20" ];
linkConfig.RequiredForOnline = "routable";
};
"40-vlan20" = {
matchConfig.Name = "vlan20";
networkConfig.Bridge = "br-vlan20";
linkConfig.RequiredForOnline = "no";
};
"40-br-vlan20" = {
matchConfig.Name = "br-vlan20";
linkConfig.RequiredForOnline = "no";
};
};
};
networking.useDHCP = false;
modules.network.vlans = [ 20 ];
boot.zfs.forceImportRoot = false;
modules.zfs = {
@@ -43,6 +74,13 @@
modules.git.enable = true;
modules.toolkit.enable = true;
guests.actualbudget = {
enable = true;
vlan = 20;
mac = "BC:24:11:C1:CD:28";
dataPath = "/pikachu/data/actualbudget";
};
environment.systemPackages = with pkgs; [
pciutils
smartmontools

View File

@@ -0,0 +1,61 @@
{
config,
lib,
...
}:
let
cfg = config.modules.services.actualbudget;
in
{
options.modules.services.actualbudget = {
enable = lib.mkEnableOption "Actual Budget";
port = lib.mkOption {
type = lib.types.port;
default = 5006;
description = "Port Actual Budget listens on.";
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/data";
description = "Directory where Actual Budget stores server and user files.";
};
user = lib.mkOption {
type = lib.types.str;
default = "actual";
description = "User account that runs Actual Budget.";
};
uid = lib.mkOption {
type = lib.types.ints.positive;
default = 20001;
description = "Fixed uid for the Actual Budget service user.";
};
group = lib.mkOption {
type = lib.types.str;
default = "storage";
description = "Group account that owns Actual Budget's writable state.";
};
};
config = lib.mkIf cfg.enable {
users.users.${cfg.user} = {
isSystemUser = true;
uid = cfg.uid;
group = cfg.group;
};
services.actual = {
enable = true;
openFirewall = true;
user = cfg.user;
group = cfg.group;
settings = {
inherit (cfg) port dataDir;
};
};
};
}