feat: refactor the modules/servers directory.

This commit is contained in:
CronyAkatsuki 2026-01-19 21:36:24 +01:00
parent 4e783c052b
commit 8b754d3a7e
69 changed files with 61 additions and 62 deletions

View file

@ -0,0 +1,14 @@
{...}: let
servicesDir = ./services;
serviceFiles =
builtins.filter
(name: builtins.match "^.*\\.nix$" name != null)
(builtins.attrNames (builtins.readDir servicesDir));
hostModules = map (fn: import "${servicesDir}/${fn}") serviceFiles;
in {
imports =
[
../../common
]
++ hostModules;
}

View file

@ -0,0 +1,49 @@
{...}: {
virtualisation.oci-containers.containers.crafty-controller = {
image = "registry.gitlab.com/crafty-controller/crafty-4:latest";
autoStart = true;
ports = [
"8443:8443"
"8123:8123"
"19132:19132"
"25500-25600:25500-25600"
];
labels = {
"io.containers.autoupdate" = "registry";
};
volumes = [
"/var/lib/crafty-controller/backups:/crafty/backups"
"/var/lib/crafty-controller/logs:/crafty/logs"
"/var/lib/crafty-controller/servers:/crafty/servers"
"/var/lib/crafty-controller/config:/crafty/app/config"
"/var/lib/crafty-controller/import:/crafty/import"
];
};
networking.firewall = {
allowedTCPPorts = [25500];
allowedUDPPorts = [25500];
};
services.traefik.dynamicConfigOptions.http = {
services.crafty.loadBalancer.servers = [
{
url = "https://localhost:8443";
}
];
routers.crafty = {
rule = "Host(`crafty.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "crafty";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/crafty-controller"];
server.paths = ["/var/lib/crafty-controller"];
};
}

View file

@ -0,0 +1,15 @@
{...}: let
servicesDir = ./services;
serviceFiles =
builtins.filter
(name: builtins.match "^.*\\.nix$" name != null)
(builtins.attrNames (builtins.readDir servicesDir));
hostModules = map (fn: import "${servicesDir}/${fn}") serviceFiles;
in {
imports =
[
../../common
./secrets.nix
]
++ hostModules;
}

View file

@ -0,0 +1,12 @@
{
age = {
secrets = {
rclone = {
file = ../../../../secrets/rclone.age;
};
navidrome = {
file = ../../../../secrets/navidrome.age;
};
};
};
}

View file

@ -0,0 +1,32 @@
{...}: {
services.audiobookshelf = {
enable = true;
};
systemd.services.audiobookshelf = {
after = ["mnt.mount"];
bindsTo = ["mnt.mount"];
};
services.traefik.dynamicConfigOptions.http = {
services.audiobookshelf.loadBalancer.servers = [
{
url = "http://localhost:8000";
}
];
routers.audiobookshelf = {
rule = "Host(`abs.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "audiobookshelf";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/audiobookshelf"];
server.paths = ["/var/lib/audiobookshelf"];
};
}

View file

@ -0,0 +1,37 @@
{config, ...}: {
services.navidrome = {
enable = true;
settings = {
MusicFolder = "/mnt/music";
BaseUrl = "https://nd.cronyakatsuki.xyz";
};
environmentFile = "${config.age.secrets.navidrome.path}";
};
systemd.services.navidrome = {
after = ["mnt.mount"];
bindsTo = ["mnt.mount"];
};
services.traefik.dynamicConfigOptions.http = {
services.navidrome.loadBalancer.servers = [
{
url = "http://localhost:4533";
}
];
routers.navidrome = {
rule = "Host(`nd.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "navidrome";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/navidrome"];
server.paths = ["/var/lib/navidrome"];
};
}

View file

@ -0,0 +1,19 @@
{
config,
pkgs,
...
}: {
environment.systemPackages = [pkgs.rclone];
fileSystems."/mnt" = {
device = "storage:";
fsType = "rclone";
options = [
"nodev"
"nofail"
"allow_other"
"args2env"
"config=${config.age.secrets.rclone.path}"
];
};
}

View file

@ -0,0 +1,9 @@
{...}: {
services.umurmur = {
enable = true;
openFirewall = true;
settings = {
welcometext = "Welcome to crony's nutsack.";
};
};
}

View file

@ -0,0 +1,14 @@
{...}: let
servicesDir = ./services;
serviceFiles =
builtins.filter
(name: builtins.match "^.*\\.nix$" name != null)
(builtins.attrNames (builtins.readDir servicesDir));
hostModules = map (fn: import "${servicesDir}/${fn}") serviceFiles;
in {
imports =
[
../../common
]
++ hostModules;
}

View file

@ -0,0 +1,39 @@
{...}: {
services.immich = {
enable = true;
port = 2283;
host = "127.0.0.1";
accelerationDevices = ["/dev/dri/renderD128"];
};
users.users.immich.extraGroups = ["video" "render"];
services.traefik.dynamicConfigOptions.http = {
services.immich.loadBalancer.servers = [
{
url = "http://localhost:2283";
}
];
routers.immich = {
rule = "Host(`immich.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "immich";
entrypoints = "websecure";
};
};
services.postgresqlBackup = {
enable = true;
databases = [
"immich"
];
};
services.restic.backups = {
local.paths = ["/var/lib/immich" "/var/backup/postgresql"];
server.paths = ["/var/lib/immich" "/var/backup/postgresql"];
};
}

View file

@ -0,0 +1,15 @@
{...}: let
servicesDir = ./services;
serviceFiles =
builtins.filter
(name: builtins.match "^.*\\.nix$" name != null)
(builtins.attrNames (builtins.readDir servicesDir));
hostModules = map (fn: import "${servicesDir}/${fn}") serviceFiles;
in {
imports =
[
../../common
./secrets.nix
]
++ hostModules;
}

View file

@ -0,0 +1,9 @@
{
age = {
secrets = {
wg-heimdall = {
file = ../../../../secrets/wg-heimdall.age;
};
};
};
}

View file

@ -0,0 +1,28 @@
{
services.atuin = {
enable = true;
openRegistration = false;
};
services.traefik.dynamicConfigOptions.http = {
services.atuin.loadBalancer.servers = [
{
url = "http://localhost:8888";
}
];
routers.atuin = {
rule = "Host(`atuin.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "atuin";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/backup/postgresql"];
server.paths = ["/var/backup/postgresql"];
};
}

View file

@ -0,0 +1,52 @@
{pkgs, ...}: {
systemd.services.beszel-hub = {
enable = true;
description = "Beszel agent";
after = ["network.target"];
serviceConfig = {
Type = "simple";
Restart = "always";
RestartSec = 3;
User = "beszel";
Group = "beszel";
WorkingDirectory = "/var/lib/beszel";
};
script = "${pkgs.beszel}/bin/beszel-hub serve --http '127.0.0.1:6789'";
wantedBy = ["multi-user.target"];
};
users = {
users.beszel = {
isSystemUser = true;
home = "/var/lib/beszel";
createHome = true;
group = "beszel";
};
groups.beszel = {};
};
services.traefik.dynamicConfigOptions.http = {
services.beszel.loadBalancer.servers = [
{
url = "http://localhost:6789";
}
];
routers.beszel = {
rule = "Host(`beszel.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "beszel";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/beszel"];
server.paths = ["/var/lib/beszel"];
};
}

View file

@ -0,0 +1,33 @@
{...}: {
services.ntfy-sh = {
enable = true;
settings = {
base-url = "https://ntfy.cronyakatsuki.xyz";
listen-http = "127.0.0.1:2586";
behind-proxy = true;
auth-default-access = "deny-all";
};
};
services.traefik.dynamicConfigOptions.http = {
services.ntfy-sh.loadBalancer.servers = [
{
url = "http://localhost:2586";
}
];
routers.ntfy-sh = {
rule = "Host(`ntfy.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "ntfy-sh";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/ntfy-sh"];
server.paths = ["/var/lib/ntfy-sh"];
};
}

View file

@ -0,0 +1,39 @@
{...}: {
services.redlib = {
enable = true;
address = "127.0.0.1";
settings = {
ROBOTS_DISABLE_INDEXING = "on";
THEME = "gruvboxdark";
USE_HLS = "on";
};
};
# Setup anubis to block fucking ai bots
services.anubis.instances.redlib = {
settings = {
TARGET = "http://127.0.0.1:8080";
BIND_NETWORK = "tcp";
BIND = "127.0.0.1:8081";
SERVE_ROBOTS_TXT = true;
DIFFICULTY = 5;
};
};
services.traefik.dynamicConfigOptions.http = {
services.redlib.loadBalancer.servers = [
{
url = "http://localhost:8081";
}
];
routers.redlib = {
rule = "Host(`libreddit.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "redlib";
entrypoints = "websecure";
};
};
}

View file

@ -0,0 +1,29 @@
{...}: {
services.uptime-kuma = {
enable = true;
settings = {
HOST = "127.0.0.1";
};
};
services.traefik.dynamicConfigOptions.http = {
services.uptime-kuma.loadBalancer.servers = [
{
url = "http://localhost:3001";
}
];
routers.uptime-kuma = {
rule = "Host(`uptime.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "uptime-kuma";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/uptime-kuma"];
server.paths = ["/var/lib/uptime-kuma"];
};
}

View file

@ -0,0 +1,24 @@
{config, ...}: {
networking = {
nat = {
enable = true;
enableIPv6 = true;
externalInterface = "enp1s0";
internalInterfaces = ["wg0"];
};
firewall = {
allowedTCPPorts = [53];
allowedUDPPorts = [53 51820];
};
wg-quick.interfaces.wg0.configFile = "${config.age.secrets.wg-heimdall.path}";
};
services.dnsmasq = {
enable = true;
settings = {
interface = "wg0";
};
};
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
}

View file

@ -0,0 +1,15 @@
{...}: let
servicesDir = ./services;
serviceFiles =
builtins.filter
(name: builtins.match "^.*\\.nix$" name != null)
(builtins.attrNames (builtins.readDir servicesDir));
hostModules = map (fn: import "${servicesDir}/${fn}") serviceFiles;
in {
imports =
[
../../common
./secrets.nix
]
++ hostModules;
}

View file

@ -0,0 +1,12 @@
{
age = {
secrets = {
searx = {
file = ../../../../secrets/searx.age;
};
miniflux = {
file = ../../../../secrets/miniflux.age;
};
};
};
}

View file

@ -0,0 +1,27 @@
{
services.filebrowser = {
enable = true;
};
services.traefik.dynamicConfigOptions.http = {
services.filebrowser.loadBalancer.servers = [
{
url = "http://localhost:8080";
}
];
routers.filebrowser = {
rule = "Host(`filebrowser.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "filebrowser";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/filebrowser"];
server.paths = ["/var/lib/filebrowser"];
};
}

View file

@ -0,0 +1,38 @@
{config, ...}: {
services.miniflux = {
enable = true;
config = {
LISTEN_ADDR = "127.0.0.1:8000";
BASE_URL = "https://feed.cronyakatsuki.xyz";
FETCH_YOUTUBE_WATCH_TIME = "1";
};
adminCredentialsFile = "${config.age.secrets.miniflux.path}";
};
services.traefik.dynamicConfigOptions.http = {
services.miniflux.loadBalancer.servers = [
{
url = "http://localhost:8000";
}
];
routers.miniflux = {
rule = "Host(`feed.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "miniflux";
entrypoints = "websecure";
};
};
services.postgresqlBackup = {
enable = true;
databases = ["miniflux"];
};
services.restic.backups = {
local.paths = ["/var/backup/postgresql"];
server.paths = ["/var/backup/postgresql"];
};
}

View file

@ -0,0 +1,34 @@
{config, ...}: {
services.searx = {
enable = true;
settings = {
general.instance_name = "Crony's SearXNG";
server.port = "8090";
server.bind_address = "127.0.0.1";
server.secret_key = "@SEARX_SECRET_KEY@";
};
uwsgiConfig = {
http = ":8090";
};
redisCreateLocally = true;
configureUwsgi = true;
environmentFile = "${config.age.secrets.searx.path}";
};
services.traefik.dynamicConfigOptions.http = {
services.searx.loadBalancer.servers = [
{
url = "http://localhost:8090";
}
];
routers.searx = {
rule = "Host(`searx.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "searx";
entrypoints = "websecure";
};
};
}

View file

@ -0,0 +1,37 @@
{...}: {
virtualisation.oci-containers.containers.syncyomi = {
image = "ghcr.io/syncyomi/syncyomi:latest";
autoStart = true;
ports = [
"8282:8282"
];
labels = {
"io.containers.autoupdate" = "registry";
};
volumes = [
"/var/lib/syncyomi:/config"
];
};
services.traefik.dynamicConfigOptions.http = {
services.syncyomi.loadBalancer.servers = [
{
url = "http://localhost:8282";
}
];
routers.syncyomi = {
rule = "Host(`syncyomi.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "syncyomi";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/syncyomi"];
server.paths = ["/var/lib/syncyomi"];
};
}

View file

@ -0,0 +1,47 @@
instance="http://127.0.0.1:8383"
files=$(curl -s "$instance"/files/)
# Check for keygens on server
if echo "$files" | grep -i "keygen" >> /dev/null; then
for file in $(echo "$files" | grep -i "keygen"); do
echo "Deleting file $file"
curl -X DELETE "$instance/files/$file"
done
fi
# Check for common payload names on server
if echo "$files" | grep -iE "dorpxy|mner|mnpxy" >> /dev/null; then
for file in $(echo "$files" | grep -iE "dorpxy|mner|mnpxy"); do
echo "Deleting file $file"
curl -X DELETE "$instance/files/$file"
done
fi
# Delete common php payloads
if echo "$files" | grep -i ".php" >> /dev/null; then
for file in $(echo "$files" | grep -i ".php"); do
if curl -s "$instance/files/$file" | grep -i "base64_decode" >> /dev/null; then
echo "Found payload, deleting file $file"
curl -X DELETE "$instance/files/$file"
fi
done
fi
# Delete all shell scripts that make direct mention of my upfast instance
if echo "$files" | grep -i ".sh" >> /dev/null; then
for file in $(echo "$files" | grep -i ".sh"); do
if curl -s "$instance/files/$file" | grep -i "upfast.cronyakatsuki.xyz/files" >> /dev/null; then
echo "Found payload, deleting file $file"
curl -X DELETE "$instance/files/$file"
fi
done
fi
# Delete kernel object files
if echo "$files" | grep -iE ".*.ko" >> /dev/null; then
for file in $(echo "$files" | grep -iE ".*.ko"); do
echo "Deleting file $file"
curl -X DELETE "$instance/files/$file"
done
fi

View file

@ -0,0 +1,98 @@
{
inputs,
pkgs,
lib,
...
}: let
upfast-cleaner = pkgs.writeShellApplication {
name = "upfast-cleaner";
runtimeInputs = with pkgs; [curl];
text = ./upfast-cleaner.sh;
};
in {
fileSystems."/var/lib/upfast" = {
device = "/root/10gb";
fsType = "ext4";
options = [
"loop"
"rw"
"usrquota"
"grpquota"
];
};
users = {
users.upfast = {
isSystemUser = true;
home = "/var/lib/upfast";
group = "upfast";
};
groups.upfast = {};
};
systemd.services.upfast = {
enable = true;
description = "SelfHosted file upload and share service like 0x0.st";
serviceConfig = {
Type = "simple";
User = "upfast";
Group = "upfast";
WorkingDirectory = "/var/lib/upfast";
Restart = "on-failure";
};
script = "${inputs.upfast.packages.aarch64-linux.default}/bin/upfast -p 8383 -d https://upfast.cronyakatsuki.xyz";
after = ["var-lib-upfast.mount"];
bindsTo = ["var-lib-upfast.mount"];
wantedBy = ["multi-user.target"];
};
systemd.services.upfast-cleaner = {
description = "Script to automatically delete common types of payloads/keygens.";
requires = ["upfast.service"];
after = ["upfast.service"];
serviceConfig = {
Type = "oneshot";
User = "upfast";
Group = "upfast";
WorkingDirectory = "/var/lib/upfast";
};
script = "${lib.getExe upfast-cleaner}";
};
systemd.timers.upfast-cleaner = {
enable = true;
timerConfig = {
OnBootSec = "1m";
OnUnitActiveSec = "1m";
};
wantedBy = ["timers.target"];
};
services.traefik.dynamicConfigOptions.http = {
services.upfast.loadBalancer.servers = [
{
url = "http://localhost:8383";
}
];
routers.upfast = {
rule = "Host(`upfast.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "upfast";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/upfast"];
server.paths = ["/var/lib/upfast"];
};
}

View file

@ -0,0 +1,41 @@
{...}: {
virtualisation.oci-containers.containers.wallabag = {
image = "docker.io/wallabag/wallabag:latest";
autoStart = true;
ports = [
"8181:80"
];
environment = {
"SYMFONY__ENV__DOMAIN_NAME" = "https://wallabag.cronyakatsuki.xyz";
};
labels = {
"io.containers.autoupdate" = "registry";
};
volumes = [
"/var/lib/wallabag/data:/var/www/wallabag/data"
"/var/lib/wallabag/images:/var/www/wallabag/web/assets/images"
];
};
services.traefik.dynamicConfigOptions.http = {
services.wallabag.loadBalancer.servers = [
{
url = "http://localhost:8181";
}
];
routers.wallabag = {
rule = "Host(`wallabag.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "wallabag";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/wallabag"];
server.paths = ["/var/lib/wallabag"];
};
}

View file

@ -0,0 +1,32 @@
{...}: {
virtualisation.oci-containers.containers.website = {
image = "docker.io/nginx:alpine";
autoStart = true;
ports = [
"8001:80"
];
labels = {
"io.containers.autoupdate" = "registry";
};
volumes = [
"/var/lib/website:/usr/share/nginx/html:ro"
];
};
services.traefik.dynamicConfigOptions.http = {
services.website.loadBalancer.servers = [
{
url = "http://localhost:8001";
}
];
routers.website = {
rule = "Host(`cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "website";
entrypoints = "websecure";
};
};
}

View file

@ -0,0 +1,15 @@
{...}: let
servicesDir = ./services;
serviceFiles =
builtins.filter
(name: builtins.match "^.*\\.nix$" name != null)
(builtins.attrNames (builtins.readDir servicesDir));
hostModules = map (fn: import "${servicesDir}/${fn}") serviceFiles;
in {
imports =
[
../../common
./secrets.nix
]
++ hostModules;
}

View file

@ -0,0 +1,18 @@
{
age = {
secrets = {
forgejo-db = {
file = ../../../../secrets/forgejo-db.age;
};
plausible = {
file = ../../../../secrets/plausible.age;
};
conduit = {
file = ../../../../secrets/conduit.age;
};
lemmy-env = {
file = ../../../../secrets/lemmy.env.age;
};
};
};
}

View file

@ -0,0 +1,30 @@
{...}: {
services.changedetection-io = {
enable = true;
playwrightSupport = true;
baseURL = "https://changedetection.cronyakatsuki.xyz";
behindProxy = true;
};
services.traefik.dynamicConfigOptions.http = {
services.changedetection.loadBalancer.servers = [
{
url = "http://localhost:5000";
}
];
routers.changedetection = {
rule = "Host(`changedetection.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "changedetection";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/changedetection-io"];
server.paths = ["/var/lib/changedetection-io"];
};
}

View file

@ -0,0 +1,39 @@
{config, ...}: {
services.matrix-conduit = {
enable = true;
settings = {
global = {
server_name = "cronyakatsuki.xyz";
database_backend = "rocksdb";
allow_registration = true;
allow_check_for_updates = true;
};
};
};
systemd.services.conduit.serviceConfig = {
EnvironmentFile = ["${config.age.secrets.conduit.path}"];
};
services.traefik.dynamicConfigOptions.http = {
services.conduit.loadBalancer.servers = [
{
url = "http://localhost:6167";
}
];
routers.conduit = {
rule = "Host(`matrix.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "conduit";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/matrix-conduit"];
server.paths = ["/var/lib/matrix-conduit"];
};
}

View file

@ -0,0 +1,51 @@
{config, ...}: {
services.forgejo = {
enable = true;
settings = {
session = {
COOKIE_SECURE = true;
};
service = {
REGISTER_MANUAL_CONFIRM = true;
ENABLE_CAPTCHA = true;
REQUIRE_CAPTCHA_FOR_LOGIN = true;
};
server = {
ROOT_URL = "https://git.cronyakatsuki.xyz";
HTTP_ADDR = "127.0.0.1";
};
};
database = {
passwordFile = "${config.age.secrets.forgejo-db.path}";
};
};
services.traefik.dynamicConfigOptions.http = {
services.forgejo.loadBalancer.servers = [
{
url = "http://localhost:3000";
}
];
routers.forgejo = {
rule = "Host(`git.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "forgejo";
entrypoints = "websecure";
};
};
services.openssh = {
authorizedKeysFiles = ["/var/lib/%u/.ssh/authorized_keys"];
settings = {
AllowUsers = ["forgejo"];
};
};
services.restic.backups = {
local.paths = ["/var/lib/forgejo"];
server.paths = ["/var/lib/forgejo"];
};
}

View file

@ -0,0 +1,241 @@
{
pkgs,
lib,
config,
...
}: {
# Enable container name DNS for all Podman networks.
networking.firewall.interfaces = let
matchAll =
if !config.networking.nftables.enable
then "podman+"
else "podman*";
in {
"${matchAll}".allowedUDPPorts = [53];
};
# Containers
virtualisation.oci-containers.containers."lemmy-backend" = {
image = "dessalines/lemmy:0.19.13";
environmentFiles = [
"/run/agenix/lemmy-env"
];
volumes = [
"/var/lib/lemmy/lemmy.hjson:/config/config.hjson:rw,Z"
];
dependsOn = [
"lemmy-db"
"lemmy-pictrs"
];
log-driver = "journald";
extraOptions = [
"--hostname=lemmy"
"--network-alias=lemmy"
"--network=lemmy_default"
];
};
systemd.services."podman-lemmy-backend" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"podman-network-lemmy_default.service"
];
requires = [
"podman-network-lemmy_default.service"
];
partOf = [
"podman-compose-lemmy-root.target"
];
wantedBy = [
"podman-compose-lemmy-root.target"
];
};
virtualisation.oci-containers.containers."lemmy-db" = {
image = "docker.io/postgres:16-alpine";
environmentFiles = [
"/run/agenix/lemmy-env"
];
volumes = [
"/var/lib/lemmy/volumes/postgres:/var/lib/postgresql/data:rw,Z"
];
log-driver = "journald";
extraOptions = [
"--hostname=postgres-lemmy"
"--network-alias=postgres"
"--network=lemmy_default"
];
};
systemd.services."podman-lemmy-db" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"podman-network-lemmy_default.service"
];
requires = [
"podman-network-lemmy_default.service"
];
partOf = [
"podman-compose-lemmy-root.target"
];
wantedBy = [
"podman-compose-lemmy-root.target"
];
};
virtualisation.oci-containers.containers."lemmy-pictrs" = {
image = "docker.io/asonix/pictrs:0.5";
environmentFiles = [
"/run/agenix/lemmy-env"
];
volumes = [
"/var/lib/lemmy/volumes/pictrs:/mnt:rw,Z"
];
user = "991:991";
log-driver = "journald";
extraOptions = [
"--hostname=pictrs"
"--memory=723517440b"
"--network-alias=pictrs"
"--network=lemmy_default"
];
};
systemd.services."podman-lemmy-pictrs" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"podman-network-lemmy_default.service"
];
requires = [
"podman-network-lemmy_default.service"
];
partOf = [
"podman-compose-lemmy-root.target"
];
wantedBy = [
"podman-compose-lemmy-root.target"
];
};
virtualisation.oci-containers.containers."lemmy-proxy" = {
image = "nginx:1-alpine";
environmentFiles = [
"/run/agenix/lemmy-env"
];
volumes = [
"/var/lib/lemmy/nginx_internal.conf:/etc/nginx/nginx.conf:ro,Z"
"/var/lib/lemmy/proxy_params:/etc/nginx/proxy_params:ro,Z"
];
ports = [
"127.0.0.1:1236:8536/tcp"
];
dependsOn = [
"lemmy-pictrs"
"lemmy-ui"
];
log-driver = "journald";
extraOptions = [
"--network-alias=proxy"
"--network=lemmy_default"
];
};
systemd.services."podman-lemmy-proxy" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"podman-network-lemmy_default.service"
];
requires = [
"podman-network-lemmy_default.service"
];
partOf = [
"podman-compose-lemmy-root.target"
];
wantedBy = [
"podman-compose-lemmy-root.target"
];
};
virtualisation.oci-containers.containers."lemmy-ui" = {
image = "dessalines/lemmy-ui:0.19.13";
environmentFiles = [
"/run/agenix/lemmy-env"
];
volumes = [
"/var/lib/lemmy/volumes/lemmy-ui/extra_themes:/app/extra_themes:rw"
];
dependsOn = [
"lemmy-backend"
"lemmy-pictrs"
];
log-driver = "journald";
extraOptions = [
"--network-alias=lemmy-ui"
"--network=lemmy_default"
];
};
systemd.services."podman-lemmy-ui" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"podman-network-lemmy_default.service"
];
requires = [
"podman-network-lemmy_default.service"
];
partOf = [
"podman-compose-lemmy-root.target"
];
wantedBy = [
"podman-compose-lemmy-root.target"
];
};
# Networks
systemd.services."podman-network-lemmy_default" = {
path = [pkgs.podman];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "podman network rm -f lemmy_default";
};
script = ''
podman network inspect lemmy_default || podman network create lemmy_default
'';
partOf = ["podman-compose-lemmy-root.target"];
wantedBy = ["podman-compose-lemmy-root.target"];
};
# Root service
# When started, this will automatically create all resources and start
# the containers. When stopped, this will teardown all resources.
systemd.targets."podman-compose-lemmy-root" = {
unitConfig = {
Description = "Root target generated by compose2nix.";
};
wantedBy = ["multi-user.target"];
};
services.traefik.dynamicConfigOptions.http = {
services.lemmy.loadBalancer.servers = [
{
url = "http://localhost:1236";
}
];
routers.lemmy = {
rule = "Host(`lemmy.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "lemmy";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/lemmy"];
server.paths = ["/var/lib/lemmy"];
};
}

View file

@ -0,0 +1,31 @@
{config, ...}: {
services.plausible = {
enable = true;
server = {
baseUrl = "https://plausible.cronyakatsuki.xyz";
secretKeybaseFile = "${config.age.secrets.plausible.path}";
};
};
services.traefik.dynamicConfigOptions.http = {
services.plausible.loadBalancer.servers = [
{
url = "http://localhost:8000";
}
];
routers.plausible = {
rule = "Host(`plausible.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "plausible";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/plausible"];
server.paths = ["/var/lib/plausible"];
};
}

View file

@ -0,0 +1,15 @@
{...}: let
servicesDir = ./services;
serviceFiles =
builtins.filter
(name: builtins.match "^.*\\.nix$" name != null)
(builtins.attrNames (builtins.readDir servicesDir));
hostModules = map (fn: import "${servicesDir}/${fn}") serviceFiles;
in {
imports =
[
../../common
./secrets.nix
]
++ hostModules;
}

View file

@ -0,0 +1,48 @@
{config, ...}: {
age = {
secrets = {
wg-tyr = {
file = ../../../../secrets/wg-tyr.age;
};
duckdns = {
file = ../../../../secrets/duckdns.age;
};
restic-server-local-pass = {
file = ../../../../secrets/restic-server-local-pass.age;
};
restic-server-pass = {
file = ../../../../secrets/restic-server-pass.age;
};
restic-server-repo = {
file = ../../../../secrets/restic-server-repo.age;
};
restic-server-env = {
file = ../../../../secrets/restic-server-env.age;
};
glance = {
file = ../../../../secrets/glance.age;
};
traefik = {
file = ../../../../secrets/traefik.age;
owner = "traefik";
};
ddns = {
file = ../../../../secrets/ddns.age;
path = "/var/lib/ddns-updater/config.json";
owner = "nobody";
group = "nogroup";
symlink = false;
};
linkwarden = {
file = ../../../../secrets/linkwarden.age;
owner = config.services.linkwarden.user;
};
linkwarden-db = {
file = ../../../../secrets/linkwarden.age;
};
paperless-ngx = {
file = ../../../../secrets/paperless-ngx.age;
};
};
};
}

View file

@ -0,0 +1,118 @@
{config, ...}: {
# Setup blocky for adblocking
services.blocky = {
enable = true;
settings = {
ports.dns = 53;
connectIPVersion = "v4";
upstreams.groups.default = [
"127.0.0.1:553"
];
# For initially solving DoH/DoT Requests when no system Resolver is available.
bootstrapDns = {
upstream = "https://one.one.one.one/dns-query";
ips = ["1.1.1.1" "1.0.0.1"];
};
blocking = {
denylists = {
"default" = [
"https://codeberg.org/hagezi/mirror2/raw/branch/main/dns-blocklists/wildcard/pro.txt"
"https://codeberg.org/hagezi/mirror2/raw/branch/main/dns-blocklists/wildcard/fake.txt"
"https://codeberg.org/hagezi/mirror2/raw/branch/main/dns-blocklists/wildcard/popupads.txt"
"https://codeberg.org/hagezi/mirror2/raw/branch/main/dns-blocklists/wildcard/tif.txt"
"https://codeberg.org/hagezi/mirror2/raw/branch/main/dns-blocklists/wildcard/hoster.txt"
"https://codeberg.org/hagezi/mirror2/raw/branch/main/dns-blocklists/wildcard/gambling.txt"
"https://codeberg.org/hagezi/mirror2/raw/branch/main/dns-blocklists/wildcard/native.samsung.txt"
];
};
allowlists = {
"default" = [
''
jnn-pa.googleapis.com
challenges.cloudflare.com
''
];
};
clientGroupsBlock.default = ["default"];
};
caching = {
prefetching = true;
minTime = "1m";
};
clientLookup = {
upstream = "192.168.0.1";
singleNameOrder = [1];
};
};
};
# Setup unbound for recursive dns
services.unbound = {
enable = true;
settings = {
server = {
interface = ["127.0.0.1"];
port = 553;
do-ip4 = true;
do-ip6 = false;
access-control = ["127.0.0.1 allow"];
harden-glue = true;
harden-dnssec-stripped = true;
use-caps-for-id = false;
edns-buffer-size = 1232;
hide-identity = true;
hide-version = true;
prefetch = true;
cache-max-ttl = 60;
cache-max-negative-ttl = 60;
serve-original-ttl = true;
local-zone = [''"home.cronyakatsuki.xyz." transparent''];
local-data = [
''"glance.home.cronyakatsuki.xyz IN A 192.168.0.5"''
''"syncthing.home.cronyakatsuki.xyz IN A 192.168.0.5"''
''"wallos.home.cronyakatsuki.xyz IN A 192.168.0.5"''
''"assistant.home.cronyakatsuki.xyz IN A 192.168.0.5"''
''"ddns.home.cronyakatsuki.xyz IN A 192.168.0.5"''
''"linkwarden.home.cronyakatsuki.xyz IN A 192.168.0.5"''
''"paperless.home.cronyakatsuki.xyz IN A 192.168.0.5"''
''"komga.home.cronyakatsuki.xyz IN A 192.168.0.5"''
];
};
};
};
# Setup ddns-updater
services.ddns-updater = {
enable = true;
environment = {
RESOLVER_ADDRESS = "127.0.0.1:53";
PERIOD = "30s";
};
};
services.traefik.dynamicConfigOptions.http = {
services.ddns.loadBalancer.servers = [
{
url = "http://localhost:8000";
}
];
routers.ddns = {
rule = "Host(`ddns.home.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "ddns";
entrypoints = "websecure";
};
};
}

View file

@ -0,0 +1,614 @@
{config, ...}: {
services.glance = {
enable = true;
openFirewall = false;
settings = {
server = {
host = "0.0.0.0";
};
pages = [
{
name = "Home";
columns = [
{
size = "small";
widgets = [
{
type = "calendar";
first-day-of-week = "monday";
}
{
type = "twitch-channels";
channels = [
"theprimeagen"
];
}
{
type = "rss";
title = "Rss Feeds";
cache = "1h";
feeds = [
{
url = "https://github.com/NixOS/nixpkgs/commits/nixpkgs-unstable.atom";
title = "Nixpkgs Unstable";
}
];
}
];
}
{
size = "full";
widgets = [
{
type = "group";
widgets = [
{type = "hacker-news";}
{type = "lobsters";}
];
}
{
type = "server-stats";
servers = [
{
type = "local";
name = "Tyr";
}
];
}
{
type = "custom-api";
title = "Beszel stats";
cache = "5m";
options = {
base-url = "\${BESZEL_URL}";
api-key = "\${BESZEL_TOKEN}";
};
template = ''
{{/* Required config options */}}
{{ $baseURL := .Options.StringOr "base-url" "" }}
{{ $apiKey := .Options.StringOr "api-key" "" }}
{{/* Error message template */}}
{{ define "errorMsg" }}
<div class="widget-error-header">
<div class="color-negative size-h3">ERROR</div>
<svg class="widget-error-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"></path>
</svg>
</div>
<p class="break-all">{{ . }}</p>
{{ end }}
{{ define "formatGigabytes" }}
{{ $value := . }}
{{ $label := "GB" }}
{{- if lt $value 1.0 }}
{{ $value = mul $value 1000.0 }}
{{ $label = "MB" }}
{{- else if lt $value 1000.0 }}
{{ else }}
{{ $value = div $value 1000.0 }}
{{ $label = "TB" }}
{{ end }}
{{ printf "%.1f" $value }} <span class="color-base size-h5">{{ $label }}</span>
{{ end }}
{{/* Check required fields */}}
{{ if or (eq $baseURL "") (eq $apiKey "") }}
{{ template "errorMsg" "Some required options are not set." }}
{{ else }}
{{ $token := concat "Bearer " $apiKey }}
{{ $systemsResponse := newRequest (print $baseURL "/api/collections/systems/records")
| withHeader "Authorization" $token
| getResponse }}
{{ $systems := $systemsResponse.JSON.Array "items" }}
{{ range $n, $system := $systems }}
{{ $status := $system.String "status" }}
{{ $systemStatsRequest := newRequest (print $baseURL "/api/collections/system_stats/records")
| withHeader "Authorization" $token
| withParameter "sort" "-created"
| withParameter "page" "1"
| withParameter "perPage" "1"
| withParameter "filter" (print "type='1m'&&system='" ($system.String "id") "'")
| getResponse }}
{{ $systemStats := index ($systemStatsRequest.JSON.Array "items") 0 }}
{{ $hostname := $system.String "name" }}
{{ $uptimeSec := $system.Float "info.u" }}
{{ $systemTemp := $system.Float "info.dt"}}
{{ $cpuLoad := $system.Float "info.cpu" }}
{{ $cpuLoad1m := $system.Float "info.l1" }}
{{ $cpuLoad15m := $system.Float "info.l15" }}
{{ $memoryUsedPercent := $system.Float "info.mp" }}
{{ $memoryTotalGb := $systemStats.Float "stats.m" }}
{{ $memoryUsedGb := $systemStats.Float "stats.mu" }}
{{ $swapTotalGb := $systemStats.Float "stats.s" }}
{{ $swapUsedGb := $systemStats.Float "stats.su" }}
{{ $swapUsedPercent := mul (div $swapUsedGb $swapTotalGb) 100.0 }}
{{ $rootUsedPercent := $system.Float "info.dp" }}
{{ $rootTotalGb := $systemStats.Float "stats.d" }}
{{ $rootUsedGb := $systemStats.Float "stats.du" }}
<div class="server">
<div class="server-info">
<div class="server-details">
<div class="server-name color-highlight size-h3">{{ $hostname }}</div>
<div>
{{ if eq $status "up" }}
<span>{{ printf "%.1f" (mul $uptimeSec 0.000011574) }}d</span> uptime
{{ else }}
unreachable
{{ end }}
</div>
</div>
<div class="shrink-0"{{ if eq $status "up" }} data-popover-type="html" data-popover-margin="0.2rem" data-popover-max-width="400px"{{ end }}>
{{- if eq $status "up" }}
<div data-popover-html>
<div class="flex">
<div class="size-h5 text-compact">Kernel</div>
<div class="value-separator"></div>
<div class="color-highlight">{{ $system.String "info.k" }}</div>
</div>
<div class="flex">
<div class="size-h5 text-compact">CPU</div>
<div class="value-separator"></div>
<div class="color-highlight">{{ $system.String "info.m" }}</div>
</div>
</div>
{{- end }}
<svg class="server-icon" stroke="var(--color-{{ if eq $status "up" }}positive{{ else }}negative{{ end }})" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 17.25v-.228a4.5 4.5 0 0 0-.12-1.03l-2.268-9.64a3.375 3.375 0 0 0-3.285-2.602H7.923a3.375 3.375 0 0 0-3.285 2.602l-2.268 9.64a4.5 4.5 0 0 0-.12 1.03v.228m19.5 0a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3m19.5 0a3 3 0 0 0-3-3H5.25a3 3 0 0 0-3 3m16.5 0h.008v.008h-.008v-.008Zm-3 0h.008v.008h-.008v-.008Z" />
</svg>
</div>
</div>
<div class="server-stats">
<div class="flex-1">
<div class="flex items-end size-h5">
<div>CPU</div>
{{- if ge $systemTemp 80.0 }}
<svg class="server-spicy-cpu-icon" fill="var(--color-negative)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" >
<path fill-rule="evenodd" d="M8.074.945A4.993 4.993 0 0 0 6 5v.032c.004.6.114 1.176.311 1.709.16.428-.204.91-.61.7a5.023 5.023 0 0 1-1.868-1.677c-.202-.304-.648-.363-.848-.058a6 6 0 1 0 8.017-1.901l-.004-.007a4.98 4.98 0 0 1-2.18-2.574c-.116-.31-.477-.472-.744-.28Zm.78 6.178a3.001 3.001 0 1 1-3.473 4.341c-.205-.365.215-.694.62-.59a4.008 4.008 0 0 0 1.873.03c.288-.065.413-.386.321-.666A3.997 3.997 0 0 1 8 8.999c0-.585.126-1.14.351-1.641a.42.42 0 0 1 .503-.235Z" clip-rule="evenodd" />
</svg>
{{- end }}
<div class="color-highlight margin-left-auto text-very-compact">{{ $cpuLoad }} <span class="color-base">%</span></div>
</div>
<div data-popover-type="html">
<div data-popover-html>
<div class="flex">
<div class="size-h5">1M AVG</div>
<div class="value-separator"></div>
<div class="color-highlight text-very-compact">{{ printf "%.1f" $cpuLoad1m }} <span class="color-base size-h5">%</span></div>
</div>
<div class="flex margin-top-3">
<div class="size-h5">15M AVG</div>
<div class="value-separator"></div>
<div class="color-highlight text-very-compact">{{ printf "%.1f" $cpuLoad15m }} <span class="color-base size-h5">%</span></div>
</div>
<div class="flex margin-top-3">
<div class="size-h5">TEMP C</div>
<div class="value-separator"></div>
<div class="color-highlight text-very-compact">{{ printf "%.1f" $systemTemp }} <span class="color-base size-h5">°</span></div>
</div>
</div>
<div class="progress-bar progress-bar-combined">
<div class="progress-value{{ if ge $cpuLoad1m 85.0 }} progress-value-notice{{ end }}" style="--percent: {{ $cpuLoad1m }}"></div>
<div class="progress-value{{ if ge $cpuLoad15m 85.0 }} progress-value-notice{{ end }}" style="--percent: {{ $cpuLoad15m }}"></div>
</div>
</div>
</div>
<div class="flex-1">
<div class="flex justify-between items-end size-h5">
<div>RAM</div>
<div class="color-highlight text-very-compact">{{ $memoryUsedPercent }} <span class="color-base">%</span></div>
</div>
<div data-popover-type="html">
<div data-popover-html>
<div class="flex">
<div class="size-h5">RAM</div>
<div class="value-separator"></div>
<div class="color-highlight text-very-compact">
{{ template "formatGigabytes" $memoryUsedGb }} <span class="color-base size-h5">/</span> {{ template "formatGigabytes" $memoryTotalGb }}
</div>
</div>
{{- if gt $swapTotalGb 0.0 }}
<div class="flex margin-top-3">
<div class="size-h5">SWAP</div>
<div class="value-separator"></div>
<div class="color-highlight text-very-compact">
{{ template "formatGigabytes" $swapUsedGb }} <span class="color-base size-h5">/</span> {{ template "formatGigabytes" $swapTotalGb }}
</div>
</div>
{{- end }}
</div>
<div class="progress-bar progress-bar-combined">
<div class="progress-value{{ if ge $memoryUsedPercent 85.0 }} progress-value-notice{{ end }}" style="--percent: {{ $memoryUsedPercent }}"></div>
{{- if gt $swapTotalGb 0.0 }}
<div class="progress-value{{ if ge $swapUsedPercent 85.0 }} progress-value-notice{{ end }}" style="--percent: {{ $swapUsedPercent }}"></div>
{{- end }}
</div>
</div>
</div>
<div class="flex-1">
<div class="flex justify-between items-end size-h5">
<div>DISK</div>
<div class="color-highlight text-very-compact">{{ $rootUsedPercent }} <span class="color-base">%</span></div>
</div>
<div data-popover-type="html">
<div data-popover-html>
<ul class="list list-gap-2">
<li class="flex">
<div class="size-h5">/</div>
<div class="value-separator"></div>
<div class="color-highlight text-very-compact">
{{ template "formatGigabytes" $rootUsedGb }} <span class="color-base size-h5">/</span> {{ template "formatGigabytes" $rootTotalGb }}
</div>
</li>
{{ range $key, $efs := ($systemStats.Get "stats.efs").Map }}
<li class="flex">
<div class="size-h5">{{ $key }}</div>
<div class="value-separator"></div>
<div class="color-highlight text-very-compact">
{{ template "formatGigabytes" (($efs.Get "du").Float) }} <span class="color-base size-h5">/</span> {{ template "formatGigabytes" (($efs.Get "d").Float) }}
</div>
</li>
{{ end }}
</ul>
</div>
<div class="progress-bar progress-bar-combined">
<div class="progress-value{{ if ge $rootUsedPercent 85.0 }} progress-value-notice{{ end }}" style="--percent: {{ $rootUsedPercent }}"></div>
{{ range $key, $efs := ($systemStats.Get "stats.efs").Map }}
{{ $efsTotalGb := (($efs.Get "d").Float) }}
{{ $efsUsedGb := (($efs.Get "du").Float) }}
{{ $efsPercent := mul (div $efsUsedGb $efsTotalGb) 100 }}
<div class="progress-value{{ if ge $efsPercent 85.0 }} progress-value-notice{{ end }}" style="--percent: {{ $efsPercent }}"></div>
{{ end }}
</div>
</div>
</div>
</div>
</div>
{{ end }}
{{ end }}
'';
}
];
}
{
size = "small";
widgets = [
{
type = "weather";
units = "metric";
hour-format = "24h";
location = "Otočac, Hrvatska";
}
{
type = "releases";
cache = "1d";
repositories = [
"glanceapp/glance"
"immich-app/immich"
"syncthing/syncthing"
];
}
{
type = "custom-api";
title = "GitHub Notifications";
url = "https://api.github.com/notifications?all=true&per_page=20";
headers = {
Authorization = "Bearer \${GITHUB_TOKEN}";
Accept = "application/vnd.github+json";
};
template = ''
<ul class="list list-gap-14 collapsible-container" data-collapse-after="6">
{{ range .JSON.Array "" }}
{{ $url := concat (.String "repository.html_url") "/actions" }}
{{ if ne (.String "subject.url") "" }}
{{
$notification := newRequest (.String "subject.url")
| withHeader "Authorization" "Bearer ''${GITHUB_TOKEN}"
| getResponse
}}
{{ if eq $notification.Response.StatusCode 200 }}
{{ $url = $notification.JSON.String "html_url" }}
{{ else }}
{{ $url = (.String "subject.url") | replaceMatches "repos\\/" "" | replaceMatches "api\\." "" | replaceAll "pulls" "pull" }}
{{ end }}
{{ end }}
<li>
<a href="{{ $url }}" class="size-title-dynamic {{ if .Bool "unread" }}color-primary-if-not-visited{{ else }}negative-color{{ end }}" target="_blank" rel="noreferrer">{{ .String "subject.title" }}</a>
<ul class="list-horizontal-text flex-nowrap">
<li class="min-width-0" {{ .String "updated_at" | parseTime "rfc3339" | toRelativeTime }}></li>
<li class="min-width-0"><a target="_blank" href="{{ $url }}">{{ .String "repository.full_name" }}</a></li>
</ul>
</li>
{{ end }}
</ul>
'';
}
];
}
];
}
{
name = "Gaming";
columns = [
{
size = "small";
widgets = [
{
type = "twitch-top-games";
limit = 20;
collapse-after = 13;
exclude = [
"just-chatting"
"pools-hot-tubs-and-beaches"
"music"
"art"
"asmr"
];
}
];
}
{
size = "full";
widgets = [
{
type = "group";
widgets = [
{
type = "reddit";
show-thumbnails = true;
subreddit = "pcgaming";
}
{
type = "reddit";
subreddit = "games";
}
];
}
{
type = "videos";
style = "grid-cards";
collapse-after-rows = 3;
channels = [
"UCNvzD7Z-g64bPXxGzaQaa4g" # GameRanx
"UCZ7AeeVbyslLM_8-nVy2B8Q" # Skill Up
"UCHDxYLv8iovIbhrfl16CNyg" # GameLinked
"UC9PBzalIcEQCsiIkq36PyUA" # Digital Foundry
];
}
];
}
{
size = "small";
widgets = [
{
type = "reddit";
subreddit = "gamingnews";
limit = 7;
style = "vertical-cards";
}
];
}
];
}
{
name = "SelfHosted Services";
columns = [
{
size = "small";
widgets = [
{
type = "custom-api";
title = "Audiobookshelf";
title-url = "\${AUDIOBOOKSHELF_URL}";
options = {
base-url = "\${AUDIOBOOKSHELF_URL}";
api-key = "\${AUDIOBOOKSHELF_KEY}";
};
cache = "5m";
template = ''
{{ $baseURL := .Options.StringOr "base-url" "" }}
{{ $apiKey := .Options.StringOr "api-key" "" }}
{{ define "errorMsg" }}
<div class="widget-error-header">
<div class="color-negative size-h3">ERROR</div>
<svg class="widget-error-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"></path>
</svg>
</div>
<p class="break-all">{{ . }}</p>
{{ end }}
{{ $bearer := printf "Bearer %s" $apiKey }}
{{ $librariesRequestURL := concat $baseURL "/api/libraries" }}
{{ $librariesResponse := newRequest $librariesRequestURL
| withHeader "Content-Type" "application/json"
| withHeader "Authorization" $bearer
| getResponse }}
{{ if $librariesResponse.JSON.Exists "libraries" }}
{{ $all_libraries := $librariesResponse.JSON.Array "libraries" }}
{{ $books_count := 0 }}
{{ $books_duration := 0 }}
{{ $podcasts_count := 0 }}
{{ $podcasts_duration := 0 }}
{{ range $library := $all_libraries }}
{{ $lib_id := $library.String "id" }}
{{ $lib_request_url := concat $baseURL "/api/libraries/" $lib_id "/stats"}}
{{ $lib_stats := newRequest $lib_request_url
| withHeader "Content-Type" "application/json"
| withHeader "Authorization" $bearer
| getResponse }}
{{ $lib_type := $library.String "mediaType" }}
{{ $lib_item_count := $lib_stats.JSON.Int "totalItems" }}
{{ $lib_total_duration := $lib_stats.JSON.Int "totalDuration" }}
{{ if eq $lib_type "book" }}
{{ $books_count = add $books_count $lib_item_count }}
{{ $books_duration = add $books_duration $lib_total_duration }}
{{ else if eq $lib_type "podcast" }}
{{ $podcasts_count = add $podcasts_count $lib_item_count }}
{{ $podcasts_duration = add $podcasts_duration $lib_total_duration }}
{{ end }}
{{ end }}
{{ $books_duration = duration (concat (printf "%d" $books_duration) "s") }}
{{ $podcasts_duration = duration (concat (printf "%d" $podcasts_duration) "s") }}
<div class="flex flex-column gap-5">
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h3">{{ $books_count }}</div>
<div class="size-h5 uppercase">Books</div>
</div>
<div>
<div class="color-highlight size-h3">{{ $books_duration }}</div>
<div class="size-h5 uppercase">Duration</div>
</div>
<div>
<div class="color-highlight size-h3">{{ $podcasts_count }}</div>
<div class="size-h5 uppercase">Podcasts</div>
</div>
<div>
<div class="color-highlight size-h3">{{ $podcasts_duration }}</div>
<div class="size-h5 uppercase">Duration</div>
</div>
</div>
</div>
{{ else }}
{{ template "errorMsg" "Could not fetch data from API!" }}
{{ end }}
'';
}
{
type = "custom-api";
title = "Immich Stats";
cache = "1d";
url = "https://immich.cronyakatsuki.xyz/api/server/statistics";
headers = {
x-api-key = "\${IMMICH_API_KEY}";
Accept = "application/json";
};
template = ''
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h3">{{ .JSON.Int "photos" | formatNumber }}</div>
<div class="size-h6">PHOTOS</div>
</div>
<div>
<div class="color-highlight size-h3">{{ .JSON.Int "videos" | formatNumber }}</div>
<div class="size-h6">VIDEOS</div>
</div>
<div>
<div class="color-highlight size-h3">{{ div (.JSON.Int "usage" | toFloat) 1073741824 | toInt | formatNumber }}GB</div>
<div class="size-h6">USAGE</div>
</div>
</div>
'';
}
{
type = "custom-api";
title = "Jellyfin Stats";
base-url = "\${JELLYFIN_URL}";
options = {
url = "\${JELLYFIN_URL}";
key = "\${JELLYFIN_KEY}";
};
template = ''
{{ $url := .Options.StringOr "url" "" }}
{{ $key := .Options.StringOr "key" "" }}
{{- if or (eq $url "") (eq $key "") -}}
<p>Error: The URL or API Key was not configured in the widget options.</p>
{{- else -}}
{{- $requestUrl := printf "%s/emby/Items/Counts?api_key=%s" $url $key -}}
{{- $jellyfinData := newRequest $requestUrl | getResponse -}}
{{- if eq $jellyfinData.Response.StatusCode 200 -}}
<div class="flex flex-column gap-5">
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h3">{{ $jellyfinData.JSON.Int "MovieCount" | formatNumber }}</div>
<div class="size-h5 uppercase">Movies</div>
</div>
<div>
<div class="color-highlight size-h3">{{ $jellyfinData.JSON.Int "SeriesCount" | formatNumber }}</div>
<div class="size-h5 uppercase">TV Shows</div>
</div>
<div>
<div class="color-highlight size-h3">{{ $jellyfinData.JSON.Int "EpisodeCount" | formatNumber }}</div>
<div class="size-h5 uppercase">Episodes</div>
</div>
<div>
<div class="color-highlight size-h3">{{ $jellyfinData.JSON.Int "SongCount" | formatNumber }}</div>
<div class="size-h5 uppercase">Songs</div>
</div>
</div>
</div>
{{- else -}}
<p>Failed: {{ $jellyfinData.Response.Status }}</p>
{{- end -}}
{{- end -}}
'';
}
];
}
{
size = "full";
widgets = [
];
}
];
}
];
};
};
systemd.services.glance.serviceConfig = {
EnvironmentFile = ["${config.age.secrets.glance.path}"];
};
services.traefik.dynamicConfigOptions.http = {
services.glance.loadBalancer.servers = [
{
url = "http://localhost:8080";
}
];
routers.glance = {
rule = "Host(`glance.home.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "glance";
entrypoints = "websecure";
};
};
}

View file

@ -0,0 +1,48 @@
{
virtualisation.oci-containers.containers.homeassistant = {
image = "docker.io/homeassistant/home-assistant:stable";
autoStart = true;
ports = [
"8123:8123"
];
devices = [
"/dev/ttyUSB0:/dev/ttyUSB0"
];
privileged = true;
capabilities = {
NET_ADMIN = true;
NET_RAW = true;
};
labels = {
"io.containers.autoupdate" = "registry";
};
extraOptions = ["--network=host"];
volumes = [
"/etc/localtime:/etc/localtime:ro"
"/var/lib/homeassistant:/config"
"/run/dbus:/run/dbus:ro"
];
};
services.restic.backups = {
local.paths = ["/var/lib/homeassistant"];
server.paths = ["/var/lib/homeassistant"];
};
services.traefik.dynamicConfigOptions.http = {
services.assistant.loadBalancer.servers = [
{
url = "http://localhost:8123";
}
];
routers.assistant = {
rule = "Host(`assistant.home.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "assistant";
entrypoints = "websecure";
};
};
}

View file

@ -0,0 +1,28 @@
{
services.komga = {
enable = true;
settings.server.port = 8081;
};
services.traefik.dynamicConfigOptions.http = {
services.komga.loadBalancer.servers = [
{
url = "http://localhost:8081";
}
];
routers.komga = {
rule = "Host(`komga.home.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "komga";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/komga"];
server.paths = ["/var/lib/komga"];
};
}

View file

@ -0,0 +1,31 @@
{config, ...}: {
services.linkwarden = {
enable = true;
secretFiles = {
NEXTAUTH_SECRET = config.age.secrets.linkwarden.path;
POSTGRES_PASSWORD = config.age.secrets.linkwarden-db.path;
};
};
services.restic.backups = {
local.paths = ["/var/lib/linkwarden"];
server.paths = ["/var/lib/linkwarden"];
};
services.traefik.dynamicConfigOptions.http = {
services.linkwarden.loadBalancer.servers = [
{
url = "http://localhost:3000";
}
];
routers.linkwarden = {
rule = "Host(`linkwarden.home.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "linkwarden";
entrypoints = "websecure";
};
};
}

View file

@ -0,0 +1,18 @@
{
services.mosquitto = {
enable = true;
listeners = [
{
users.crony = {
acl = ["readwrite #"];
hashedPassword = "$7$101$3MqAfbz8vp9VMrMG$nvHnl1fEX1H3JeH98JGBjdBiKZ02RW7kSBMSQK2fHzU+3hinebJxW8QMpdaH9TYKoeM9PS0y+pzvYnrk0/tkIQ==";
};
}
];
};
networking.firewall = {
enable = true;
allowedTCPPorts = [1883];
};
}

View file

@ -0,0 +1,9 @@
{
services.nfs.server = {
enable = true;
exports = ''
/export/nfs 192.168.0.0/24(rw,sync,no_subtree_check) 172.16.0.0/24(rw,sync,no_subtree_check)
'';
};
networking.firewall.allowedTCPPorts = [2049];
}

View file

@ -0,0 +1,34 @@
{config, ...}: {
services.paperless = {
enable = true;
passwordFile = config.age.secrets.paperless-ngx.path;
domain = "paperless.home.cronyakatsuki.xyz";
settings = {
PAPERLESS_OCR_LANGUAGE = "hrv+eng";
PAPERLESS_ADMIN_USER = "crony";
PAPERLESS_URL = "https://paperless.home.cronyakatsuki.xyz";
};
};
services.traefik.dynamicConfigOptions.http = {
services.paperless.loadBalancer.servers = [
{
url = "http://localhost:28981";
}
];
routers.paperless = {
rule = "Host(`paperless.home.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "paperless";
entrypoints = "websecure";
};
};
services.restic.backups = {
local.paths = ["/var/lib/paperless"];
server.paths = ["/var/lib/paperless"];
};
}

View file

@ -0,0 +1,29 @@
{
services.syncthing = {
enable = true;
openDefaultPorts = true;
guiAddress = "0.0.0.0:8384";
};
services.restic.backups = {
local.paths = ["/var/lib/syncthing"];
server.paths = ["/var/lib/syncthing"];
};
services.traefik.dynamicConfigOptions.http = {
services.syncthing.loadBalancer.servers = [
{
url = "http://localhost:8384";
}
];
routers.syncthing = {
rule = "Host(`syncthing.home.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "syncthing";
entrypoints = "websecure";
};
};
}

View file

@ -0,0 +1,45 @@
{config, ...}: {
services.traefik = {
enable = true;
staticConfigOptions = {
serversTransport.insecureSkipVerify = true;
log = {level = "DEBUG";};
certificatesResolvers = {
porkbun = {
acme = {
email = "crony@cronyakatsuki.xyz";
storage = "/var/lib/traefik/acme.json";
caserver = "https://acme-v02.api.letsencrypt.org/directory";
dnsChallenge = {
provider = "porkbun";
resolvers = ["127.0.0.1"];
propagation = {
delayBeforeChecks = 60;
disableChecks = true;
};
};
};
};
};
api = {};
entryPoints = {
web = {
address = ":80";
http.redirections.entryPoint = {
to = "websecure";
scheme = "https";
};
};
websecure = {
address = ":443";
};
};
};
};
systemd.services.traefik.serviceConfig = {
EnvironmentFile = ["${config.age.secrets.traefik.path}"];
};
networking.firewall.allowedTCPPorts = [80 443];
}

View file

@ -0,0 +1,38 @@
{
virtualisation.oci-containers.containers.wallos = {
image = "docker.io/bellamy/wallos:latest";
autoStart = true;
ports = [
"8282:80/tcp"
];
labels = {
"io.containers.autoupdate" = "registry";
};
volumes = [
"/var/lib/wallos/db:/var/www/html/db"
"/var/lib/wallos/logos:/var/www/html/images/uploads/logos"
];
};
services.restic.backups = {
local.paths = ["/var/lib/wallos"];
server.paths = ["/var/lib/wallos"];
};
services.traefik.dynamicConfigOptions.http = {
services.wallos.loadBalancer.servers = [
{
url = "http://localhost:8282";
}
];
routers.wallos = {
rule = "Host(`wallos.home.cronyakatsuki.xyz`)";
tls = {
certResolver = "porkbun";
};
service = "wallos";
entrypoints = "websecure";
};
};
}

View file

@ -0,0 +1,17 @@
{config, ...}: {
networking = {
nat = {
enable = true;
enableIPv6 = true;
externalInterface = "enp1s0";
internalInterfaces = ["wg0"];
};
firewall = {
allowedTCPPorts = [53];
allowedUDPPorts = [53 51820];
};
wg-quick.interfaces.wg0.configFile = "${config.age.secrets.wg-tyr.path}";
};
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
}