test: setting up nas structure

This commit is contained in:
Nick 2025-12-09 00:52:21 -06:00
parent 4225970826
commit a92229390e
36 changed files with 275 additions and 1039 deletions

View file

@ -2,7 +2,6 @@
let
inherit (moduleFunctions.instancesFunctions)
sslPath
sopsPath
secretPath
;
@ -12,13 +11,16 @@ in
{
label = label;
name = name;
paths = {
path0 = sslPath;
};
secretPaths = {
path0 = secretPath;
};
sops = {
path0 = "${sopsPath}/${name}";
interfaces = {
interface0 = {
paths = {
mntPaths = {
path0 = sslPath;
};
secretPaths = {
path0 = "${secretPath}/${name}";
};
};
};
};
}

View file

@ -2,37 +2,49 @@
let
inherit (moduleFunctions.instancesFunctions)
domain0
servicePath
mntPath
sslPath
sopsPath
secretPath
;
audiobookshelfLabel = "Audiobookshelf";
audiobookshelfName = "audiobookshelf";
audiobookshelfSubdomain = "books";
audiobookshelfDomain = "${audiobookshelfSubdomain}.${domain0}";
label = "Audiobookshelf";
name = "audiobookshelf";
subdomain = "books";
in
{
label = audiobookshelfLabel;
name = audiobookshelfName;
label = label;
name = name;
short = "Books";
sops = {
path0 = "${sopsPath}/${audiobookshelfName}";
};
domains = {
url0 = audiobookshelfDomain;
};
subdomain = audiobookshelfSubdomain;
tags = [
name
"audiobooks"
"reading"
];
paths = {
path0 = "${servicePath}/${audiobookshelfLabel}";
};
ports = {
port0 = 8000;
};
ssl = {
cert = "${sslPath}/${audiobookshelfSubdomain}.${domain0}/fullchain.pem";
key = "${sslPath}/${audiobookshelfSubdomain}.${domain0}/key.pem";
interfaces = {
interface0 =
let
domain = "${subdomain}.${domain0}";
secret = "${secretPath}/${name}";
ssl = "${sslPath}/${domain}";
in
{
domain = domain;
subdomain = subdomain;
paths = {
mntPaths = {
path0 = "${mntPath}/${name}";
};
secretPaths = {
path0 = secret;
};
};
ssl = {
path = ssl;
cert = "${ssl}/fullchain.pem";
key = "${ssl}/key.pem";
};
};
};
}

View file

@ -1,7 +1,7 @@
{ moduleFunctions }:
let
inherit (moduleFunctions.instancesFunctions)
sopsPath
dummy
;
caddyLabel = "Caddy";
@ -10,9 +10,7 @@ in
{
label = caddyLabel;
name = caddyName;
sops = {
path0 = "${sopsPath}/${caddyName}";
};
short = dummy;
ports = {
port0 = 80;
port1 = 443;

View file

@ -8,8 +8,6 @@ let
;
label = "ComfyUI";
name = "comfyui";
domain = "${name}.${domain0}";
ssl = "${sslPath}/${domain}";
in
{
label = label;
@ -20,24 +18,33 @@ in
"comfy"
"ui"
];
domains = {
url0 = domain;
};
ports = {
port0 = 8188;
};
varPaths = {
path0 = "${varPath}/${name}";
};
mntPaths = {
path0 = "${mntPath}/${name}";
};
secretPaths = {
path0 = "/run/secrets";
};
ssl = {
path = ssl;
cert = "${ssl}/fullchain.pem";
key = "${ssl}/key.pem";
interfaces = {
interface0 =
let
domain = "${name}.${domain0}";
ssl = "${sslPath}/${domain}";
in
{
domain = domain;
paths = {
varPaths = {
path0 = "${varPath}/${name}";
};
mntPaths = {
path0 = "${mntPath}/${name}";
};
secretPaths = {
path0 = "/run/secrets";
};
};
ssl = {
path = ssl;
cert = "${ssl}/fullchain.pem";
key = "${ssl}/key.pem";
};
};
};
}

View file

@ -35,12 +35,12 @@ in
{
microvm = {
id = "vm-${name}";
mac = "02:00:00:00:54:04";
mac = "02:00:00:00:60:70";
idUser = "vmuser-firefly";
macUser = "02:00:00:00:00:04";
ip = "192.168.50.114";
macUser = "02:00:00:00:00:70";
ip = "192.168.50.70";
gate = "192.168.50.1";
ssh = 2204;
ssh = 2570;
};
email = "noreply@${domain0}";
domain = domain;

View file

@ -4,42 +4,50 @@ let
domain0
servicePath
sslPath
sopsPath
secretPath
;
label = "Glance";
name = "glance";
subdomain = "dashboard";
domain = "${subdomain}.${domain0}";
in
{
label = label;
name = name;
short = label;
email = {
address0 = "noreply@${domain0}";
};
sops = {
path0 = "${sopsPath}/${name}";
};
domains = {
url0 = domain;
};
subdomain = subdomain;
tags = [
"glance"
"dashboard"
"weather"
"podcasts"
];
paths = {
path0 = "${servicePath}/${label}";
};
ports = {
port0 = 3434;
};
ssl = {
cert = "${sslPath}/${subdomain}.${domain0}/fullchain.pem";
key = "${sslPath}/${subdomain}.${domain0}/key.pem";
interfaces = {
interface0 =
let
subdomain = "dashboard";
domain = "${subdomain}.${domain0}";
secret = "${secretPath}/${name}";
ssl = "${sslPath}/${domain}";
in
{
domain = domain;
subdomain = subdomain;
email = "noreply@${domain0}";
paths = {
mntPaths = {
path0 = "${servicePath}/${label}";
};
secretPath = {
path0 = secret;
};
};
ssl = {
path = ssl;
cert = "${ssl}/fullchain.pem";
key = "${ssl}/key.pem";
};
};
};
}

View file

@ -1,27 +0,0 @@
{ moduleFunctions }:
let
inherit (moduleFunctions.instancesFunctions)
servicePath
sopsPath
;
label = "Matrix";
name = "matrix";
in
{
label = label;
name = name;
short = label;
sops = {
path0 = "${sopsPath}/${name}";
};
subdomain = name;
tags = [
];
paths = {
path0 = "${servicePath}/${label}";
path1 = "";
path2 = "";
};
}

View file

@ -35,12 +35,12 @@ in
subdomain = short;
microvm = {
id = "vm-${short}";
mac = "02:00:00:00:56:09";
mac = "02:00:00:00:66:76";
idUser = "vmuser-${short}";
macUser = "02:00:00:00:00:09";
ip = "192.168.50.119";
macUser = "02:00:00:00:00:76";
ip = "192.168.50.76";
gate = "192.168.50.1";
ssh = 2209;
ssh = 2576;
};
ssl = {
path = ssl;

View file

@ -1,41 +0,0 @@
{ moduleFunctions }:
let
inherit (moduleFunctions.instancesFunctions)
domain1
servicePath
sslPath
sopsPath
;
label = "Owncast";
name = "owncast";
subdomain = "stream";
domain = "${subdomain}.${domain1}";
in
{
label = label;
name = name;
short = "Cast";
sops = {
path0 = "${sopsPath}/${name}";
};
domains = {
url0 = domain;
};
subdomain = subdomain;
tags = [
];
paths = {
path0 = "${servicePath}/${label}";
path1 = "/mnt/media/storage/${name}";
};
ports = {
port0 = 9454;
port1 = 1935;
};
ssl = {
cert = "${sslPath}/${subdomain}.${domain1}/fullchain.pem";
key = "${sslPath}/${subdomain}.${domain1}/key.pem";
};
}

View file

@ -2,36 +2,24 @@
let
inherit (moduleFunctions.instancesFunctions)
domain1
servicePath
mntPath
sslPath
sopsPath
;
label = "PeerTube";
name = "peertube";
subdomain = "video";
domain = "${subdomain}.${domain1}";
in
{
label = label;
name = name;
short = "Peer";
email = {
address0 = "noreply@${domain1}";
};
sops = {
path0 = "${sopsPath}/${name}";
};
domains = {
url0 = domain;
};
subdomain = subdomain;
email = "noreply@${domain1}";
tags = [
name
"video"
"peer"
"fediverse"
];
paths = {
path0 = "${servicePath}/${label}";
};
ports = {
port0 = 9000; # HTTP
port1 = 1935;
@ -39,8 +27,30 @@ in
port3 = 5432;
port4 = 52800;
};
ssl = {
cert = "${sslPath}/${subdomain}.${domain1}/fullchain.pem";
key = "${sslPath}/${subdomain}.${domain1}/key.pem";
interfaces = {
interface0 =
let
subdomain = "video";
domain = "${subdomain}.${domain1}";
secret = "${sslPath}/${name}";
ssl = "${sslPath}/${domain}";
in
{
domain = domain;
subdomain = subdomain;
paths = {
mntPaths = {
path0 = "${mntPath}/${name}";
};
secretPath = {
path0 = secret;
};
};
ssl = {
path = ssl;
cert = "${ssl}/fullchain.pem";
key = "${ssl}/key.pem";
};
};
};
}

View file

@ -35,12 +35,12 @@ in
subdomain = short;
microvm = {
id = "vm-${short}";
mac = "02:00:00:00:56:11";
mac = "02:00:00:00:69:79";
idUser = "vmuser-${short}";
macUser = "02:00:00:00:00:11";
ip = "192.168.50.121";
macUser = "02:00:00:00:00:79";
ip = "192.168.50.79";
gate = "192.168.50.1";
ssh = 2211;
ssh = 2579;
};
ssl = {
path = ssl;

View file

@ -2,43 +2,51 @@
let
inherit (moduleFunctions.instancesFunctions)
domain0
servicePath
mntPath
secretPath
sslPath
sopsPath
;
label = "SearXNG";
name = "searx";
subdomain = "search";
domain = "${subdomain}.${domain0}";
in
{
label = label;
name = name;
short = "Sear";
email = {
address0 = "noreply@${domain0}";
};
sops = {
path0 = "${sopsPath}/${name}";
};
domains = {
url0 = domain;
};
subdomain = subdomain;
tags = [
"search"
"sear"
"searx"
];
paths = {
path0 = "${servicePath}/${label}";
};
ports = {
port0 = 8888;
};
ssl = {
cert = "${sslPath}/${subdomain}.${domain0}/fullchain.pem";
key = "${sslPath}/${subdomain}.${domain0}/key.pem";
interfaces = {
interface0 =
let
subdomain = "search";
domain = "${subdomain}.${domain0}";
ssl = "${sslPath}/${domain}";
secret = "${secretPath}/${name}";
in
{
domain = domain;
email = "noreply@${domain0}";
subdomain = subdomain;
paths = {
mntPaths = {
path0 = "${mntPath}/${name}";
};
secretPaths = {
path0 = secret;
};
};
ssl = {
path = ssl;
cert = "${ssl}/fullchain.pem";
key = "${ssl}/key.pem";
};
};
};
}

View file

@ -38,12 +38,12 @@ in
subdomain = name;
microvm = {
id = "vm-${id}";
mac = "02:00:00:00:56:10";
mac = "02:00:00:00:75:85";
idUser = "vmuser-${id}";
macUser = "02:00:00:00:00:10";
ip = "192.168.50.120";
macUser = "02:00:00:00:00:85";
ip = "192.168.50.85";
gate = "192.168.50.1";
ssh = 2210;
ssh = 2585;
};
ssl = {
cert = "${ssl}/fullchain.pem";

View file

@ -86,6 +86,11 @@ in
nas
sambaEris
;
inherit (modules.guests.firefly-iii) fireflyNick;
inherit (modules.guests.opencloud) opencloudNick;
inherit (modules.guests.photoprism) photoprismNick;
inherit (modules.guests.vaultwarden) vaultwardenNick;
inherit (modules.guests.syncthing) syncthingNick;
};
};

View file

@ -6,7 +6,7 @@ let
inherit (flake.config.people) user0;
inherit (flake.config.people.users.${user0}) email;
inherit (flake.config.services) instances;
service = instances.acme;
serviceCfg = instances.acme;
dns0 = instances.web.dns.provider0;
dns1 = instances.web.dns.provider1;
in
@ -29,7 +29,7 @@ in
"pass"
];
sopsPath = secret: dns: {
path = "/var/lib/secrets/${instances.acme.name}/${dns}-${secret}";
path = "/var/lib/secrets/${serviceCfg.name}/${dns}-${secret}";
owner = "root";
mode = "600";
};
@ -50,7 +50,7 @@ in
systemd = {
tmpfiles.rules = [
"Z ${service.sops.path0} 755 ${service.name} ${service.name} -"
"Z ${serviceCfg.secretPaths.path0} 755 ${serviceCfg.name} ${serviceCfg.name} -"
];
};
}

View file

@ -1,7 +1,7 @@
{ flake, ... }:
let
inherit (flake.config.services) instances;
service = instances.caddy;
serviceCfg = instances.caddy;
importList =
let
@ -26,8 +26,8 @@ in
networking = {
firewall = {
allowedTCPPorts = [
service.ports.port0 # 80
service.ports.port1 # 443
serviceCfg.ports.port0 # 80
serviceCfg.ports.port1 # 443
];
};
};

View file

@ -6,12 +6,15 @@
...
}:
let
inherit (labHelpers) guestPath;
inherit (import ./config { inherit config flake pkgs; }) fireflyVM;
inherit (flake.config.people) user0;
inherit (flake.config.people.users.${user0}) email;
inherit (flake.config.services.instances) firefly-iii;
interface0Cfg = firefly-iii.interfaces.interface0;
in
{
fireflyNick = fireflyVM {
user = user0;
ip = interface0Cfg.microvm.ip;
@ -19,7 +22,7 @@ let
userMac = interface0Cfg.microvm.macUser;
ssh = interface0Cfg.microvm.ssh0;
host = interface0Cfg.domain;
mnt = "";
mnt = guestPath user0;
owner = email.address2;
};
@ -44,7 +47,4 @@ let
# host = "";
# owner = "";
# };
in
fireflyNick
# // fireflyStacie // fireflyGarnet
}

View file

@ -5,21 +5,21 @@
...
}:
let
inherit (labHelpers) mntPath;
inherit (import ./config { inherit flake pkgs; }) forgejoVM;
inherit (flake.config.people) user0;
inherit (flake.config.services) instances;
interface0Cfg = instances.forgejo.interfaces.interface0;
in
{
forgejoNick = forgejoVM {
user = user0;
ip = interface0Cfg.microvm.ip;
mac = interface0Cfg.microvm.mac;
userMac = interface0Cfg.microvm.macUser;
ssh = interface0Cfg.microvm.ssh;
mnt = "";
mnt = mntPath;
host = interface0Cfg.domain;
};
in
forgejoNick
# // forgejoStacie // forgejoGarnet
}

View file

@ -1,5 +1,6 @@
{
labHelpers = {
mntPath = "/mnt/storage";
guestPath = user: "/mnt/storage/users/${user}/guests";
docsPath = user: "/mnt/storage/users/${user}/home/docs";
mediaPath = user: "/mnt/storage/users/${user}/home/media";

View file

@ -5,21 +5,22 @@
...
}:
let
inherit (labHelpers) mntPath;
inherit (import ./config { inherit flake pkgs; }) jellyfinVM;
inherit (flake.config.people) user0;
inherit (flake.config.services) instances;
interface0Cfg = instances.jellyfin.interfaces.interface0;
in
{
jellyfinNick = jellyfinVM {
user = user0;
ip = interface0Cfg.microvm.ip;
mac = interface0Cfg.microvm.mac;
userMac = interface0Cfg.microvm.macUser;
ssh = interface0Cfg.microvm.ssh;
mnt = "";
mnt = mntPath;
host = interface0Cfg.domain;
};
in
jellyfinNick
# // forgejoStacie // forgejoGarnet
}

View file

@ -5,21 +5,22 @@
...
}:
let
inherit (labHelpers) mntPath;
inherit (import ./config { inherit flake pkgs; }) mastodonVM;
inherit (flake.config.people) user0;
inherit (flake.config.services) instances;
interface0Cfg = instances.mastodon.interfaces.interface0;
in
{
mastodonNick = mastodonVM {
user = user0;
ip = interface0Cfg.microvm.ip;
mac = interface0Cfg.microvm.mac;
userMac = interface0Cfg.microvm.macUser;
ssh = interface0Cfg.microvm.ssh;
mnt = "";
mnt = mntPath;
host = interface0Cfg.domain;
};
in
mastodonNick
# // mastodonStacie // mastodonGarnet
}

View file

@ -5,10 +5,13 @@
...
}:
let
inherit (labHelpers) mntPath;
inherit (import ./config { inherit flake pkgs; }) minecraftVM;
inherit (flake.config.services) instances;
inherit (flake.config.people) user0;
in
{
minecraftNick01 =
let
interfaceCfg = instances.minecraft.interfaces.interface0;
@ -20,7 +23,7 @@ let
userMac = interfaceCfg.microvm.macUser;
ssh = interfaceCfg.microvm.ssh;
port = interfaceCfg.microvm.port;
mnt = "";
mnt = mntPath;
worldNumber = "01";
config = {
allow-flight = false;
@ -67,7 +70,7 @@ let
userMac = interfaceCfg.microvm.macUser;
ssh = interfaceCfg.microvm.ssh;
port = interfaceCfg.microvm.port;
mnt = "";
mnt = mntPath;
worldNumber = "02";
config = {
allow-flight = false;
@ -101,5 +104,4 @@ let
};
};
in
minecraftNick01 // minecraftNick02
}

View file

@ -5,22 +5,36 @@
...
}:
let
inherit (labHelpers) mntPath guestPath;
inherit (import ./config { inherit flake pkgs; }) opencloudVM;
inherit (flake.config.people) user0;
inherit (flake.config.services.instances) opencloud;
interface0Cfg = opencloud.interfaces.interface0;
interface1Cfg = opencloud.interfaces.interface1;
in
{
opencloudNick = opencloudVM {
user = user0;
ip = interface0Cfg.microvm.ip;
mac = interface0Cfg.microvm.mac;
userMac = interface0Cfg.microvm.macUser;
ssh = interface0Cfg.microvm.ssh;
mnt = "";
mnt = guestPath user0;
host = interface0Cfg.domain;
};
opencloudProject = opencloudVM {
user = user0;
ip = interface1Cfg.microvm.ip;
mac = interface1Cfg.microvm.mac;
userMac = interface1Cfg.microvm.macUser;
ssh = interface1Cfg.microvm.ssh;
mnt = mntPath;
host = "${interface0Cfg.subdomain}.${flake.inputs.linkpage.secrets.domains.projectsite}";
};
# opencloudStacie = opencloudVM {
# user = "stacie";
# ip = ipAddress id1;
@ -40,7 +54,4 @@ let
# mnt = guestPath "garnet";
# host = "";
# };
in
opencloudNick
# // opencloudStacie // opencloudGarnet
}

View file

@ -38,7 +38,6 @@ in
passwordFile = "/run/secrets/${user}-pass";
storagePath = "/var/lib/${serviceCfg.name}";
originalsPath = "/var/lib/${serviceCfg.name}-media";
importPath = "photos";
address = "0.0.0.0";
};
openssh = {

View file

@ -5,19 +5,22 @@
...
}:
let
inherit (labHelpers) guestPath mediaPath;
inherit (import ./config { inherit flake lib; }) photoprismVM;
inherit (flake.config.services.instances) photoprism;
inherit (flake.config.people) user0;
interface0Cfg = photoprism.interfaces.interface0;
in
{
photoprismNick = photoprismVM {
user = user0;
ip = interface0Cfg.microvm.ip;
mac = interface0Cfg.microvm.mac;
userMac = interface0Cfg.microvm.macUser;
ssh = interface0Cfg.microvm.ssh;
mnt = "";
data = "";
mnt = guestPath user0;
data = mediaPath user0;
host = interface0Cfg.domain;
};
@ -41,6 +44,4 @@ let
# data = mediaPath "stacie";
# };
in
photoprismNick
# // photoprismStacie // photoprismGarnet
}

View file

@ -5,19 +5,22 @@
...
}:
let
inherit (labHelpers) mntPath;
inherit (import ./config { inherit flake pkgs; }) qbittorrentVM;
inherit (flake.config.people) user0;
inherit (flake.config.services) instances;
interface0Cfg = instances.qbittorrent.interfaces.interface0;
qbittorrentNick = qbittorrentVM {
in
{
qbittorrentCeres = qbittorrentVM {
user = user0;
ip = interface0Cfg.microvm.ip;
mac = interface0Cfg.microvm.mac;
userMac = interface0Cfg.microvm.macUser;
ssh = interface0Cfg.microvm.ssh;
mnt = "";
mnt = mntPath;
host = interface0Cfg.domain;
port = 51820;
endpoint = "185.111.110.1";
@ -25,6 +28,4 @@ let
dns = [ "10.2.0.1" ];
key = "QPfiwJQmt5VLEOh1ufLbi1lj6LUnwQY0tgDSh3pWx1k=";
};
in
qbittorrentNick
# // qbittorrentStacie // qbittorrentGarnet
}

View file

@ -4,6 +4,12 @@
...
}:
let
inherit (labHelpers)
docsPath
guestPath
mediaPath
miscPath
;
inherit (import ./config { inherit flake; }) syncthingVM;
inherit (flake.config.services) instances;
inherit (flake.config.people) user0;
@ -52,19 +58,19 @@ let
{
mountPoint = "/var/lib/${serviceCfg.name}/docs";
proto = "virtiofs";
source = "";
source = docsPath user;
tag = "${serviceCfg.name}_${user}_docs";
}
{
mountPoint = "/var/lib/${serviceCfg.name}/media";
proto = "virtiofs";
source = "";
source = mediaPath user;
tag = "${serviceCfg.name}_${user}_media";
}
{
mountPoint = "/var/lib/${serviceCfg.name}/misc";
proto = "virtiofs";
source = "";
source = miscPath user;
tag = "${serviceCfg.name}_${user}_misc";
}
];
@ -75,6 +81,8 @@ let
"d /var/lib/${serviceCfg.name}/misc 0755 ${serviceCfg.name} ${serviceCfg.name} -"
];
in
{
syncthingNick =
let
phoneID = "OALKHLZ-OODUWVX-PAC2LI7-UMZMSZO-FELLRCD-RS4DHJS-PVA5YQK-WTFXXQI";
@ -85,7 +93,7 @@ let
mac = interface0Cfg.microvm.mac;
userMac = interface0Cfg.microvm.macUser;
ssh = interface0Cfg.microvm.ssh;
mnt = "";
mnt = guestPath user0;
host = interface0Cfg.domain;
folders = foldersHelper user0;
devices = devicesHelper user0 phoneID "Phone" "192.168.50.8";
@ -112,7 +120,4 @@ let
# syncID = "";
# deviceIP = "";
# };
in
syncthingNick
# // syncthingStacie // syncthingGarnet
}

View file

@ -4,19 +4,22 @@
...
}:
let
inherit (labHelpers) guestPath;
inherit (import ./config { inherit flake; }) vaultwardenVM;
inherit (flake.config.people) user0;
inherit (flake.config.services.instances) vaultwarden;
interface0Cfg = vaultwarden.interfaces.interface0;
in
{
vaultwardenNick = vaultwardenVM {
user = user0;
ip = interface0Cfg.microvm.ip;
mac = interface0Cfg.microvm.mac;
userMac = interface0Cfg.microvm.macUser;
ssh = interface0Cfg.microvm.ssh;
mnt = "";
mnt = guestPath user0;
host = interface0Cfg.domain;
};
@ -40,6 +43,4 @@ let
# host = "";
# };
in
vaultwardenNick
# // vaultwardenStacie // vaultwardenGarnet
}

View file

@ -8,7 +8,9 @@ let
inherit (import ./config { inherit flake pkgs; }) websiteVM;
inherit (flake.config.services) instances;
websiteNick =
in
{
websiteUpRoot =
let
websitePkg = flake.self.packages.${pkgs.system}.website;
interfaceCfg = instances.website.interfaces.interface0;
@ -34,5 +36,4 @@ let
package = websitePkg;
};
in
websiteNick // websiteProject
}

View file

@ -9,7 +9,9 @@ let
inherit (flake.config.services) instances;
interfaceCfg = instances.zookeeper.interfaces.interface0;
zookeeperNick =
in
{
zookeeperBot =
let
appPackage = flake.self.packages.${pkgs.system}.zookeeper;
in
@ -21,5 +23,4 @@ let
package = appPackage;
};
in
zookeeperNick
}

View file

@ -24,20 +24,18 @@ in
};
paths =
let
inst = instance: instances.${instance}.mntPaths.path0;
inst = instance: interface: instances.${instance}.interfaces.${interface}.paths.mntPaths.path0;
in
[
"/home/${user0}/.ssh"
(inst "firefly-iii")
(inst "forgejo")
(inst "mastodon")
(inst "opencloud1")
(inst "minecraft0")
(inst "minecraft1")
(inst "vaultwarden")
((inst "jellyfin") + "/cache")
((inst "jellyfin") + "/data")
((inst "jellyfin") + "/media/music")
(inst "forgejo" "interface0")
(inst "mastodon" "interface0")
(inst "opencloud" "interface1")
(inst "minecraft" "interface0")
(inst "minecraft" "interface1")
((inst "jellyfin" "interface0") + "/cache")
((inst "jellyfin" "interface0") + "/data")
((inst "jellyfin" "interface0") + "/media/music")
];
};
};

View file

@ -1,29 +0,0 @@
{
flake,
config,
pkgs,
lib,
...
}:
let
serverHelpers = {
ipAddress = ip: "192.168.50.${ip}";
mntPath = "/mnt/storage";
minecraft = {
id0 = 40;
id1 = 41;
id2 = 42;
ssh0 = 2440;
ssh1 = 2441;
ssh2 = 2442;
};
};
minecraft = import ./guests/firefly-iii { inherit serverHelpers; };
in
{
imports = [
minecraft
];
}

View file

@ -1,11 +0,0 @@
let
importList =
let
content = builtins.readDir ./.;
dirContent = builtins.filter (n: content.${n} == "directory") (builtins.attrNames content);
in
map (name: ./. + "/${name}") dirContent;
in
{
imports = importList;
}

View file

@ -1,84 +0,0 @@
{
flake,
lib,
pkgs,
...
}:
let
inherit (flake.config.machines.devices) ceres;
inherit (flake.config.services) instances;
service = instances.postgresql;
# backupPath = "${instances.syncthing.paths.path1}/${service.name}";
in
{
services = {
postgresqlBackup = {
enable = true;
# location = backupPath;
# compression = "zstd";
startAt = "*-*-* 07:00:00";
databases = [
instances.mastodon.name
instances.firefly-iii.name
];
};
postgresql = {
enable = true;
};
};
networking = {
firewall = {
allowedTCPPorts = [
service.ports.port0
];
};
};
fileSystems."/var/lib/postgresql" = {
device = service.paths.path0;
fsType = "none";
options = [
"bind"
];
depends = [
ceres.storage0.mount
];
};
users.users.${service.name}.extraGroups = [
instances.mastodon.name
instances.forgejo.name
instances.syncthing.name
];
systemd.services.sync-postgres-backups = {
description = "Sync PostgreSQL backups to Syncthing";
after = [
"postgresqlBackup-firefly-iii.service"
"postgresqlBackup-mastodon.service"
];
serviceConfig = {
Type = "oneshot";
User = instances.syncthing.name;
Group = instances.syncthing.name;
};
script = ''
${pkgs.rsync}/bin/rsync -av --delete \
/var/backup/postgresql/ \
${instances.syncthing.paths.path1}/${service.name}/
${pkgs.rsync}/bin/rsync -av --delete \
/var/lib/${instances.firefly-iii.name}/storage/ \
${instances.syncthing.paths.path1}/${service.name}/firefly-iii-storage/
'';
};
systemd.timers.sync-postgres-backups = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 22:10:00"; # 10 mins after backup
Persistent = true;
};
};
}

View file

@ -1,29 +0,0 @@
{
flake,
...
}:
let
inherit (flake.config.services) instances;
service = instances.postgresql;
in
{
services = {
postgresqlBackup = {
enable = true;
startAt = "*-*-* 07:00:00";
};
postgresql = {
enable = true;
};
};
networking = {
firewall = {
allowedTCPPorts = [
service.ports.port0
];
};
};
}

View file

@ -1,616 +0,0 @@
# ============================================================================
# MicroVM Service Template - Production-Ready Configuration
# ============================================================================
# This template is based on proven, working configurations from:
# - Vaultwarden (simple service with environment file)
# - Forgejo (complex service with separate secret files)
# - Jellyfin (media service with multiple mounts and tmpfiles)
#
# CRITICAL SUCCESS FACTORS (learned from production deployments):
# 1. Use serviceCfg.name for all service references (not hardcoded strings)
# 2. Secrets MUST use service-specific subdirectories: /run/secrets/${serviceCfg.name}/
# 3. Host directories MUST exist with correct permissions BEFORE VM starts
# 4. Use 0777 permissions when VM service runs as non-root user with different UID
# 5. systemd.tmpfiles can be used INSIDE the VM for VM-internal directories
# 6. Host and VM tmpfiles rules serve different purposes - use both when needed
#
# Architecture Overview:
# ┌────────────────────────────────────────────────┐
# │ Host (NixOS Server) │
# │ │
# │ ┌──────────────┐ ┌──────────────┐ │
# │ │ Caddy │ │ br-vms │ │
# │ │ (Reverse │──────│ Bridge │ │
# │ │ Proxy) │ │ 192.168.50 │ │
# │ │ TLS Term │ │ .240 │ │
# │ └──────────────┘ └──────┬───────┘ │
# │ :443 │ │
# │ │ ┌─────▼──────┐ │
# │ │ │ vm-NAME │ │
# │ │ │ (TAP) │ │
# │ │ └─────┬──────┘ │
# │ │ │ │
# │ ┌─────▼──────────────────────▼────────────┐ │
# │ │ │ │
# │ │ MicroVM Guest │ │
# │ │ ┌────────────┐ ┌────────────┐ │ │
# │ │ │ Service │ │ enp0s5 │ │ │
# │ │ │ :PORT │ │192.168.50 │ │ │
# │ │ │ │ │ .1XX │ │ │
# │ │ └────────────┘ └────────────┘ │ │
# │ │ │ │
# │ │ VirtioFS Mounts: │ │
# │ │ • /nix/.ro-store → Host /nix/store │ │
# │ │ • /var/lib/NAME → Host /mnt/storage │ │
# │ │ • /run/secrets → Host /run/secrets/NAME│ │
# │ └─────────────────────────────────────────┘ │
# │ │
# └────────────────────────────────────────────────┘
#
# Network Flow:
# 1. Internet → Router:443 (port forward) → Host:443 (Caddy)
# 2. Caddy terminates TLS using ACME certificates
# 3. Caddy proxies HTTP to VM's LAN IP (e.g., 192.168.50.151:8085)
# 4. Request: br-vms → TAP (vm-NAME) → VM enp0s5 → Service
# 5. Response follows same path in reverse
#
# IMPORTANT: Split-DNS for LAN Access
# - External users: DNS resolves to public IP → router forwards to host
# - Internal users: MUST have DNS resolve to 192.168.50.240 (host bridge IP)
# OR use /etc/hosts entries, otherwise NAT hairpinning may fail
#
# ============================================================================
{
config,
flake,
...
}:
let
# ============================================================================
# CONFIGURATION REFERENCES
# ============================================================================
# These pull from your centralized instance definitions
# Located in: modules/config/instances/config/*.nix
inherit (flake.config.people) user0;
inherit (flake.config.services) instances;
# REPLACE 'service' with your actual service name identifier
# This should match the attribute name in your instances configuration
# Examples: vaultwarden, forgejo, jellyfin, etc.
serviceCfg = instances.service; # CHANGE THIS
# SMTP configuration (if your service needs email)
# Remove this line if your service doesn't use SMTP
smtpCfg = instances.smtp;
# Host/web configuration for routing and DNS
hostCfg = instances.web;
# Service domain (e.g., "service.example.com")
host = serviceCfg.domains.url0;
# DNS provider for ACME DNS-01 challenge
dns0 = instances.web.dns.provider0;
dns0Path = "dns/${dns0}";
in
{
# ============================================================================
# HOST-SIDE CONFIGURATION
# ============================================================================
# These configurations run on the HOST, not inside the VM
# They MUST be configured BEFORE the VM starts
# ──────────────────────────────────────────────────────────────────────────
# 1. Caddy Group Membership
# ──────────────────────────────────────────────────────────────────────────
# Allow Caddy to read ACME certificates
users.users.caddy.extraGroups = [ "acme" ];
# ──────────────────────────────────────────────────────────────────────────
# 2. ACME TLS Certificate
# ──────────────────────────────────────────────────────────────────────────
# Request Let's Encrypt certificate using DNS-01 challenge
security.acme.certs."${host}" = {
dnsProvider = dns0;
environmentFile = config.sops.secrets.${dns0Path}.path;
group = "caddy";
};
# ──────────────────────────────────────────────────────────────────────────
# 3. Host Storage Directories
# ──────────────────────────────────────────────────────────────────────────
# Create directories on the host BEFORE VM starts
# The VM will mount these via VirtioFS
#
# PERMISSION PATTERNS (choose based on your service):
# - 0755: Safe default when VM service runs as root
# - 0777: Required when VM service runs as non-root with different UID
# (e.g., jellyfin runs as UID 999 inside VM)
#
# EXAMPLES FROM WORKING CONFIGS:
# Vaultwarden: "d ${serviceCfg.mntPaths.path0} 0777 root root -"
# Forgejo: "d ${serviceCfg.mntPaths.path0} 0777 root root -"
# Jellyfin: "d ${serviceCfg.mntPaths.path0} 0777 root root -"
# "d ${serviceCfg.mntPaths.path0}/cache 0777 root root -"
systemd.tmpfiles.rules = [
# Main data directory
"d ${serviceCfg.mntPaths.path0} 0777 root root -"
# OPTIONAL: Additional directories if needed (like Jellyfin's cache)
# "d ${serviceCfg.mntPaths.path0}/cache 0777 root root -"
];
# ──────────────────────────────────────────────────────────────────────────
# 4. Secrets Management (SOPS)
# ──────────────────────────────────────────────────────────────────────────
# Configure secrets decryption on the host
# SOPS-nix will decrypt these to /run/secrets/${serviceCfg.name}/*
#
# CRITICAL: Always use ${serviceCfg.name} for the path prefix!
# This prevents conflicts between multiple VMs
#
# PATTERN 1: Single environment file (like Vaultwarden)
# sops.secrets = {
# "${serviceCfg.name}/env" = {
# owner = "root";
# mode = "0600";
# };
# };
#
# PATTERN 2: Multiple secret files (like Forgejo)
# sops.secrets = {
# "${serviceCfg.name}/smtp" = {
# owner = "root";
# mode = "0600";
# };
# "${serviceCfg.name}/database" = {
# owner = "root";
# mode = "0600";
# };
# };
#
# PATTERN 3: No secrets (like Jellyfin - if service doesn't need secrets)
# sops.secrets = {};
sops.secrets = {
# CHOOSE ONE OF THE PATTERNS ABOVE AND UNCOMMENT
# "${serviceCfg.name}/env" = {
# owner = "root";
# mode = "0600";
# };
};
# ============================================================================
# CADDY REVERSE PROXY (Host-Side)
# ============================================================================
# Caddy terminates TLS and proxies to the VM
services.caddy.virtualHosts."${host}" = {
extraConfig = ''
# Forward all requests to the VM's IP and port
reverse_proxy ${serviceCfg.interface.ip}:${toString serviceCfg.ports.port0} {
# Pass the real client IP to the service
header_up X-Real-IP {remote_host}
}
# Use ACME certificate managed by NixOS
tls ${serviceCfg.ssl.cert} ${serviceCfg.ssl.key}
# Compress responses
encode zstd gzip
'';
};
# ============================================================================
# MICROVM DEFINITION
# ============================================================================
# Everything below defines the VM's configuration
microvm.vms.${serviceCfg.name} = {
# VM Lifecycle
autostart = true; # Start VM automatically on host boot
restartIfChanged = true; # Restart VM when configuration changes
# ──────────────────────────────────────────────────────────────────────
# VM Guest Configuration
# ──────────────────────────────────────────────────────────────────────
# Everything inside 'config' runs INSIDE the VM
config = {
# NixOS version (should match host or be compatible)
system.stateVersion = "24.05";
# Timezone (should match host for consistent logging)
time.timeZone = "America/Winnipeg";
# SSH Access - allow SSH into VM using host user's keys
users.users.root.openssh.authorizedKeys.keys = flake.config.people.users.${user0}.sshKeys;
# ────────────────────────────────────────────────────────────────────
# Services Configuration
# ────────────────────────────────────────────────────────────────────
services = {
# ══════════════════════════════════════════════════════════════════
# YOUR SERVICE CONFIGURATION GOES HERE
# ══════════════════════════════════════════════════════════════════
# Choose one of the patterns below based on your service type:
# ┌─────────────────────────────────────────────────────────────────
# │ PATTERN 1: Simple Service (Vaultwarden-style)
# │ - Uses environment file for secrets
# │ - Single configuration block
# └─────────────────────────────────────────────────────────────────
# vaultwarden = {
# enable = true;
# dbBackend = "sqlite";
# config = {
# DOMAIN = "https://${host}";
#
# # Email Configuration
# SMTP_AUTH_MECHANISM = "Plain";
# SMTP_EMBED_IMAGES = true;
# SMTP_FROM = serviceCfg.email.address0;
# SMTP_FROM_NAME = serviceCfg.label;
# SMTP_HOST = smtpCfg.hostname;
# SMTP_PORT = smtpCfg.ports.port1;
# SMTP_SECURITY = smtpCfg.records.record1;
# SMTP_USERNAME = smtpCfg.email.address0;
#
# # Security Configuration
# DISABLE_ADMIN_TOKEN = false;
#
# # Web Server Settings
# ROCKET_ADDRESS = "0.0.0.0";
# ROCKET_PORT = serviceCfg.ports.port0;
# };
#
# # Mount secrets from host
# environmentFile = "/run/secrets/env";
# };
# ┌─────────────────────────────────────────────────────────────────
# │ PATTERN 2: Complex Service (Forgejo-style)
# │ - Uses separate secret files
# │ - More complex settings structure
# └─────────────────────────────────────────────────────────────────
# forgejo = {
# enable = true;
# lfs.enable = true;
#
# # Separate secret files (not environment variables)
# secrets = {
# mailer.PASSWD = "/run/secrets/smtp";
# };
#
# dump = {
# interval = "5:00";
# type = "zip";
# file = "forgejo-backup";
# enable = true;
# };
#
# settings = {
# server = {
# DOMAIN = host;
# ROOT_URL = "https://${host}/";
# HTTP_PORT = serviceCfg.ports.port0;
# };
#
# service.DISABLE_REGISTRATION = true;
#
# actions = {
# ENABLED = true;
# DEFAULT_ACTIONS_URL = "github";
# };
#
# mirror = {
# ENABLED = true;
# };
#
# mailer = {
# ENABLED = true;
# SMTP_ADDR = smtpCfg.hostname;
# FROM = smtpCfg.email.address1;
# USER = smtpCfg.email.address1;
# PROTOCOL = "${smtpCfg.name}+${smtpCfg.records.record1}";
# SMTP_PORT = smtpCfg.ports.port1;
# SEND_AS_PLAIN_TEXT = true;
# USE_CLIENT_CERT = false;
# };
# };
# };
# ┌─────────────────────────────────────────────────────────────────
# │ PATTERN 3: Media Service (Jellyfin-style)
# │ - Simple enable, uses openFirewall
# │ - No secrets needed
# └─────────────────────────────────────────────────────────────────
# jellyfin = {
# enable = true;
# openFirewall = true;
# };
# ══════════════════════════════════════════════════════════════════
# SSH Server (for VM management) - ALWAYS INCLUDE
# ══════════════════════════════════════════════════════════════════
openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "prohibit-password";
};
};
};
# ────────────────────────────────────────────────────────────────────
# Firewall Configuration
# ────────────────────────────────────────────────────────────────────
# Open necessary ports inside the VM
#
# EXAMPLES:
# Vaultwarden: [22, 25, 139, 587, 2525, serviceCfg.ports.port0]
# Forgejo: [22, 25, 139, 587, 2525, serviceCfg.ports.port0]
# Jellyfin: [22, serviceCfg.ports.port0, port1, port2]
networking.firewall.allowedTCPPorts = [
22 # SSH (always include)
serviceCfg.ports.port0 # Main service port
# OPTIONAL: SMTP ports if service sends email
# 25 # SMTP
# 587 # SMTP Submission
# 2525 # Alternative SMTP
# OPTIONAL: Additional ports (like Jellyfin's discovery ports)
# serviceCfg.ports.port1
# serviceCfg.ports.port2
];
# ────────────────────────────────────────────────────────────────────
# OPTIONAL: Temporary Filesystem
# ────────────────────────────────────────────────────────────────────
# Some services need a large /tmp (e.g., Forgejo for large Git operations)
# Jellyfin also uses this pattern
#
# UNCOMMENT IF NEEDED:
# fileSystems."/tmp" = {
# device = "tmpfs";
# fsType = "tmpfs";
# options = [
# "size=4G" # Adjust size as needed
# "mode=1777" # Sticky bit for multi-user access
# ];
# };
# ────────────────────────────────────────────────────────────────────
# Systemd Configuration
# ────────────────────────────────────────────────────────────────────
systemd = {
# Network Configuration (systemd-networkd)
# Configure the VM's network interface
#
# INTERFACE NAME: "enp0s5" is typical for QEMU with q35 machine
# To verify: SSH into VM and run: ip link show
network = {
enable = true;
networks."20-lan" = {
# Match the network interface created by QEMU
matchConfig.Name = "enp0s5";
# Static IP Configuration
# MUST be on same subnet as host bridge (e.g., 192.168.50.0/24)
addresses = [
{ Address = "${serviceCfg.interface.ip}/24"; }
];
# Default route for internet access
# PATTERN 1: Use "0.0.0.0/0" (Forgejo)
# PATTERN 2: Use "${hostCfg.localhost.address1}/0" (Vaultwarden, Jellyfin)
# Both work - choose one
routes = [
{
Destination = "0.0.0.0/0"; # or "${hostCfg.localhost.address1}/0"
Gateway = serviceCfg.interface.gate;
}
];
# DNS servers
dns = [
"1.1.1.1" # Cloudflare
"8.8.8.8" # Google
];
};
};
# OPTIONAL: VM-internal tmpfiles (Jellyfin and Forgejo use this)
# This creates directories INSIDE the VM, separate from host tmpfiles
# Used when the service needs specific ownership/permissions inside VM
#
# EXAMPLES:
# Jellyfin: "d ${serviceCfg.varPaths.path0}/media 0755 ${serviceCfg.name} ${serviceCfg.name} -"
# Forgejo: "d ${serviceCfg.varPaths.path0} 0755 ${serviceCfg.name} ${serviceCfg.name} -"
#
# UNCOMMENT IF NEEDED:
# tmpfiles.rules = [
# "d ${serviceCfg.varPaths.path0} 0755 ${serviceCfg.name} ${serviceCfg.name} -"
# ];
};
# Ensure systemd-networkd starts on boot
systemd.services.systemd-networkd.wantedBy = [ "multi-user.target" ];
# ────────────────────────────────────────────────────────────────────
# MicroVM Hardware Configuration
# ────────────────────────────────────────────────────────────────────
microvm = {
# Virtual CPU cores
# Adjust based on service needs:
# - Light services (Vaultwarden, Forgejo): 2 cores
# - Heavy services (Jellyfin): 6 cores
vcpu = 2;
# Memory in MB
# Adjust based on service needs:
# - Light services (Vaultwarden): 3072 MB (3 GB)
# - Medium services (Forgejo): 3072 MB (3 GB)
# - Heavy services (Jellyfin): 8192 MB (8 GB)
mem = 3072;
# Hypervisor - QEMU with KVM provides best performance
hypervisor = "qemu";
# ──────────────────────────────────────────────────────────────────
# Network Interfaces
# ──────────────────────────────────────────────────────────────────
# All working configs use TWO interfaces: TAP + User-mode
interfaces = [
# Primary Interface: TAP (LAN Connectivity)
{
type = "tap";
id = serviceCfg.interface.id; # e.g., "vm-service"
mac = serviceCfg.interface.mac; # e.g., "02:00:00:00:00:51"
}
# Secondary Interface: User-mode (NAT/Fallback)
{
type = "user";
id = serviceCfg.interface.idUser; # e.g., "vmuser-service"
mac = serviceCfg.interface.macUser; # e.g., "02:00:00:00:01:51"
}
];
# ──────────────────────────────────────────────────────────────────
# Port Forwarding (Host → VM)
# ──────────────────────────────────────────────────────────────────
# Forward SSH from host to VM for easy access
# Access via: ssh -p 22XX root@localhost (from host)
forwardPorts = [
{
from = "host";
host.port = serviceCfg.interface.ssh; # e.g., 2201
guest.port = 22;
}
];
# ──────────────────────────────────────────────────────────────────
# Shared Directories (VirtioFS)
# ──────────────────────────────────────────────────────────────────
# Share directories from host to VM
#
# IMPORTANT: All source paths must exist on host BEFORE VM starts
# Use systemd.tmpfiles.rules (in host section) to create directories
shares = [
# ┌───────────────────────────────────────────────────────────────
# │ Nix Store (Read-Only) - ALWAYS INCLUDE
# └───────────────────────────────────────────────────────────────
{
mountPoint = "/nix/.ro-store";
proto = "virtiofs";
source = "/nix/store";
tag = "read_only_nix_store";
}
# ┌───────────────────────────────────────────────────────────────
# │ Service Data (Read-Write)
# └───────────────────────────────────────────────────────────────
# CHOOSE ONE PATTERN:
#
# PATTERN 1: Direct path (Vaultwarden)
# {
# mountPoint = "/var/lib/bitwarden_rs";
# proto = "virtiofs";
# source = serviceCfg.mntPaths.path0;
# tag = "${serviceCfg.name}_data";
# }
#
# PATTERN 2: Use serviceCfg.name (Forgejo)
# {
# mountPoint = "/var/lib/${serviceCfg.name}";
# proto = "virtiofs";
# source = serviceCfg.mntPaths.path0;
# tag = "${serviceCfg.name}_data";
# }
#
# PATTERN 3: Use serviceCfg.varPaths (Jellyfin)
# {
# mountPoint = serviceCfg.varPaths.path0;
# proto = "virtiofs";
# source = serviceCfg.mntPaths.path0;
# tag = "${serviceCfg.name}_data";
# }
{
mountPoint = "/var/lib/${serviceCfg.name}"; # ADJUST THIS
proto = "virtiofs";
source = serviceCfg.mntPaths.path0;
tag = "${serviceCfg.name}_data";
}
# ┌───────────────────────────────────────────────────────────────
# │ OPTIONAL: Additional Data Mounts (like Jellyfin's cache)
# └───────────────────────────────────────────────────────────────
# UNCOMMENT IF NEEDED:
# {
# mountPoint = serviceCfg.varPaths.path1;
# proto = "virtiofs";
# source = "${serviceCfg.mntPaths.path0}/cache";
# tag = "${serviceCfg.name}_cache";
# }
# ┌───────────────────────────────────────────────────────────────
# │ Secrets (Read-Only) - INCLUDE IF SERVICE NEEDS SECRETS
# └───────────────────────────────────────────────────────────────
# CRITICAL: Source must use service-specific subdirectory!
# This matches the sops.secrets configuration above
#
# UNCOMMENT IF SERVICE NEEDS SECRETS:
# {
# mountPoint = "/run/secrets";
# proto = "virtiofs";
# source = "/run/secrets/${serviceCfg.name}";
# tag = "host_secrets";
# }
];
};
};
};
}
# ============================================================================
# QUICK REFERENCE: Pattern Comparison
# ============================================================================
#
# ┌──────────────┬─────────────┬──────────────┬─────────────┐
# │ Aspect │ Vaultwarden │ Forgejo │ Jellyfin │
# ├──────────────┼─────────────┼──────────────┼─────────────┤
# │ vCPU │ 2 │ 2 │ 6 │
# │ Memory │ 3072 MB │ 3072 MB │ 8192 MB │
# │ Secrets │ env file │ sep. files │ none │
# │ /tmp mount │ no │ yes (4G) │ yes (4G) │
# │ VM tmpfiles │ no │ yes │ yes │
# │ Host perms │ 0777 │ 0777 │ 0777 │
# │ Data mounts │ 1 │ 1 │ 2 │
# │ Secrets mnt │ yes │ yes │ no │
# └──────────────┴─────────────┴──────────────┴─────────────┘
#
# ============================================================================
# CHECKLIST: Steps to Create New Service
# ============================================================================
#
# 1. [ ] Copy this template to modules/nixos/services/YOUR-SERVICE/default.nix
# 2. [ ] Replace 'service' with your service name in instances reference
# 3. [ ] Uncomment and configure your service in services = { ... }
# 4. [ ] Adjust vcpu/mem based on service requirements
# 5. [ ] Configure secrets (if needed) - both sops.secrets and shares
# 6. [ ] Set correct mountPoint for service data (check service docs)
# 7. [ ] Adjust firewall ports based on service needs
# 8. [ ] Add /tmp mount if service needs large temporary space
# 9. [ ] Test build: sudo nixos-rebuild build --flake .#YOUR-HOST
# 10. [ ] Deploy: sudo nixos-rebuild switch --flake .#YOUR-HOST
# 11. [ ] Verify TAP exists: ip link show vm-YOUR-SERVICE
# 12. [ ] SSH to VM: ssh -p 22XX root@localhost
# 13. [ ] Check network in VM: ip addr show enp0s5
# 14. [ ] Test service: curl http://VM-IP:PORT
# 15. [ ] Test external access: https://YOUR-SERVICE.example.com
#
# ============================================================================