dotfiles/servers/eisen/configuration.nix
Daniel Bulant 64522f794b
update
2026-09-03 09:19:49 +02:00

745 lines
20 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
nix-index-database,
pkgs,
lib,
name ? "eisen",
...
}:
let
# these are used both in service configuration but also to
# create mappings {name}.eisen.danbulant.cloud to port in caddy
ports = {
status = 3001;
glance = 5678;
jellyfin = 8096;
qb = 8081;
sonarr = 8989;
radarr = 7878;
jackett = 9117;
prowlarr = 9696;
keep = 8100;
grafana = 3002;
tolgee = 8200;
# ntfy = 3003;
forgejo = 8300;
snaps = 8400;
matrix = 6167;
sable = 6200;
livekit = 7880;
};
internalPorts = {
prometheus-node = 9000;
prometheus-qb = 9200;
prometheus-sonarr = 9101;
prometheus-radarr = 9102;
prometheus-prowlarr = 9103;
prometheus-llama-swap = 9409;
prometheus = 9090;
livekit-jwt = 8080;
tuwunel-admin = 8009;
};
matrixServerName = "badapple.cz";
matrixHost = "matrix.badapple.cz";
livekitKeyFile = "/var/lib/livekit/keys";
in
{
deployment = {
# buildOnTarget = true;
targetHost = "eisen";
};
nixpkgs.config.permittedInsecurePackages = [
"pnpm-9.15.9"
];
programs.nix-index-database.comma.enable = true;
imports = [
nix-index-database.nixosModules.nix-index
./hardware-configuration.nix
../../modules/llama-swap-exporter.nix
../../modules/tuwunel-admin.nix
];
nix = {
daemonCPUSchedPolicy = "idle";
daemonIOSchedClass = "idle";
};
networking = {
hostName = name;
nameservers = [ "1.1.1.1" ];
networkmanager.enable = true;
};
time.timeZone = lib.mkForce "Europe/Prague";
i18n.defaultLocale = "en_US.UTF-8";
security = {
rtkit.enable = true;
polkit.enable = true;
};
services = {
logind.lidSwitchExternalPower = "ignore";
geoclue2.enable = true;
localtimed.enable = true;
openssh.enable = true;
tailscale = {
enable = true;
useRoutingFeatures = "both";
openFirewall = true;
extraUpFlags = [ "--advertise-exit-node" ];
};
avahi.enable = true;
lldpd.enable = true;
jellyfin = {
enable = true;
};
matrix-tuwunel = {
enable = true;
settings.global = {
server_name = matrixServerName;
port = [ ports.matrix ];
allow_registration = true;
oidc_native_auth = true;
ip_source = "rightmost_x_forwarded_for";
registration_token_file = "/etc/secrets/matrix-registration-token";
database_backup_path = "/var/lib/tuwunel/database-backups";
database_backups_to_keep = 2;
admin_signal_execute = [ "server backup-database" ];
well_known = {
client = "https://${matrixHost}";
livekit_url = "https://${matrixHost}/livekit/jwt";
};
};
};
tuwunel-admin = {
enable = true;
settings = {
server.bind = "127.0.0.1:${toString internalPorts.tuwunel-admin}";
matrix = {
homeservers = [ "https://${matrixHost}" ];
allow_any_server = false;
admin_bot = "@tuwunel:${matrixServerName}";
admin_room_alias = "#admins:${matrixServerName}";
device_id = "tuwunel-admin";
device_display_name = "tuwunel-admin";
};
};
};
livekit = {
enable = true;
keyFile = livekitKeyFile;
openFirewall = true;
settings = {
port = ports.livekit;
room.auto_create = false;
rtc = {
tcp_port = 7881;
use_external_ip = true;
port_range_start = 50100;
port_range_end = 50105;
};
};
};
lk-jwt-service = {
enable = true;
keyFile = livekitKeyFile;
livekitUrl = "wss://${matrixHost}/livekit/sfu";
port = internalPorts.livekit-jwt;
};
sonarr = {
enable = true;
settings.server.port = ports.sonarr;
};
radarr = {
enable = true;
settings.server.port = ports.radarr;
};
prowlarr = {
enable = true;
settings.server.port = ports.prowlarr;
};
karakeep = {
enable = true;
extraEnvironment = {
PORT = toString ports.keep;
# DISABLE_SIGNUPS = "true";
DISABLE_NEW_RELEASE_CHECK = "true";
};
environmentFile = "/etc/secrets/karakeep.env";
};
# llama-swap-exporter = {
# enable = true;
# url = "http://100.120.15.10:${toString ports.llama-swap}/api/metrics";
# port = internalPorts.prometheus-llama-swap;
# };
forgejo = {
enable = true;
settings = {
server = {
DOMAIN = "git.badapple.cz";
ROOT_URL = "https://git.badapple.cz";
HTTP_PORT = ports.forgejo;
};
service.DISABLE_REGISTRATION = true;
};
# actions = {
# ENABLED = true;
# DEFAULT_ACTIONS_URL = "github";
# };
lfs.enable = true;
dump.enable = true;
dump.age = "5d";
};
dnsmasq = {
enable = true;
};
uptime-kuma = {
enable = true;
settings = {
PORT = toString ports.status;
};
};
grafana = {
enable = true;
settings = {
server.http_port = ports.grafana;
security = {
secret_key = "$__file{/etc/secrets/gf_secret_key}";
};
};
};
prometheus = {
enable = true;
exporters = {
exportarr-radarr = {
enable = true;
url = "http://127.0.0.1:${toString ports.radarr}";
port = internalPorts.prometheus-radarr;
apiKeyFile = "/etc/secrets/radarr_api_key";
};
exportarr-sonarr = {
enable = true;
url = "http://127.0.0.1:${toString ports.sonarr}";
port = internalPorts.prometheus-sonarr;
apiKeyFile = "/etc/secrets/sonarr_api_key";
};
exportarr-prowlarr = {
enable = true;
url = "http://127.0.0.1:${toString ports.prowlarr}";
port = internalPorts.prometheus-prowlarr;
apiKeyFile = "/etc/secrets/prowlarr_api_key";
};
node = {
enable = true;
port = internalPorts.prometheus-node;
};
};
scrapeConfigs = [
{
job_name = "node";
static_configs = [
{
targets = [ "localhost:${toString internalPorts.prometheus-node}" ];
}
];
}
{
job_name = "qb";
static_configs = [
{
targets = [ "localhost:${toString internalPorts.prometheus-qb}" ];
}
];
}
{
job_name = "sonarr";
static_configs = [
{
targets = [ "localhost:${toString internalPorts.prometheus-sonarr}" ];
}
];
}
{
job_name = "radarr";
static_configs = [
{
targets = [ "localhost:${toString internalPorts.prometheus-radarr}" ];
}
];
}
{
job_name = "llama-swap";
static_configs = [
{
targets = [ "localhost:${toString internalPorts.prometheus-llama-swap}" ];
}
];
}
{
job_name = "uptime-kuma";
static_configs = [
{
targets = [ "localhost:${toString ports.status}" ];
# generated, only accessible through tailscale, not really sensitive
}
];
basic_auth = {
username = "";
password = "uk1_SAAatRz9luyFXItVnbXyOdVuU2fkMhZITrnPY27z";
};
}
];
};
# ntfy-sh = {
# enable = true;
# settings = {
# listen-http = ":${toString ports.ntfy}";
# base-url = "http://ntfy.eisen";
# };
# };
# grafana-to-ntfy = {
# enable = true;
# settings = {
# ntfyUrl = "http://ntfy.eisen/grafana";
# };
# };
glance = {
enable = true;
settings = {
server = {
port = ports.glance;
};
pages = import ./glance-pages.nix;
};
environmentFile = "/etc/secrets/glance.env";
};
caddy = {
enable = true;
extraConfig = ''
(auth) {
forward_auth unix//run/tailscale-nginx-auth/tailscale-nginx-auth.sock {
uri /auth
header_up Remote-Addr {remote_host}
header_up Remote-Port {remote_port}
header_up Original-URI {uri}
copy_headers {
Tailscale-User>X-Webauth-User
Tailscale-Name>X-Webauth-Name
Tailscale-Login>X-Webauth-Login
Tailscale-Tailnet>X-Webauth-Tailnet
Tailscale-Profile-Picture>X-Webauth-Profile-Picture
}
}
}
'';
virtualHosts =
builtins.listToAttrs (
map (k: {
name = "${k}.eisen.danbulant.cloud:80, ${k}.eisen:80";
value = {
# import auth
extraConfig = ''
reverse_proxy http://localhost:${toString ports.${k}}
'';
};
}) (builtins.attrNames ports)
)
// {
"${matrixHost}:80" = {
extraConfig = ''
@matrixClientWellKnown path /.well-known/matrix/client
handle @matrixClientWellKnown {
header Access-Control-Allow-Origin "*"
header Content-Type application/json
respond `{"m.homeserver":{"base_url":"https://${matrixHost}"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://${matrixHost}/livekit/jwt"}]}` 200
}
@matrixServerWellKnown path /.well-known/matrix/server
handle @matrixServerWellKnown {
header Access-Control-Allow-Origin "*"
header Content-Type application/json
respond `{"m.server":"${matrixHost}:443"}` 200
}
@jwtService path /livekit/jwt/sfu/get* /livekit/jwt/healthz* /livekit/jwt/get_token*
handle @jwtService {
uri strip_prefix /livekit/jwt
reverse_proxy http://localhost:${toString internalPorts.livekit-jwt}
}
@livekit path /livekit/sfu*
handle @livekit {
uri strip_prefix /livekit/sfu
reverse_proxy http://localhost:${toString ports.livekit}
}
handle {
reverse_proxy http://localhost:${toString ports.matrix}
}
'';
};
"admin.${matrixHost}:80" = {
extraConfig = ''
reverse_proxy http://localhost:${toString internalPorts.tuwunel-admin}
'';
};
"sable.badapple.cz:80" = {
extraConfig = ''
reverse_proxy http://localhost:${toString ports.sable}
'';
};
"translations.danbulant.cloud:80, translations.rpi1.danbulant.cloud:80" = {
extraConfig = ''
reverse_proxy http://localhost:${toString ports.tolgee}
'';
};
"snaps.rpi1.danbulant.cloud" = {
extraConfig = ''
reverse_proxy http://localhost:${toString ports.snaps}
'';
};
};
};
tailscale.permitCertUid = "caddy";
tailscaleAuth = {
# this is what's used above in forward_auth
enable = true;
group = "caddy";
};
};
systemd.services.lk-jwt-service.environment.LIVEKIT_FULL_ACCESS_HOMESERVERS = matrixServerName;
systemd.services.livekit-keys = {
description = "Generate the shared LiveKit API key";
before = [
"livekit.service"
"lk-jwt-service.service"
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StateDirectory = "livekit";
StateDirectoryMode = "0700";
UMask = "0077";
};
script = ''
if [[ ! -s "$STATE_DIRECTORY/keys" ]]; then
key="$(${lib.getExe pkgs.openssl} rand -base64 48)"
printf 'lk-jwt-service: %s\n' "$key" > "$STATE_DIRECTORY/keys"
fi
'';
};
systemd.services.livekit = {
requires = [ "livekit-keys.service" ];
after = [ "livekit-keys.service" ];
};
systemd.services.lk-jwt-service = {
requires = [ "livekit-keys.service" ];
after = [ "livekit-keys.service" ];
};
services.restic.backups.matrix = {
repository = "sftp:restic-eisen@fern:/media/large/restic/eisen";
passwordFile = "/etc/secrets/restic-password";
initialize = true;
paths = [
"/var/lib/tuwunel/database-backups"
"/var/lib/tuwunel/media"
"/var/lib/livekit/keys"
"/etc/secrets/matrix-registration-token"
];
extraOptions = [
"sftp.command='${lib.getExe pkgs.tailscale} ssh restic-eisen@fern -s sftp'"
];
extraBackupArgs = [ "--tag=matrix" ];
pruneOpts = [
"--keep-hourly=24"
"--keep-daily=14"
"--keep-weekly=8"
"--keep-monthly=12"
];
timerConfig = {
OnCalendar = "*-*-* 09,12,15,18,21:00:00";
Persistent = true;
RandomizedDelaySec = "20m";
};
backupPrepareCommand = ''
#!${pkgs.runtimeShell}
set -eu
backup_meta=/var/lib/tuwunel/database-backups/meta
before="$(${pkgs.findutils}/bin/find "$backup_meta" -maxdepth 1 -type f -printf '%f\n' 2>/dev/null | ${pkgs.coreutils}/bin/sort || true)"
${lib.getExe' pkgs.systemd "systemctl"} kill --kill-whom=main --signal=SIGUSR2 tuwunel.service
# RocksDB writes the metadata file only after an online backup is complete.
for ((attempt = 0; attempt < 120; attempt++)); do
after="$(${pkgs.findutils}/bin/find "$backup_meta" -maxdepth 1 -type f -printf '%f\n' 2>/dev/null | ${pkgs.coreutils}/bin/sort || true)"
if [[ -n "$after" && "$after" != "$before" ]]; then
exit 0
fi
${pkgs.coreutils}/bin/sleep 5
done
echo "Timed out waiting for Tuwunel's online database backup" >&2
exit 1
'';
};
# A sleeping Fern is normal. Keep retrying without overlapping timer runs,
# and promptly abandon dead SSH connections if it sleeps during a backup.
systemd.services.restic-backups-matrix.serviceConfig = {
Restart = "on-failure";
RestartSec = "30m";
TimeoutStartSec = "12h";
};
# The Karakeep module still emits this option, but Meilisearch 1.51 removed it.
systemd.services.meilisearch.serviceConfig.ExecStartPre = lib.mkAfter [
"${lib.getExe pkgs.gnused} -i '/^experimental_dumpless_upgrade =/d' \${RUNTIME_DIRECTORY}/config.toml"
];
# LiveKit's TCP ICE fallback is separate from its HTTP/WebSocket port.
networking.firewall.allowedTCPPorts = [ 7881 ];
# systemd.services.syncthing.environment.STNODEFAULTFOLDER = "true";
virtualisation = {
docker = {
enable = true;
enableOnBoot = true;
};
oci-containers = {
backend = "docker";
containers = {
gluetun = {
image = "qmcgaw/gluetun";
capabilities = {
NET_ADMIN = true;
};
devices = [ "/dev/net/tun" ];
environmentFiles = [ "/etc/secrets/gluetun.env" ];
ports = [
"${toString ports.qb}:${toString ports.qb}"
"${toString ports.jackett}:${toString ports.jackett}"
];
# VPN_SERVICE_PROVIDER=protonvpn
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU
# SERVER_COUNTRIES=Denmark
environment = {
VPN_PORT_FORWARDING = "on";
# TOR_ONLY = "on";
PORT_FORWARD_ONLY = "on";
FIREWALL_OUTBOUND_SUBNETS = "192.168.1.0/24,100.64.0.0/10";
FIREWALL_INPUT_PORTS = "41641,22,80,443,53";
VPN_PORT_FORWARDING_UP_COMMAND = ''
/bin/sh -c 'wget -O- -nv --retry-connrefused --post-data "json={\"listen_port\":{{PORT}},\"current_network_interface\":\"{{VPN_INTERFACE}}\",\"random_port\":false,\"upnp\":false}" http://127.0.0.1:${toString ports.qb}/api/v2/app/setPreferences'
'';
VPN_PORT_FORWARDING_DOWN_COMMAND = ''
/bin/sh -c 'wget -O- -nv --retry-connrefused --post-data "json={\"listen_port\":0,\"current_network_interface\":\"lo\"}" http://127.0.0.1:${toString ports.qb}/api/v2/app/setPreferences'
'';
};
# extraOptions = [ "--network=host" ];
};
qbittorrent = {
image = "lscr.io/linuxserver/qbittorrent";
environment = {
WEBUI_PORT = toString ports.qb;
};
volumes = [
"/media/large/downloads:/downloads"
"qbittorrent-config:/config"
];
extraOptions = [ "--network=container:gluetun" ];
};
jackett = {
image = "lscr.io/linuxserver/jackett";
volumes = [
"jackett-config:/config"
];
extraOptions = [ "--network=container:gluetun" ];
};
prometheus-qb = {
image = "ghcr.io/esanchezm/prometheus-qbittorrent-exporter";
environment = {
QBITTORRENT_PORT = toString ports.qb;
QBITTORRENT_HOST = "localhost";
EXPORTER_PORT = toString internalPorts.prometheus-qb;
};
extraOptions = [ "--network=host" ];
# ports = [ "8000:${toString internalPorts.prometheus-qb}" ];
};
sable = {
image = "ghcr.io/sableclient/sable:latest";
ports = [
"127.0.0.1:${toString ports.sable}:8080"
];
};
tolgee = {
image = "tolgee/tolgee";
volumes = [
"data:/data"
];
ports = [
"${toString ports.tolgee}:8080"
];
environment = {
TOLGEE_AUTHENTICATION_ENABLED = "true";
};
};
};
};
};
# hardware.nvidia-container-toolkit.enable = true;
boot = {
kernelPackages = pkgs.linuxPackages_latest;
# Shortcuts for fixing things
# alt+sysrq (prtsc) + key
# h: Print help to the system log.
# f: Trigger the kernel oom killer.
# s: Sync data to disk before triggering the reset options below.
# e: SIGTERM all processes except PID 0.
# i: SIGKILL all processes except PID 0.
# b: Reboot the system.
kernel.sysctl."kernel.sysrq" = 1;
# zfs.enabled = false;
swraid.enable = false;
initrd.systemd.enable = true;
loader = {
systemd-boot.enable = true;
efi.canTouchEfiVariables = true;
# timeout = 0;
# grub.enable = true;
# grub.device = "/dev/disk/by-id/ata-Apacer_AS350_512GB_2021012802000028";
# grub.efiSupport = true;
};
};
users.users.dan = {
isNormalUser = true;
extraGroups = [
"networkmanager"
"wheel"
"docker"
"fuse"
"video"
"wireshark"
"gamemode"
"scanner"
"lp"
"kvm"
"adbusers"
];
shell = pkgs.nushell;
};
nix.settings.trusted-users = [
"root"
"@wheel"
"dan"
];
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver
intel-vaapi-driver
intel-ocl
libva-vdpau-driver
intel-compute-runtime-legacy1
];
};
environment.sessionVariables = {
LIBVA_DRIVER_NAME = "i965";
};
environment.systemPackages = with pkgs; [
bun
jellyfin-ffmpeg
intel-gpu-tools
lsof
rsync
git
nvtopPackages.full
btop
lshw
bat
lsd
fastfetch
fish
nix-output-monitor
nh
duf
dust
cachix
qemu
ffmpeg
httpie
socat
websocat
oh-my-posh
];
nixpkgs.config.allowUnfree = true;
# nixpkgs.config.cudaSupport = true;
nix.settings = {
substituters = [
"https://cache.nixos.org"
"https://nix-community.cachix.org"
];
trusted-public-keys = [
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
};
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. Its perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "25.05"; # Did you read the comment?
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# /etc/hosts :)
networking.extraHosts = "";
}