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";
sopsPath = "${var}/secrets";
secretPath = "${var}/secrets";
cachePath = "/var/cache";
dummy = "";
};

View file

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

View file

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

View file

@ -44,15 +44,15 @@ in
ceres = {
imports = builtins.attrValues {
inherit (modules)
acmeCeres
acme
# audiobookshelf
caddyCeres
caddy
# comfyui
# filesorter
# firefly-iii
# forgejo
# glance
# jellyfin
jellyfin
# logrotate
# mastodon
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
importList =
let
content = builtins.readDir ./.;
dirContent = builtins.filter (n: content.${n} == "directory") (builtins.attrNames content);
in
map (name: ./. + "/${name}") dirContent;
inherit (flake.config.people) user0;
inherit (flake.config.people.users.${user0}) email;
inherit (flake.config.services) instances;
service = instances.acme;
dns0 = instances.web.dns.provider0;
dns1 = instances.web.dns.provider1;
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
importList =
let
content = builtins.readDir ./.;
dirContent = builtins.filter (n: content.${n} == "directory") (builtins.attrNames content);
in
map (name: ./. + "/${name}") dirContent;
inherit (flake.config.services) instances;
service = instances.caddy;
in
{
imports = importList;
services.caddy = {
enable = true;
};
networking = {
firewall = {
allowedTCPPorts = [
service.ports.port0
service.ports.port1
];
};
};
}

View file

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

View file

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