{ 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"; }; }; }