diff --git a/modules/default.nix b/modules/default.nix index e6a2234..0bd7deb 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -2,6 +2,7 @@ { imports = [ + ./global.nix ./services/default.nix ]; } \ No newline at end of file diff --git a/modules/global.nix b/modules/global.nix new file mode 100644 index 0000000..9909389 --- /dev/null +++ b/modules/global.nix @@ -0,0 +1,41 @@ +{ lib, ... }: + +with lib; + +{ + options.numbus = { + + services = { + domain = mkOption { + type = types.str; + example = "numbus.eu"; + description = "The root domain name (e.g., example.com) that your services will use"; + }; + dns = mkOption { + type = types.enum [ "pi-hole" "adguard" ]; + default = "pi-hole"; + example = "pi-hole"; + description = "The preferred DNS resolver service (pi-hole or adguard) that other services should depend on"; + }; + }; + traefikDynamicConfigDir = mkOption { + type = types.str; + default = "/etc/traefik/rules"; + example = "/etc/traefik/rules"; + description = "! Choose a directory outside of /etc/ will break things ! The directory where Traefik's dynamic configuration files will be stored"; + }; + + email = { + administratorEmail = mkOption { + type = types.str; + example = "admin@your-domain.com"; + description = "The email that will be used to send critical notifications such as hardware failures, services errors, ACME updates, etc"; + }; + userEmail = mkOption { + type = types.str; + example = "user@your-domain.com"; + description = "The email that will be used by services to send notifications"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/services/frigate.nix b/modules/services/frigate.nix index ff82aa3..3cfc9b4 100644 --- a/modules/services/frigate.nix +++ b/modules/services/frigate.nix @@ -1,50 +1,86 @@ -{ config, pkgs, ... }: +{ config, pkgs, lib, ... }: + +with lib; let - container_name = "frigate"; - compose_file = "podman/frigate/compose.yaml"; - config_dir = "/mnt/config/frigate"; - data_dir = "/mnt/data/frigate"; + cfg = config.numbus.services.frigate; + containerName = "frigate"; + pod = "home-assistant"; + composeFile = "podman/frigate/compose.yaml"; in { - config = { - environment.etc."${compose_file}".text = + options.numbus.services.frigate = { + enable = mkEnableOption "Frigate fully-local NVR (Network Video Recorder)"; + + configDir = mkOption { + type = types.str; + default = "/mnt/config/frigate"; + example = "/mnt/config/frigate"; + description = "The directory where Frigate's configuration files will be stored"; + }; + + dataDir = mkOption { + type = types.str; + default = "/mnt/data/frigate"; + example = "/mnt/data/frigate"; + description = "The directory where Frigate's data (i.e. clips, recordings, exports) will be stored"; + }; + + subdomain = mkOption { + type = types.str; + default = "frigate"; + example = "frigate"; + description = "The subdomain that Frigate will use (i.e. your-subdomain.your-domain.com)"; + }; + + devices = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/dri:/dev/dri" "/dev/bus/usb:/dev/bus/usb" "/dev/apex_0:/dev/apex_0" ]; + description = "List of devices to map into the container. /dev/dri is used for graphics acceleration, /dev/bus/usb for USB Coral TPUs, and /dev/apex_0 for PCI coral TPUs"; + }; + + port = mkOption { + type = types.str; + default = "8971"; + example = "8971"; + description = "The port that Frigate will use. Be careful, do not use a port already in use such as 80 or 443"; + }; + }; + + config = mkIf cfg.enable { + environment.etc."${composeFile}".text = /* yaml */ '' services: - frigate: + ${containerName}: image: ghcr.io/blakeblackshear/frigate:stable - container_name: frigate - shm_size: "512MB" + container_name: ${containerName} + hostname: ${containerName} + shm_size: "256mb" networks: home-assistant_frontend: home-assistant_backend: + ports: + - "${cfg.port}:8971/tcp" volumes: - - ${config_dir}:/config - - ${data_dir}/clips:/media/frigate/clips - - ${data_dir}/recordings:/media/frigate/recordings - - ${data_dir}/exports:/media/frigate/exports + - ${cfg.configDir}:/config + - ${cfg.dataDir}:/media/frigate - /etc/localtime:/etc/localtime:ro - type: tmpfs target: /tmp/cache tmpfs: - size: 2000000000 + size: 1000000000 environment: - FRIGATE_MQTT_USER: $FRIGATE_MQTT_USER - FRIGATE_MQTT_PASSWORD: $FRIGATE_MQTT_PASSWORD - # --- frigate devices --- # - labels: - - traefik.enable=true - - traefik.docker.network=home-assistant_frontend - - traefik.http.services.frigate.loadbalancer.server.port=8971 - - traefik.http.services.frigate.loadbalancer.server.scheme=http - - traefik.http.routers.frigate-https.entrypoints=websecure - - traefik.http.routers.frigate-https.rule=Host(`frigate.$DOMAIN_NAME`) - - traefik.http.routers.frigate-https.tls=true - - traefik.http.routers.frigate-https.tls.certresolver=cloudflare + - FRIGATE_MQTT_USER=$FRIGATE_MQTT_USER + - FRIGATE_MQTT_PASSWORD=$FRIGATE_MQTT_PASSWORD +${lib.optionalString (cfg.devices != []) '' + devices: +${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)} +''} restart: unless-stopped networks: @@ -54,43 +90,65 @@ in external: true ''; - systemd.services."${container_name}" = { - description = "Podman container : ${container_name}"; - after = [ "traefik.service" "home-assistant.service" "pi-hole.service" ]; - requires = [ "traefik.service" "home-assistant.service" ]; - wantedBy = [ "multi-user.target" ]; - path = [ pkgs.podman pkgs.coreutils ]; + environment.etc."${config.numbus.services.traefikDynamicConfigDir}/frigate.yaml".text = + /* + yaml + */ + '' + http: + routers: + ${containerName}: + rule: "Host(`${cfg.subdomain}.${config.numbus.services.domain}`)" + entrypoints: + - "websecure" + service: ${containerName} + middlewares: + - secureHeaders + tls: + certresolver: "cloudflare" + options: "secureTLS" + services: + ${containerName}: + loadBalancer: + servers: + - url: "https://host.containers.internal:${cfg.port}" + ''; + systemd.services."${containerName}" = { + description = "Podman container : ${containerName}"; + requires = [ "traefik.service" "home-assistant.service" "${config.numbus.services.dns}.service" ]; + after = [ "traefik.service" "home-assistant.service" "${config.numbus.services.dns}.service" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.podman pkgs.podman-compose pkgs.coreutils pkgs.sudo ]; serviceConfig = { - User = "numbus-admin"; - Environment = [ "XDG_RUNTIME_DIR=/run/user/1000" ]; Type = "exec"; - TimeoutStartSec = "600"; - ExecStartPre = [ - "${pkgs.bash}/bin/bash -c 'sleep $((RANDOM % 180))'" - "-${pkgs.podman-compose}/bin/podman-compose -f /etc/${compose_file} pull" - ]; - ExecStart = "${pkgs.podman-compose}/bin/podman-compose -f /etc/${compose_file} up --remove-orphans"; - ExecStop = "${pkgs.podman-compose}/bin/podman-compose -f /etc/${compose_file} down"; + ExecStartPre = "bash -c 'sleep $((RANDOM % 180))'"; + ExecStart = "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} up --remove-orphans"; + ExecStop = "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} down"; Restart = "on-failure"; - RestartSec = "5m"; - StartLimitBurst = "3"; + RestartSec = "1m"; + StartLimitBurst = "5"; }; }; - systemd.services."update-${container_name}" = { - description = "Update ${container_name} container"; + systemd.services."update-${containerName}" = { + description = "Update ${containerName} container"; + path = [ pkgs.podman pkgs.podman-compose pkgs.sudo ]; serviceConfig = { Type = "oneshot"; - ExecStart = "${pkgs.systemd}/bin/systemctl restart ${container_name}.service"; + ExecStart = [ + "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} pull" + "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} down" + "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} up -d" + ]; }; }; - systemd.timers."update-${container_name}" = { + systemd.timers."update-${containerName}" = { timerConfig = { OnCalendar = "02:00"; RandomizedDelaySec = "60m"; - Unit = "update-${container_name}.service"; + Unit = "update-${containerName}.service"; }; wantedBy = [ "timers.target" ]; }; diff --git a/modules/services/gitea.nix b/modules/services/gitea.nix index 0b21a15..92790b8 100644 --- a/modules/services/gitea.nix +++ b/modules/services/gitea.nix @@ -1,8 +1,10 @@ { config, pkgs, ... }: let - container_name = "gitea"; - compose_file = "podman/gitea/compose.yaml"; + containerName = "gitea"; + composeFile = "podman/gitea/compose.yaml"; + configDir = "/mnt/config/gitea"; + dataDir = "gitea_data"; in { @@ -13,18 +15,20 @@ in */ '' services: - gitea: - image: docker.io/gitea/gitea:latest - container_name: gitea + gitea-server: + image: docker.gitea.com/gitea:latest-rootless + container_name: gitea-server + hostname: gitea-server networks: gitea_frontend: gitea_backend: + ports: + - "3000:3000/tcp" #http volumes: - - gitea_data:/data + - gitea_data:/var/lib/gitea + - ${configDir}:/etc/gitea - /etc/localtime:/etc/localtime:ro environment: - - USER_UID=1000 - - USER_GID=1000 - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=$POSTGRES_HOST:$POSTGRES_PORT - GITEA__database__NAME=$DB_NAME @@ -32,30 +36,22 @@ in - GITEA__database__PASSWD=$DB_PASSWORD - GITEA__server__SSH_PORT=2424 - GITEA__server__ROOT_URL=gitea.$DOMAIN_NAME - labels: - - traefik.enable=true - - traefik.docker.network=gitea_frontend - - traefik.http.services.gitea.loadbalancer.server.port=3000 - - traefik.http.services.gitea.loadbalancer.server.scheme=http - - traefik.http.routers.gitea-https.entrypoints=websecure - - traefik.http.routers.gitea-https.rule=Host(`gitea.$DOMAIN_NAME`) - - traefik.http.routers.gitea-https.tls=true - - traefik.http.routers.gitea-https.tls.certresolver=cloudflare depends_on: - gitea-database restart: unless-stopped gitea-database: - image: docker.io/library/postgres:17.5 + image: docker.io/library/postgres:14 container_name: gitea-database - environment: - - POSTGRES_USER=$DB_USERNAME - - POSTGRES_PASSWORD=$DB_PASSWORD - - POSTGRES_DB=$DB_NAME + hostname: gitea-database networks: gitea_backend: volumes: - gitea_database:/var/lib/postgresql/data + environment: + - POSTGRES_USER=$DB_USERNAME + - POSTGRES_PASSWORD=$DB_PASSWORD + - POSTGRES_DB=$DB_NAME restart: unless-stopped volumes: @@ -64,9 +60,19 @@ in networks: gitea_frontend: - external: true + name: gitea_frontend + driver: bridge + ipam: + config: + - subnet: "10.89.3.0/24" + gateway: "10.89.3.254" gitea_backend: - external: true + name: gitea_backend + driver: bridge + ipam: + config: + - subnet: "10.89.4.0/24" + gateway: "10.89.4.254" ''; systemd.services."${container_name}" = { diff --git a/modules/services/traefik.nix b/modules/services/traefik.nix index e8594f6..57fe286 100644 --- a/modules/services/traefik.nix +++ b/modules/services/traefik.nix @@ -1,14 +1,54 @@ { config, pkgs, ... }: +with lib; + let - container_name = "traefik"; - compose_file = "podman/traefik/compose.yaml"; - config_dir = "/mnt/config/traefik"; + cfg = config.numbus.services.traefik; + containerName = "traefik"; + pod = "false"; + composeFile = "podman/traefik/compose.yaml"; in { - config = { - environment.etc."${compose_file}".text = + options.numbus.services.traefik = { + enable = mkOption { + type = lib.types.bool; + default = true; + example = true; + description = "Traefik reverse-proxy"; + }; + + staticConfigFile = mkOption { + type = types.str; + default = "traefik/config.yaml"; + example = "traefik/config.yaml"; + description = "The directory path where Traefik's static configuration file will be stored, prefixed by /etc/"; + }; + + dataDir = mkOption { + type = types.str; + default = "/mnt/config/traefik"; + example = "/mnt/config/traefik"; + description = "The directory where traefik's data (i.e. SSL certificates) will be stored"; + }; + + subdomain = mkOption { + type = types.str; + default = "traefik"; + example = "traefik"; + description = "The subdomain that traefik will use (i.e. your-subdomain.your-domain.com)"; + }; + + logLevel = mkOption { + type = types.enum [ "TRACE" "DEBUG" "INFO" "WARN" "ERROR" "FATAL" ]; + default = "ERROR"; + example = "ERROR"; + description = "The level of detail Traefik should print in the logs : TRACE, DEBUG, INFO, WARN, ERROR, FATAL (from most to least verbose)"; + }; + }; + + config = mkIf cfg.enable { + environment.etc."${composeFile}".text = /* yaml */ @@ -17,48 +57,155 @@ in traefik: image: docker.io/library/traefik:latest container_name: traefik - networks: -TRAEFIK_NETWORKS + hostname: traefik + network_mode: pasta ports: - - "80:80" - - "443:443" + - "80:80/tcp" + - "443:443/tcp" volumes: - /run/user/1000/podman/podman.sock:/run/docker.sock:ro - - ${config_dir}/rules/:/etc/traefik/conf/:ro - - ${config_dir}/traefik.yaml:/etc/traefik/traefik.yaml:ro - - ${config_dir}/certs/:/var/traefik/certs/:rw + - ${cfg.staticConfigFile}:/etc/traefik/traefik.yaml:ro + - ${config.numbus.services.traefikDynamicConfigDir}:/etc/traefik/conf:ro + - ${cfg.dataDir}:/var/traefik/certs:rw environment: - CF_DNS_API_TOKEN=$CF_DNS_API_TOKEN - labels: - - traefik.enable=true - - traefik.http.services.traefik.loadbalancer.server.port=8080 - - traefik.http.services.traefik.loadbalancer.server.scheme=http - - traefik.http.routers.traefik-https.entrypoints=websecure - - traefik.http.routers.traefik-https.rule=Host(`traefik.$DOMAIN_NAME`) - - traefik.http.routers.traefik-https.tls=true - - traefik.http.routers.traefik-https.tls.certresolver=cloudflare - restart: always - networks: -TRAEFIK_REF_NETWORKS + security_opt: + - no-new-privileges:true + restart: unless-stopped ''; - systemd.services.traefik = { - description = "Podman container : ${container_name}"; - after = [ "numbus-activation.service" ]; - wantedBy = [ "multi-user.target" ]; - path = [ pkgs.podman pkgs.coreutils ]; + environment.etc."${cfg.staticConfigFile}".text = + /* + yaml + */ + '' + global: + checkNewVersion: false + sendAnonymousUsage: false + log: + level: ${cfg.logLevel} + accesslog: {} + api: + dashboard: false + insecure: false + entryPoints: + web: + address: :80 + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + address: :443 + forwardedHeaders: + trustedIPs: + - "127.0.0.1/32" + - "10.0.0.0/8" + - "192.168.0.0/16" + - "172.16.0.0/12" + certificatesResolvers: + cloudflare: + acme: + email: ${config.numbus.email.administratorEmail} + storage: /var/traefik/certs/cloudflare-acme.json + caServer: "https://acme-v02.api.letsencrypt.org/directory" + dnsChallenge: + provider: cloudflare + resolvers: + - "1.1.1.1:53" + - "9.9.9.9:53" + serversTransport: + insecureSkipVerify: true + providers: + file: + directory: "/etc/traefik/conf/" + watch: true + ''; + environment.etc."${config.numbus.services.traefikDynamicConfigDir}/secureHeaders.yaml".text = + /* + yaml + */ + '' + http: + middlewares: + secureHeaders: + headers: + FrameDeny: true + AccessControlAllowMethods: 'GET,OPTIONS,PUT' + AccessControlAllowOriginList: + - origin-list-or-null + AccessControlMaxAge: 100 + AddVaryHeader: true + BrowserXssFilter: true + ContentTypeNosniff: true + ForceSTSHeader: true + STSIncludeSubdomains: true + STSPreload: true + ContentSecurityPolicy: default-src 'self' 'unsafe-inline' + CustomFrameOptionsValue: SAMEORIGIN + ReferrerPolicy: same-origin + PermissionsPolicy: vibrate 'self' + STSSeconds: 315360000 + ''; + + environment.etc."${config.numbus.services.traefikDynamicConfigDir}/secureTLS.yaml".text = + /* + yaml + */ + '' + tls: + options: + secureTLS: + minVersion: VersionTLS12 + sniStrict: true + curvePreferences: + - CurveP521 + - CurveP384 + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + ''; + + systemd.services."${containerName}" = { + description = "Podman container : ${containerName}"; + requires = [ "network.target" ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.podman pkgs.podman-compose pkgs.coreutils pkgs.sudo ]; serviceConfig = { - User = "numbus-admin"; - Environment = [ "XDG_RUNTIME_DIR=/run/user/1000" ]; Type = "exec"; - ExecStartPre = "${pkgs.podman-compose}/bin/podman-compose -f /etc/${compose_file} pull"; - ExecStart = "${pkgs.podman-compose}/bin/podman-compose -f /etc/${compose_file} up --remove-orphans"; - ExecStop = "${pkgs.podman-compose}/bin/podman-compose -f /etc/${compose_file} down"; + ExecStartPre = "bash -c 'sleep $((RANDOM % 180))'"; + ExecStart = "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} up --remove-orphans"; + ExecStop = "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} down"; Restart = "on-failure"; - RestartSec = "5m"; - StartLimitBurst = "3"; + RestartSec = "1m"; + StartLimitBurst = "5"; }; }; + + systemd.services."update-${containerName}" = { + description = "Update ${containerName} container"; + path = [ pkgs.podman pkgs.podman-compose pkgs.sudo ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = [ + "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} pull" + "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} down" + "sudo -u numbus-admin podman-compose --in-pod ${pod} -f /etc/${composeFile} up -d" + ]; + }; + }; + + systemd.timers."update-${containerName}" = { + timerConfig = { + OnCalendar = "02:00"; + RandomizedDelaySec = "60m"; + Unit = "update-${containerName}.service"; + }; + wantedBy = [ "timers.target" ]; + }; }; } \ No newline at end of file