test: jellyfin microVM

This commit is contained in:
Nick 2025-11-08 22:25:51 -06:00
parent b553e92ad1
commit e25c1a2e06
13 changed files with 271 additions and 358 deletions

View file

@ -287,6 +287,7 @@ in
sslPath = "${var}/acme"; sslPath = "${var}/acme";
sopsPath = "${var}/secrets"; sopsPath = "${var}/secrets";
secretPath = "${var}/secrets"; secretPath = "${var}/secrets";
cachePath = "/var/cache";
dummy = ""; dummy = "";
}; };

View file

@ -5,6 +5,7 @@ let
sslPath sslPath
varPath varPath
mntPath mntPath
cachePath
secretPath secretPath
; ;
label = "Jellyfin"; label = "Jellyfin";
@ -53,6 +54,7 @@ in
}; };
varPaths = { varPaths = {
path0 = "${varPath}/${name}"; path0 = "${varPath}/${name}";
path1 = "${cachePath}/${name}";
}; };
mntPaths = { mntPaths = {
path0 = "${mntPath}/${name}"; path0 = "${mntPath}/${name}";

View file

@ -1,40 +1,40 @@
{ {
pkgs, pkgs,
lib, # lib,
flake, # flake,
osConfig, # osConfig,
... ...
}: }:
let # let
inherit (flake.config.machines.devices) mars deimos; # inherit (flake.config.machines.devices) mars deimos;
hostname = osConfig.networking.hostName; # hostname = osConfig.networking.hostName;
sharedPaths = '' # sharedPaths = ''
${pkgs.zoxide}/bin/zoxide add ~/projects/dotfiles # ${pkgs.zoxide}/bin/zoxide add ~/projects/dotfiles
${pkgs.zoxide}/bin/zoxide add ~/downloads # ${pkgs.zoxide}/bin/zoxide add ~/downloads
${pkgs.zoxide}/bin/zoxide add ~/projects # ${pkgs.zoxide}/bin/zoxide add ~/projects
${pkgs.zoxide}/bin/zoxide add /mnt/media/ceres/jellyfin # ${pkgs.zoxide}/bin/zoxide add /mnt/media/ceres/jellyfin
${pkgs.zoxide}/bin/zoxide add /mnt/media/ceres/comfyui # ${pkgs.zoxide}/bin/zoxide add /mnt/media/ceres/comfyui
''; # '';
desktopPaths = '' # desktopPaths = ''
${pkgs.zoxide}/bin/zoxide add ~/projects/website # ${pkgs.zoxide}/bin/zoxide add ~/projects/website
${pkgs.zoxide}/bin/zoxide add ~/projects/workflowbuilder # ${pkgs.zoxide}/bin/zoxide add ~/projects/workflowbuilder
${pkgs.zoxide}/bin/zoxide add /mnt/media/storage # ${pkgs.zoxide}/bin/zoxide add /mnt/media/storage
''; # '';
zoxidePaths = { # zoxidePaths = {
home.activation.initZoxidePaths = lib.hm.dag.entryAfter [ "writeBoundary" ] ( # home.activation.initZoxidePaths = lib.hm.dag.entryAfter [ "writeBoundary" ] (
if hostname == mars.name then # if hostname == mars.name then
(sharedPaths + desktopPaths) # (sharedPaths + desktopPaths)
else if hostname == deimos.name then # else if hostname == deimos.name then
sharedPaths # sharedPaths
else # else
"" # ""
); # );
}; # };
in # in
{ {
programs.zoxide = { programs.zoxide = {
enable = true; enable = true;
@ -44,4 +44,4 @@ in
]; ];
}; };
} }
// zoxidePaths # // zoxidePaths

View file

@ -44,15 +44,15 @@ in
ceres = { ceres = {
imports = builtins.attrValues { imports = builtins.attrValues {
inherit (modules) inherit (modules)
acmeCeres acme
# audiobookshelf # audiobookshelf
caddyCeres caddy
# comfyui # comfyui
# filesorter # filesorter
# firefly-iii # firefly-iii
# forgejo # forgejo
# glance # glance
# jellyfin jellyfin
# logrotate # logrotate
# mastodon # mastodon
microvm microvm

View file

@ -1,87 +0,0 @@
{
config,
flake,
...
}:
let
inherit (flake.config.people) user0;
inherit (flake.config.people.users.${user0}) email;
inherit (flake.config.services) instances;
domain0 = instances.web.domains.url0;
domain1 = instances.web.domains.url1;
domain4 = flake.inputs.linkpage.secrets.domains.projectsite;
service = instances.acme;
dns0 = instances.web.dns.provider0;
dns1 = instances.web.dns.provider1;
dns0Path = "dns/${dns0}";
dns1Path = "dns/${dns1}";
in
{
security.acme = {
acceptTerms = true;
defaults = {
email = email.address0;
server = "https://acme-v02.api.letsencrypt.org/directory";
};
certs =
let
dnsConfig = provider: dns: {
dnsProvider = dns;
environmentFile = config.sops.secrets.${provider}.path;
};
in
{
# "${instances.audiobookshelf.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.glance.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.jellyfin.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.ollama.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.searx.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.syncthing.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.vaultwarden.domains.url0}" = dnsConfig dns0Path dns0; # Moved to vaultwarden service module
# "${instances.prompter.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.comfyui.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.firefly-iii.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.opencloud.domains.url0}" = dnsConfig dns0Path dns0;
"${instances.forgejo.domains.url0}" = dnsConfig dns0Path dns0;
# "${instances.mastodon.domains.url0}" = dnsConfig dns0Path dns0;
# "${domain0}" = dnsConfig dns0Path dns0;
# "${domain1}" = dnsConfig dns0Path dns0;
# "${domain4}" = dnsConfig dns1Path dns1;
};
};
sops =
let
dnsList = [
dns0
dns1
];
secretList = [
"pass"
];
sopsPath = secret: dns: {
path = "/var/lib/secrets/${instances.acme.name}/${dns}-${secret}";
owner = "root";
mode = "600";
};
in
{
secrets = builtins.listToAttrs (
builtins.concatLists (
map (
dns:
map (secret: {
name = "dns/${dns}";
value = sopsPath secret dns;
}) secretList
) dnsList
)
);
};
systemd = {
tmpfiles.rules = [
"Z ${service.sops.path0} 755 ${service.name} ${service.name} -"
];
};
}

View file

@ -1,75 +0,0 @@
{
config,
flake,
...
}:
let
inherit (flake.config.people) user0;
inherit (flake.config.people.users.${user0}) email;
inherit (flake.config.services) instances;
service = instances.acme;
domain0 = instances.web.domains.url0;
dns0 = instances.web.dns.provider0;
dns0Path = "dns/${dns0}";
instanceName = service: (instances.${service}.subdomain);
dnsConfig = provider: dns: {
dnsProvider = dns;
directory = instances.acme.paths.path0;
environmentFile = config.sops.secrets.${provider}.path;
};
in
{
security.acme = {
acceptTerms = true;
defaults = {
email = email.address0;
server = "https://acme-v02.api.letsencrypt.org/directory";
};
certs = builtins.listToAttrs (
(map
(service: {
name = "${instanceName service}.${domain0}";
value = dnsConfig dns0Path dns0;
})
[
# instances.opencloud.name
]
)
);
};
sops =
let
dnsList = [
dns0
];
secretList = [
"pass"
];
sopsPath = secret: dns: {
path = "/var/lib/secrets/${instances.acme.name}/${dns}-${secret}";
owner = "root";
mode = "600";
};
in
{
secrets = builtins.listToAttrs (
builtins.concatLists (
map (
dns:
map (secret: {
name = "dns/${dns}";
value = sopsPath secret dns;
}) secretList
) dnsList
)
);
};
systemd = {
tmpfiles.rules = [
"Z ${service.sops.path0} 755 ${service.name} ${service.name} -"
];
};
}

View file

@ -1,11 +1,56 @@
{
flake,
...
}:
let let
importList = inherit (flake.config.people) user0;
let inherit (flake.config.people.users.${user0}) email;
content = builtins.readDir ./.; inherit (flake.config.services) instances;
dirContent = builtins.filter (n: content.${n} == "directory") (builtins.attrNames content); service = instances.acme;
in dns0 = instances.web.dns.provider0;
map (name: ./. + "/${name}") dirContent; dns1 = instances.web.dns.provider1;
in in
{ {
imports = importList; security.acme = {
acceptTerms = true;
defaults = {
email = email.address0;
server = "https://acme-v02.api.letsencrypt.org/directory";
};
};
sops =
let
dnsList = [
dns0
dns1
];
secretList = [
"pass"
];
sopsPath = secret: dns: {
path = "/var/lib/secrets/${instances.acme.name}/${dns}-${secret}";
owner = "root";
mode = "600";
};
in
{
secrets = builtins.listToAttrs (
builtins.concatLists (
map (
dns:
map (secret: {
name = "dns/${dns}";
value = sopsPath secret dns;
}) secretList
) dnsList
)
);
};
systemd = {
tmpfiles.rules = [
"Z ${service.sops.path0} 755 ${service.name} ${service.name} -"
];
};
} }

View file

@ -1,25 +0,0 @@
{ flake, ... }:
let
inherit (flake.config.services) instances;
service = instances.caddy;
in
{
services.caddy = {
enable = true;
};
users.users.${service.name}.extraGroups = [
"acme"
"mastodon"
"firefly-iii"
];
networking = {
firewall = {
allowedTCPPorts = [
service.ports.port0
service.ports.port1
];
};
};
}

View file

@ -1,25 +0,0 @@
{ flake, ... }:
let
inherit (flake.config.services.instances) caddy;
service = caddy;
in
{
services.caddy = {
enable = true;
};
users.users.${service.name}.extraGroups = [
"acme"
];
networking = {
firewall = {
allowedTCPPorts = [
service.ports.port0
service.ports.port1
service.ports.port2
];
};
};
}

View file

@ -1,11 +1,20 @@
{ flake, ... }:
let let
importList = inherit (flake.config.services) instances;
let
content = builtins.readDir ./.; service = instances.caddy;
dirContent = builtins.filter (n: content.${n} == "directory") (builtins.attrNames content);
in
map (name: ./. + "/${name}") dirContent;
in in
{ {
imports = importList; services.caddy = {
enable = true;
};
networking = {
firewall = {
allowedTCPPorts = [
service.ports.port0
service.ports.port1
];
};
};
} }

View file

@ -1,13 +1,34 @@
{ flake, ... }: {
config,
flake,
...
}:
let let
inherit (flake.config.people) user0; inherit (flake.config.people) user0;
inherit (flake.config.machines.devices) ceres; inherit (flake.config.services) instances;
inherit (flake.config.services.instances) jellyfin web; serviceCfg = flake.config.services.instances.jellyfin;
service = jellyfin; hostCfg = flake.config.services.instances.web;
localhost = web.localhost.address0; host = serviceCfg.domains.url0;
host = service.domains.url0; dns0 = instances.web.dns.provider0;
dns0Path = "dns/${dns0}";
in in
{ {
users.users.caddy.extraGroups = [ "acme" ];
security.acme.certs."${host}" = {
dnsProvider = dns0;
environmentFile = config.sops.secrets.${dns0Path}.path;
group = "caddy";
};
microvm.vms.jellyin = {
autostart = true;
restartIfChanged = true;
config = {
system.stateVersion = "24.05";
time.timeZone = "America/Winnipeg";
users.users.root.openssh.authorizedKeys.keys = flake.config.people.users.${user0}.sshKeys;
services = { services = {
jellyfin = { jellyfin = {
enable = true; enable = true;
@ -18,6 +39,95 @@ in
openFirewall = true; openFirewall = true;
enable = true; enable = true;
}; };
openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "prohibit-password";
};
};
};
networking.firewall.allowedTCPPorts = [
serviceCfg.ports.port0
serviceCfg.ports.port1
serviceCfg.ports.port2
];
systemd.network = {
enable = true;
networks."20-lan" = {
matchConfig.Name = "enp0s5";
addresses = [
{ Address = "${serviceCfg.interface.ip}/24"; }
];
routes = [
{
Destination = "${hostCfg.localhost.address1}/0";
Gateway = serviceCfg.interface.gate;
}
];
dns = [
"1.1.1.1"
"8.8.8.8"
];
};
};
systemd.services.systemd-networkd.wantedBy = [ "multi-user.target" ];
microvm = {
vcpu = 4;
mem = 4096;
hypervisor = "qemu";
interfaces = [
{
type = "tap";
id = serviceCfg.interface.id;
mac = serviceCfg.interface.mac;
}
{
type = "user";
id = serviceCfg.interface.idUser;
mac = serviceCfg.interface.macUser;
}
];
forwardPorts = [
{
from = "host";
host.port = serviceCfg.interface.ssh;
guest.port = 22;
}
];
shares = [
{
mountPoint = "/nix/.ro-store";
proto = "virtiofs";
source = "/nix/store";
tag = "read_only_nix_store";
}
{
mountPoint = serviceCfg.varPaths.path0;
proto = "virtiofs";
source = serviceCfg.mntPaths.path0;
tag = "service_data";
}
{
mountPoint = serviceCfg.varPaths.path1;
proto = "virtiofs";
source = "${serviceCfg.mntPaths.path0}/cache";
tag = "service_cache";
}
];
};
};
};
systemd.tmpfiles.rules = [
"d ${serviceCfg.mntPaths.path0} 0755 root root -"
];
services = {
caddy = { caddy = {
virtualHosts = { virtualHosts = {
"${host}" = { "${host}" = {
@ -25,50 +135,12 @@ in
redir /.well-known/carddav /remote.php/dav/ 301 redir /.well-known/carddav /remote.php/dav/ 301
redir /.well-known/caldav /remote.php/dav/ 301 redir /.well-known/caldav /remote.php/dav/ 301
reverse_proxy ${localhost}:${toString service.ports.port0} reverse_proxy ${serviceCfg.interface.ip}:${toString serviceCfg.ports.port0}
tls ${service.ssl.cert} ${service.ssl.key} tls ${serviceCfg.ssl.cert} ${serviceCfg.ssl.key}
''; '';
}; };
}; };
}; };
}; };
fileSystems =
let
settings = {
fsType = "none";
options = [
"bind"
];
depends = [
ceres.storage0.mount
];
};
in
{
"/var/lib/${service.name}" = {
device = service.paths.path0;
}
// settings;
"/var/cache/${service.name}" = {
device = "${service.paths.path1}";
}
// settings;
};
systemd.tmpfiles.rules = [
"Z ${service.paths.path0} 0755 ${user0} ${service.name} -"
"Z ${service.paths.path0} 0755 ${user0} ${service.name} -"
];
networking = {
firewall = {
allowedTCPPorts = [
service.ports.port0
service.ports.port1
service.ports.port2
];
};
};
} }

View file

@ -4,21 +4,15 @@
... ...
}: }:
let let
vaultwardenCfg = flake.config.services.instances.vaultwarden;
smtpCfg = flake.config.services.instances.smtp;
inherit (flake.config.people) user0; inherit (flake.config.people) user0;
inherit (flake.config.services) instances; inherit (flake.config.services) instances;
serviceCfg = flake.config.services.instances.vaultwarden;
smtpCfg = flake.config.services.instances.smtp;
host = serviceCfg.domains.url0;
dns0 = instances.web.dns.provider0; dns0 = instances.web.dns.provider0;
dns0Path = "dns/${dns0}"; dns0Path = "dns/${dns0}";
in in
{ {
users.users.caddy.extraGroups = [ "acme" ];
security.acme.certs."${vaultwardenCfg.domains.url0}" = {
dnsProvider = dns0;
environmentFile = config.sops.secrets.${dns0Path}.path;
group = "caddy";
};
microvm.vms.vaultwarden = { microvm.vms.vaultwarden = {
autostart = true; autostart = true;
@ -32,13 +26,13 @@ in
dbBackend = "sqlite"; dbBackend = "sqlite";
config = { config = {
# Domain Configuration # Domain Configuration
DOMAIN = "https://${vaultwardenCfg.domains.url0}"; DOMAIN = "https://${host}";
# Email Configuration # Email Configuration
SMTP_AUTH_MECHANISM = "Plain"; SMTP_AUTH_MECHANISM = "Plain";
SMTP_EMBED_IMAGES = true; SMTP_EMBED_IMAGES = true;
SMTP_FROM = vaultwardenCfg.email.address0; SMTP_FROM = serviceCfg.email.address0;
SMTP_FROM_NAME = vaultwardenCfg.label; SMTP_FROM_NAME = serviceCfg.label;
SMTP_HOST = smtpCfg.hostname; SMTP_HOST = smtpCfg.hostname;
SMTP_PORT = smtpCfg.ports.port1; SMTP_PORT = smtpCfg.ports.port1;
SMTP_SECURITY = smtpCfg.records.record1; SMTP_SECURITY = smtpCfg.records.record1;
@ -57,11 +51,11 @@ in
# Rocket (Web Server) Settings # Rocket (Web Server) Settings
ROCKET_ADDRESS = "0.0.0.0"; ROCKET_ADDRESS = "0.0.0.0";
ROCKET_PORT = vaultwardenCfg.ports.port0; ROCKET_PORT = serviceCfg.ports.port0;
}; };
# Environment file with secrets (mounted from host) # Environment file with secrets (mounted from host)
environmentFile = "/run/secrets/vaultwarden/env"; environmentFile = "/run/secrets/${serviceCfg.name}/env";
}; };
services.openssh = { services.openssh = {
@ -78,18 +72,18 @@ in
139 # SMTP 139 # SMTP
587 # SMTP 587 # SMTP
2525 # SMTP 2525 # SMTP
vaultwardenCfg.ports.port0 serviceCfg.ports.port0
]; ];
systemd.network = { systemd.network = {
enable = true; enable = true;
networks."20-lan" = { networks."20-lan" = {
matchConfig.Name = "enp0s5"; matchConfig.Name = "enp0s5";
addresses = [ { Address = "${vaultwardenCfg.interface.ip}/24"; } ]; addresses = [ { Address = "${serviceCfg.interface.ip}/24"; } ];
routes = [ routes = [
{ {
Destination = "0.0.0.0/0"; Destination = "0.0.0.0/0";
Gateway = vaultwardenCfg.interface.gate; Gateway = serviceCfg.interface.gate;
} }
]; ];
dns = [ dns = [
@ -108,19 +102,19 @@ in
interfaces = [ interfaces = [
{ {
type = "tap"; type = "tap";
id = vaultwardenCfg.interface.id; id = serviceCfg.interface.id;
mac = vaultwardenCfg.interface.mac; mac = serviceCfg.interface.mac;
} }
{ {
type = "user"; type = "user";
id = vaultwardenCfg.interface.idUser; id = serviceCfg.interface.idUser;
mac = vaultwardenCfg.interface.macUser; mac = serviceCfg.interface.macUser;
} }
]; ];
forwardPorts = [ forwardPorts = [
{ {
from = "host"; from = "host";
host.port = vaultwardenCfg.interface.ssh; host.port = serviceCfg.interface.ssh;
guest.port = 22; guest.port = 22;
} }
]; ];
@ -134,7 +128,7 @@ in
{ {
mountPoint = "/var/lib/bitwarden_rs"; mountPoint = "/var/lib/bitwarden_rs";
proto = "virtiofs"; proto = "virtiofs";
source = vaultwardenCfg.mntPaths.path0; source = serviceCfg.mntPaths.path0;
tag = "vaultwarden_data"; tag = "vaultwarden_data";
} }
{ {
@ -148,22 +142,32 @@ in
}; };
}; };
systemd.tmpfiles.rules = [ security.acme.certs."${host}" = {
"d ${vaultwardenCfg.mntPaths.path0} 0755 root root -" dnsProvider = dns0;
]; environmentFile = config.sops.secrets.${dns0Path}.path;
group = "caddy";
};
services.caddy.virtualHosts."${vaultwardenCfg.domains.url0}" = { services.caddy.virtualHosts = {
"${host}" = {
extraConfig = '' extraConfig = ''
reverse_proxy ${vaultwardenCfg.interface.ip}:${toString vaultwardenCfg.ports.port0} { reverse_proxy ${serviceCfg.interface.ip}:${toString serviceCfg.ports.port0} {
header_up X-Real-IP {remote_host} header_up X-Real-IP {remote_host}
} }
tls ${vaultwardenCfg.ssl.cert} ${vaultwardenCfg.ssl.key} tls ${serviceCfg.ssl.cert} ${serviceCfg.ssl.key}
encode zstd gzip encode zstd gzip
''; '';
}; };
};
users.users.caddy.extraGroups = [ "acme" ];
systemd.tmpfiles.rules = [
"d ${serviceCfg.mntPaths.path0} 0755 root root -"
];
sops.secrets = { sops.secrets = {
"vaultwarden/env" = { "${serviceCfg.name}/env" = {
owner = "root"; owner = "root";
mode = "0600"; mode = "0600";
}; };

View file

@ -9,13 +9,10 @@ let
wireguardService = instances.wireGuard; wireguardService = instances.wireGuard;
in in
{ {
# Enable microVM host
microvm.host.enable = true; microvm.host.enable = true;
# systemd-networkd for bridge management (required for TAP interfaces)
systemd.network.enable = true; systemd.network.enable = true;
# Bridge configuration for microVMs
systemd.network.netdevs."10-br-vms" = { systemd.network.netdevs."10-br-vms" = {
netdevConfig = { netdevConfig = {
Name = "br-vms"; Name = "br-vms";
@ -23,7 +20,6 @@ in
}; };
}; };
# Attach physical interface and tap interfaces to bridge
systemd.network.networks."20-lan" = { systemd.network.networks."20-lan" = {
matchConfig.Name = [ matchConfig.Name = [
"enp10s0" "enp10s0"
@ -34,7 +30,6 @@ in
}; };
}; };
# Bridge gets the host IP
systemd.network.networks."30-br-vms" = { systemd.network.networks."30-br-vms" = {
matchConfig.Name = "br-vms"; matchConfig.Name = "br-vms";
networkConfig = { networkConfig = {
@ -47,12 +42,9 @@ in
networking = { networking = {
hostName = ceres.name; hostName = ceres.name;
# NetworkManager disabled - using declarative networking
networkmanager.enable = false; networkmanager.enable = false;
nftables.enable = true; nftables.enable = true;
useDHCP = false; useDHCP = false;
# Network configuration handled by systemd-networkd bridge
firewall = { firewall = {
enable = true; enable = true;
allowedTCPPorts = [ allowedTCPPorts = [