This commit is contained in:
Joakim Repomaa
2025-02-08 19:44:52 +02:00
parent 271d9e8f88
commit 913d3d1238
32 changed files with 844 additions and 544 deletions

89
modules/services/bin.nix Normal file
View File

@@ -0,0 +1,89 @@
{ lib, pkgs, config, ... }:
let
cfg = config.modules.services.bin;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
rustypasteConfig = (pkgs.formats.toml { }).generate "rustypaste-config.toml" {
server = {
address = "[::1]:${toString cfg.port}";
max_content_length = "1GB";
upload_path = "/var/lib/rustypaste/uploads";
timeout = "5m";
};
landing_page = {
text = ''
Submit files via HTTP POST here:
curl -F 'file=@example.txt' https://${fqdn}
This will return the URL of the uploaded file.
The server administrator might remove any pastes that they do not personally
want to host.
If you are the server administrator and want to change this page, just go
into your config file and change it! If you change the expiry time, it is
recommended that you do.
By default, pastes expire every hour. The server admin may or may not have
changed this.
Check out the GitHub repository at https://github.com/orhun/rustypaste
Command line tool is available at https://github.com/orhun/rustypaste-cli
'';
content_type = "text/plain; charset=utf-8";
};
paste = {
default_extension = "txt";
random_url = { type = "petname"; words = 2; separator = "-"; };
delete_expirted_files = { enabled = true; interval = "1h"; };
default_expiry = "100y";
mime_override = [
{ mime = "text/plain"; regex = "^.*\.(log|txt|diff|sh|rs|toml|cr|nix|rb|js|tsx|ts|jsx)$"; }
];
};
};
in
{
options.modules.services.bin = {
enable = lib.mkEnableOption "Enable Rustypaste";
subdomain = lib.mkOption {
type = lib.types.str;
};
port = lib.mkOption {
type = lib.types.int;
default = 3600;
};
};
config = lib.mkIf cfg.enable {
systemd.services.rustypaste = {
enable = true;
description = "Rustypaste pastebin";
environment = {
CONFIG = rustypasteConfig;
AUTH_TOKENS_FILE = "/var/secrets/rustypaste-tokens";
};
serviceConfig = {
ExecStart = "${pkgs.rustypaste}/bin/rustypaste";
WorkingDirectory = "/var/lib/rustypaste";
StateDirectory = "rustypaste";
DynamicUser = true;
BindReadOnlyPaths = [ "/var/secrets/rustypaste-tokens" ];
};
wantedBy = [ "multi-user.target" ];
confinement = {
enable = true;
packages = [ rustypasteConfig ];
};
};
modules.services.webserver = {
enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
};
};
}

View File

@@ -0,0 +1,18 @@
{ ... }:
{
imports = [
./webserver.nix
./vaultwarden.nix
./immich.nix
./syncthing.nix
./invidious.nix
./grafana.nix
./gtrackmap.nix
./owncast.nix
./tailscale.nix
./workout-tracker.nix
./gotosocial.nix
./bin.nix
./workout-sync.nix
];
}

View File

@@ -0,0 +1,61 @@
{ config, lib, ... }:
let
cfg = config.modules.services.gotosocial;
domain = config.networking.domain;
fqdn = "${cfg.subdomain}.${domain}";
port = cfg.port;
in
{
options.modules.services.gotosocial = {
enable = lib.mkEnableOption "Enable Gotosocial";
subdomain = lib.mkOption {
type = lib.types.str;
};
port = lib.mkOption {
type = lib.types.int;
default = 3500;
};
settings = lib.mkOption {
type = lib.types.attrs;
default = { };
};
};
config = lib.mkIf cfg.enable {
services.gotosocial = {
enable = true;
environmentFile = "/var/secrets/gotosocial.env";
settings = {
host = fqdn;
account-domain = domain;
protocol = "https";
bind-address = "localhost";
instance-languages = [ "de" "fi" "en" ];
instance-inject-mastodon-version = true;
accounts-registration-open = true;
instance-expose-public-timeline = true;
letsencrypt-enabled = false;
inherit port;
} // cfg.settings;
setupPostgresqlDB = true;
};
modules.services.webserver.vHosts = {
${domain}.locations = lib.listToAttrs (
lib.map
(path: {
name = "/.well-known/${path}";
value.extraConfig = ''
rewrite ^.*$ https://${fqdn}/.well-known/${path} permanent;
'';
}) [
"host-meta"
"webfinger"
"nodeinfo"
]
);
"${fqdn}".locations."/".proxyPort = port;
};
};
}

View File

@@ -0,0 +1,72 @@
{ lib, config, ... }:
let
cfg = config.modules.services.grafana;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
in
{
options.modules.services.grafana = {
enable = lib.mkEnableOption "Enable Grafana";
subdomain = lib.mkOption {
type = lib.types.str;
};
config = lib.mkOption {
type = lib.types.attrs;
};
port = lib.mkOption {
type = lib.types.int;
default = 3300;
};
};
config = lib.mkIf cfg.enable {
services.grafana = {
enable = true;
settings = {
server = {
root_url = "https://${fqdn}";
http_port = cfg.port;
};
database = {
host = "/var/run/postgresql";
type = "postgres";
user = "grafana";
};
smtp = {
enabled = true;
host = "horologium.uberspace.de";
from_address = "noreply@freun.dev";
from_name = "Vaultwarden";
user = "noreply@freun.dev";
password = "$__file{/var/secrets/smtp-password}";
};
};
};
services.prometheus = {
enable = true;
exporters.node.enable = true;
scrapeConfigs = [
{
job_name = "node";
static_configs = [
{ targets = [ "localhost:9100" ]; }
];
}
];
};
modules.services.webserver.vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
services.postgresql = {
enable = lib.mkDefault true;
ensureDatabases = [ "grafana" ];
ensureUsers = [{
name = "grafana";
ensureDBOwnership = true;
}];
};
};
}

View File

@@ -0,0 +1,26 @@
{ lib, config, ... }:
let
cfg = config.modules.services.gtrackmap;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
in
{
options.modules.services.gtrackmap = {
enable = lib.mkEnableOption "Enable GTrackmap";
subdomain = lib.mkOption {
type = lib.types.str;
};
port = lib.mkOption {
type = lib.types.int;
default = 3200;
};
};
config = {
services.gtrackmap = {
enable = true;
inherit (cfg) port;
};
modules.services.webserver.vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
};
}

208
modules/services/immich.nix Normal file
View File

@@ -0,0 +1,208 @@
{ pkgs, lib, config, ... }:
let
cfg = config.modules.services.immich;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
volumeServices = names: (
lib.lists.foldl
(services: name:
services // {
"podman-volume-immich_${name}" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
podman volume inspect immich_${name} || podman volume create immich_${name}
'';
partOf = [ "podman-compose-immich-root.target" ];
wantedBy = [ "podman-compose-immich-root.target" ];
};
}
)
{ }
names
);
containerServices = services: (
lib.lists.foldl
(acc: { name, volumes ? [ ], ... }:
let
volume_services = map (volume: "podman-volume-immich_${volume}.service") volumes;
dependent_services = [ "podman-network-immich_default.service" ] ++ volume_services;
in
acc // {
"podman-immich_${name}" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
};
after = dependent_services;
requires = dependent_services;
partOf = [
"podman-compose-immich-root.target"
];
wantedBy = [
"podman-compose-immich-root.target"
];
};
}
)
{ }
services
);
in
{
options.modules.services.immich = {
enable = lib.mkEnableOption "Enable Immich";
subdomain = lib.mkOption {
type = lib.types.str;
};
version = lib.mkOption {
type = lib.types.str;
default = "latest";
};
port = lib.mkOption {
type = lib.types.int;
default = 2283;
};
dataDir = lib.mkOption {
type = lib.types.str;
};
storageDirs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
};
config = lib.mkIf cfg.enable
{
virtualisation.podman = {
enable = true;
autoPrune.enable = true;
dockerCompat = true;
defaultNetwork.settings = {
# Required for container networking to be able to use names.
dns_enabled = true;
};
};
virtualisation.oci-containers.backend = "podman";
networking.firewall.trustedInterfaces = [ "podman1" ];
modules.firewall.interfaces.podman1 = [ "dns" ];
# Containers
virtualisation.oci-containers.containers = {
"immich_machine_learning" = {
image = "ghcr.io/immich-app/immich-machine-learning:${cfg.version}";
environmentFiles = [
"/var/secrets/immich.env"
];
volumes = [
"immich_model_cache:/cache:rw"
];
log-driver = "journald";
extraOptions = [
"--network-alias=immich-machine-learning"
"--network=immich_default"
];
};
"immich_postgres" = {
image = "registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0";
environmentFiles = [
"/var/secrets/immich.env"
];
environment = {
POSTGRES_INITDB_ARGS = "--data-checksums";
};
volumes = [
"immich_db_data:/var/lib/postgresql/data:rw"
];
cmd = [ "postgres" "-c" "shared_preload_libraries=vectors.so" "-c" "search_path=\"$user\", public, vectors" "-c" "logging_collector=on" "-c" "max_wal_size=2GB" "-c" "shared_buffers=512MB" "-c" "wal_compression=on" ];
log-driver = "journald";
extraOptions = [
"--network-alias=database"
"--network=immich_default"
];
};
"immich_redis" = {
image = "registry.hub.docker.com/library/redis:6.2-alpine";
environmentFiles = [
"/var/secrets/immich.env"
];
log-driver = "journald";
extraOptions = [
"--network-alias=redis"
"--network=immich_default"
];
};
"immich_server" = {
image = "ghcr.io/immich-app/immich-server:${cfg.version}";
environmentFiles = [
"/var/secrets/immich.env"
];
volumes = [
"/etc/localtime:/etc/localtime:ro"
"${cfg.dataDir}:/usr/src/app/upload:rw"
] ++ map (dir: "${dir}:${dir}:ro") cfg.storageDirs;
ports = [
"${toString cfg.port}:2283/tcp"
];
dependsOn = [
"immich_postgres"
"immich_redis"
];
log-driver = "journald";
extraOptions = [
"--network-alias=immich-server"
"--network=immich_default"
];
};
};
systemd.services = (containerServices [
{ name = "machine_learning"; volumes = [ "model_cache" ]; }
{ name = "postgres"; volumes = [ "db_data" ]; }
{ name = "redis"; }
{ name = "server"; }
]) // {
# Networks
"podman-network-immich_default" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "${pkgs.podman}/bin/podman network rm -f immich_default";
};
script = ''
podman network inspect immich_default || podman network create immich_default
'';
partOf = [ "podman-compose-immich-root.target" ];
wantedBy = [ "podman-compose-immich-root.target" ];
};
} // (volumeServices [
"db_data"
"model_cache"
]);
# 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-immich-root" = {
unitConfig = {
Description = "Root target generated by compose2nix.";
};
wantedBy = [ "multi-user.target" ];
};
modules.services.webserver = {
enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
};
};
}

View File

@@ -0,0 +1,41 @@
{ config, lib, ... }:
let
cfg = config.modules.services.invidious;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
in
{
options.modules.services.invidious = {
enable = lib.mkEnableOption "Enable Invidious";
subdomain = lib.mkOption {
type = lib.types.str;
};
port = lib.mkOption {
type = lib.types.int;
default = 3000;
};
};
config = lib.mkIf cfg.enable {
services.invidious = {
enable = true;
domain = fqdn;
address = "127.0.0.1";
inherit (cfg) port;
settings = {
external_port = 443;
db = {
dbname = "invidious";
user = "invidious";
};
};
};
services.postgresql.enable = lib.mkDefault true;
modules.services.webserver = {
enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
};
};
}

View File

@@ -0,0 +1,26 @@
{ lib, config, ... }:
let
cfg = config.modules.services.owncast;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
in
{
options.modules.services.owncast = {
enable = lib.mkEnableOption "Enable Owncast";
subdomain = lib.mkOption {
type = lib.types.str;
};
port = lib.mkOption {
type = lib.types.int;
default = 8080;
};
};
config = lib.mkIf cfg.enable {
services.owncast = {
enable = true;
openFirewall = true;
inherit (cfg) port;
};
modules.services.webserver.vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
};
}

View File

@@ -0,0 +1,35 @@
{ config, lib, ... }:
let
cfg = config.modules.services.syncthing;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
in
{
options.modules.services.syncthing = {
enable = lib.mkEnableOption "Enable Syncthing";
dataDir = lib.mkOption {
type = lib.types.str;
};
subdomain = lib.mkOption {
type = lib.types.str;
};
port = lib.mkOption {
type = lib.types.int;
default = 8384;
};
};
config = lib.mkIf cfg.enable {
services.syncthing = {
enable = true;
dataDir = cfg.dataDir;
openDefaultPorts = true;
guiAddress = "[::1]:${toString cfg.port}";
settings.gui.insecureSkipHostCheck = true;
};
modules.services.webserver = {
enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = cfg.port;
};
};
}

View File

@@ -0,0 +1,18 @@
{ lib, config, ... }:
let
cfg = config.modules.services.tailscale;
in
{
options = {
modules.services.tailscale = {
enable = lib.mkEnableOption "Enable Tailscale";
};
};
config = lib.mkIf cfg.enable {
services.tailscale = {
enable = true;
useRoutingFeatures = "server";
};
};
}

View File

@@ -0,0 +1,52 @@
{ lib, config, ... }:
let
cfg = config.modules.services.vaultwarden;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
port = config.services.vaultwarden.config.ROCKET_PORT;
in
{
options.modules.services.vaultwarden = {
enable = lib.mkEnableOption "Enable Vaultwarden";
subdomain = lib.mkOption {
type = lib.types.str;
};
secrets = lib.mkOption {
type = lib.types.str;
};
config = lib.mkOption {
type = lib.types.attrs;
default = { };
};
};
config = lib.mkIf cfg.enable {
services.vaultwarden = {
enable = true;
dbBackend = "postgresql";
environmentFile = "/var/secrets/vaultwarden.env";
config = {
DOMAIN = "https://${fqdn}";
DATABASE_URL = "postgres://%2Fvar%2Frun%2Fpostgresql/vaultwarden";
WEBSOCKET_ENABLED = true;
SIGNUPS_VERIFY = true;
PASSWORD_ITERATIONS = 600000;
ROCKET_LIMITS = "{json=10485760}";
ROCKET_PORT = 8000;
} // cfg.config;
};
modules.services.webserver = {
enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = port;
};
services.postgresql = {
enable = lib.mkDefault true;
ensureDatabases = [ "vaultwarden" ];
ensureUsers = [{
name = "vaultwarden";
ensureDBOwnership = true;
}];
};
};
}

View File

@@ -0,0 +1,94 @@
{ lib, config, ... }:
let
cfg = config.modules.services.webserver;
types = {
location = lib.types.submodule {
options = {
proxyPort = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
};
extraConfig = lib.mkOption {
type = lib.types.str;
default = "";
};
};
};
vhost = lib.types.submodule {
options = {
proxyBuffering = lib.mkOption {
type = lib.types.bool;
default = true;
};
locations = lib.mkOption {
type = lib.types.attrsOf types.location;
};
};
};
};
in
{
options.modules.services.webserver = {
enable = lib.mkEnableOption "Enable nginx";
acme = {
dnsChallenge = lib.mkEnableOption "Enable DNS challenge";
dnsProvider = lib.mkOption {
type = lib.types.str;
default = "hetzner";
};
environmentFile = lib.mkOption {
type = lib.types.str;
default = "/var/secrets/lego";
};
};
vHosts = lib.mkOption {
type = lib.types.attrsOf types.vhost;
default = { };
};
};
config = lib.mkIf cfg.enable {
services.nginx = {
enable = lib.mkDefault true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
recommendedBrotliSettings = true;
recommendedGzipSettings = true;
recommendedZstdSettings = true;
recommendedOptimisation = true;
virtualHosts = lib.mapAttrs
(_: { proxyBuffering, locations }: {
forceSSL = true;
enableACME = true;
http2 = true;
acmeRoot = lib.mkIf cfg.acme.dnsChallenge null;
extraConfig = lib.mkIf (!proxyBuffering) ''
proxy_buffering off;
'';
locations = lib.mapAttrs
(_: { proxyPort, extraConfig }: lib.mergeAttrsList [
{ inherit extraConfig; }
(if (lib.isInt proxyPort) then {
proxyWebsockets = true;
proxyPass = "http://localhost:${toString proxyPort}";
} else { })
])
locations;
})
cfg.vHosts;
};
security.acme = {
acceptTerms = true;
defaults = {
email = "admin@j.repomaa.com";
dnsProvider = lib.mkIf cfg.acme.dnsChallenge cfg.acme.dnsProvider;
};
};
modules.firewall.allInterfaces = [ "web" ];
};
}

View File

@@ -0,0 +1,45 @@
{ pkgs, inputs, config, lib, ... }:
let
cfg = config.modules.services.workout-sync;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
port = cfg.port;
workout-sync = inputs.workout-sync.packages.${pkgs.system}.default;
in
{
options.modules.services.workout-sync = {
enable = lib.mkEnableOption "Enable Workout Sync";
subdomain = lib.mkOption {
type = lib.types.str;
};
port = lib.mkOption {
type = lib.types.int;
default = 3344;
};
};
config = lib.mkIf cfg.enable {
modules.services.workout-tracker.enable = lib.mkDefault true;
systemd.services.workout-sync = {
enable = true;
description = "Workout sync service";
environment = {
PORT = toString port;
WORKOUT_TRACKER_URL = "http://localhost:${toString config.services.workout-tracker.port}";
};
serviceConfig = {
ExecStart = "${workout-sync}/bin/workout-sync";
Restart = "always";
DynamicUser = true;
EnvironmentFile = "/var/secrets/workout-sync.env";
};
wantedBy = [ "multi-user.target" ];
confinement.enable = true;
};
modules.services.webserver = {
enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = port;
};
};
}

View File

@@ -0,0 +1,34 @@
{ pkgs, config, lib, inputs, ... }:
let
cfg = config.modules.services.workout-tracker;
fqdn = "${cfg.subdomain}.${config.networking.domain}";
port = cfg.port;
in
{
options.modules.services.workout-tracker = {
enable = lib.mkEnableOption "Enable Workout Tracker";
version = lib.mkOption {
type = lib.types.str;
};
subdomain = lib.mkOption {
type = lib.types.str;
};
port = lib.mkOption {
type = lib.types.int;
default = 3322;
};
};
config = lib.mkIf cfg.enable {
services.workout-tracker = {
enable = true;
inherit port;
package = inputs.nixpkgs-unstable.legacyPackages.${pkgs.system}.workout-tracker;
};
modules.services.webserver = {
enable = lib.mkDefault true;
vHosts.${fqdn}.locations."/".proxyPort = port;
};
};
}