Files
nixos-dotfiles/hosts/nixserve/services.nix
T
ulve 1550e333e0 nixserve: open Music Assistant/mDNS ports, add paperless service, bump flake inputs
Allow TCP 8095 (Music Assistant web UI) and UDP 5353 (mDNS) through the
firewall so the LAN interface can reach it and Chromecast discovery works;
previously only the trusted tailscale0 interface had access.
2026-10-12 09:08:52 +02:00

330 lines
11 KiB
Nix

{ config, pkgs, rssdownloader-src, ... }:
let
rssdownloader = pkgs.callPackage ./rssdownloader.nix { src = rssdownloader-src; };
proxyVhost = { upstream, websockets ? true, maxBodySize ? "1g", proxySsl ? false }: {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = upstream;
proxyWebsockets = websockets;
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
proxy_buffering off;
client_max_body_size ${maxBodySize};
'' + (if proxySsl then ''
proxy_ssl_verify off;
'' else "");
};
};
dockerService = { name, description, dir }: {
inherit description;
after = [ "docker.service" "network.target" ];
requires = [ "docker.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
WorkingDirectory = dir;
ExecStart = "${pkgs.docker-compose}/bin/docker-compose up -d";
ExecStop = "${pkgs.docker-compose}/bin/docker-compose down";
};
};
in
{
services.nfs.server = {
enable = true;
statdPort = 4000;
lockdPort = 4001;
mountdPort = 4002;
exports = ''
/media 192.168.50.0/24(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000,fsid=0,insecure)
/media 100.64.0.0/10(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000,fsid=0,insecure)
'';
};
services.jellyfin.enable = true;
services.paperless = {
enable = true;
dataDir = "/media/apps/paperless";
address = "127.0.0.1";
port = 28981;
domain = "paperless.maybec.at";
settings = {
PAPERLESS_OCR_LANGUAGE = "eng+swe";
};
};
services.tailscale = {
enable = true;
openFirewall = true;
useRoutingFeatures = "server";
};
services.syncthing = {
enable = true;
user = "ulve";
dataDir = "/home/ulve";
configDir = "/home/ulve/.config/syncthing";
openDefaultPorts = true;
settings.devices."macbook" = {
id = "U4YVRUT-JAAXHQ4-TA6FAGH-SO6Z7CI-KDFPRNG-UZYNXDK-6OQY7ZA-UBTZRQW";
};
settings.devices."phone" = {
id = "WXTP2PT-GW3C3EJ-JYYYXUT-VEKCAQV-NQTPQLI-TOMDAI3-CSUUHF5-6DH72AM";
};
settings.folders."Downloads" = {
path = "/home/ulve/Downloads";
devices = [ "macbook" ];
};
settings.folders."yrw8u-ut036" = {
path = "/home/ulve/Phone";
devices = [ "phone" ];
};
settings.folders."xdnt7-iaqtd" = {
path = "/home/ulve/Vaults";
devices = [ "macbook" ];
};
};
#services.immich = {
# enable = true;
# mediaLocation = "/media/immich";
#};
users.users.git = {
isSystemUser = true;
group = "git";
home = "/media/git";
shell = pkgs.bash;
};
users.groups.git = {};
services.gitea = {
enable = true;
user = "git";
group = "git";
stateDir = "/media/git";
settings = {
server = {
DOMAIN = "git.maybec.at";
ROOT_URL = "https://git.maybec.at/";
HTTP_ADDR = "127.0.0.1";
HTTP_PORT = 3001;
START_SSH_SERVER = false;
SSH_DOMAIN = "git.maybec.at";
SSH_PORT = 22;
SSH_USER = "git";
};
service.DISABLE_REGISTRATION = true;
};
};
services.openssh.extraConfig = ''
Match User git
AuthorizedKeysCommand ${config.services.gitea.package}/bin/gitea keys -c ${config.services.gitea.customDir}/conf/app.ini -e git -u %u -t %t -k %k
AuthorizedKeysCommandUser git
'';
services.zfs.autoScrub = {
enable = true;
interval = "monthly";
pools = [ "media" ];
};
# DNS ad-blocking for other LAN/Tailscale clients only.
# nixserve's own DNS resolution is untouched (Tailscale MagicDNS via resolv.conf).
services.pihole-ftl = {
enable = true;
openFirewallDNS = true;
openFirewallWebserver = true;
lists = [
{ url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"; description = "StevenBlack unified hosts"; }
];
settings = {
dns.upstreams = [ "1.1.1.1" "1.0.0.1" ];
webserver.api.cli_pw = true;
};
};
services.pihole-web = {
enable = true;
ports = [ "8081o" ];
};
# Lock the smart TV down to *.maybec.at only: everything else fails to resolve.
# This seeds Pi-hole's gravity.db (groups/clients/rules) declaratively on every
# boot/rebuild -- that data lives outside pihole.toml so it isn't covered by
# services.pihole-ftl.settings.
systemd.services.pihole-tv-lockdown = {
description = "Seed Pi-hole gravity.db with smart TV DNS lockdown rules";
# gravity.db's group/client/rule tables are queried live per DNS lookup
# (not cached at FTL startup), so this only needs to run after FTL has
# created the database -- no need to race it to run "before" FTL starts.
after = [ "pihole-ftl.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = config.services.pihole-ftl.user;
Group = config.services.pihole-ftl.group;
};
script = ''
set -euo pipefail
${pkgs.sqlite}/bin/sqlite3 "${config.services.pihole-ftl.stateDirectory}/gravity.db" <<'SQL'
INSERT OR IGNORE INTO "group" (name, enabled, description)
VALUES ('tv-only', 1, 'Smart TV: only *.maybec.at may resolve');
INSERT OR IGNORE INTO domainlist (type, domain, enabled, comment)
VALUES (2, '.*', 1, 'tv-only: deny everything');
INSERT OR IGNORE INTO domainlist (type, domain, enabled, comment)
VALUES (3, '(^|\.)maybec\.at$', 1, 'tv-only: allow maybec.at and subdomains');
INSERT OR IGNORE INTO domainlist_by_group (domainlist_id, group_id)
SELECT d.id, g.id FROM domainlist d, "group" g
WHERE d.domain IN ('.*', '(^|\.)maybec\.at$') AND g.name = 'tv-only';
INSERT OR IGNORE INTO client (ip, comment)
VALUES ('20:28:bc:15:1d:c0', 'Smart TV (locked to maybec.at only)');
INSERT OR IGNORE INTO client_by_group (client_id, group_id)
SELECT c.id, g.id FROM client c, "group" g
WHERE c.ip = '20:28:bc:15:1d:c0' AND g.name = 'tv-only';
-- Pi-hole auto-links every new domainlist/client row into the Default
-- group (tr_domainlist_add / tr_client_add triggers). Strip that back
-- out, otherwise the deny-all regex applies to every client on the
-- network instead of just the TV.
DELETE FROM domainlist_by_group
WHERE group_id = 0
AND domainlist_id IN (SELECT id FROM domainlist WHERE domain IN ('.*', '(^|\.)maybec\.at$'));
DELETE FROM client_by_group
WHERE group_id = 0
AND client_id IN (SELECT id FROM client WHERE ip = '20:28:bc:15:1d:c0');
SQL
'';
};
virtualisation.docker = {
enable = true;
enableOnBoot = true;
autoPrune = {
enable = true;
dates = "weekly";
flags = [ "--all" ];
};
daemon.settings = {
log-driver = "json-file";
log-opts = {
max-size = "10m";
max-file = "3";
};
};
};
systemd.services.transmission = dockerService {
name = "transmission";
description = "Transmission via Mullvad VPN";
dir = "/home/ulve/code/transmission";
};
systemd.services.aftersummer = dockerService {
name = "aftersummer";
description = "Aftersummer";
dir = "/home/ulve/code/aftersummer";
};
systemd.services.spine = dockerService {
name = "spine";
description = "Spine ebook server";
dir = "/home/ulve/code/wargaming";
};
systemd.services.bookthing = dockerService {
name = "bookthing";
description = "Bookthing audiobook server";
dir = "/home/ulve/code/bookthing";
};
systemd.services.tailscale-exit = dockerService {
name = "tailscale-exit";
description = "Tailscale exit node via Mullvad VPN";
dir = "/home/ulve/code/tailscale";
};
systemd.services.colony-archive = dockerService {
name = "colony-archive";
description = "Colony Archive tabletop RPG campaign manager";
dir = "/home/ulve/code/colony-archive";
};
systemd.services.pingvin-share = dockerService {
name = "pingvin-share";
description = "Pingvin Share file sharing service";
dir = "/home/ulve/code/pingvin-share";
};
systemd.services.frigate = dockerService {
name = "frigate";
description = "Frigate NVR";
dir = "/home/ulve/code/frigate";
};
systemd.services.mosquitto = dockerService {
name = "mosquitto";
description = "Mosquitto MQTT broker";
dir = "/home/ulve/code/mosquitto";
};
systemd.services.homeassistant = dockerService {
name = "homeassistant";
description = "Home Assistant";
dir = "/home/ulve/code/homeassistant";
};
systemd.services.dozzle = {
description = "Dozzle Docker log viewer";
after = [ "docker.service" "network.target" ];
requires = [ "docker.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStartPre = "${pkgs.docker}/bin/docker rm -f dozzle || true";
ExecStart = "${pkgs.docker}/bin/docker run --rm --name dozzle -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle";
ExecStop = "${pkgs.docker}/bin/docker stop dozzle";
Restart = "on-failure";
};
};
security.acme = {
acceptTerms = true;
defaults.email = "johansson.olov@gmail.com";
};
services.nginx = {
enable = true;
virtualHosts."aftersummer.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:5588"; };
virtualHosts."spine.maybec.at" = proxyVhost { upstream = "http://localhost:3000"; };
virtualHosts."bookthing.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:8000"; };
virtualHosts."jelly.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:8096"; };
virtualHosts."photos.maybec.at" = proxyVhost { upstream = "http://[::1]:2283"; maxBodySize = "50g"; };
virtualHosts."colony.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:3333"; };
virtualHosts."git.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:3001"; maxBodySize = "10g"; };
virtualHosts."share.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:8090"; maxBodySize = "10g"; };
virtualHosts."cam.maybec.at" = proxyVhost { upstream = "https://127.0.0.1:8971"; proxySsl = true; };
virtualHosts."home.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:8123"; };
virtualHosts."paperless.maybec.at" = proxyVhost { upstream = "http://127.0.0.1:28981"; };
};
}