{ config, inputs, lib, pkgs, ... }: with lib; let cfg = config.modules.services.atticd; in { options.modules.services.atticd = { enable = mkEnableOption "Whether to enable atticd, a Multi-tenant Nix Binary Cache"; hosts = mkOption { type = types.listOf types.str; description = "List of FQDNs where attic is reachable at"; }; baseURL = mkOption { type = types.str; description = "URL to use as a API Endpoint"; }; storagePath = mkOption { type = types.path; description = "Path to the directory to store all files under"; }; watchStore = mkEnableOption "Whether to watch the Nix store for new paths"; secrets = { attic-credentials = mkOption { type = types.path; description = "Path to the acme environment file"; }; }; }; config = mkMerge [ (mkIf cfg.enable { services.atticd = { enable = true; environmentFile = cfg.secrets.attic-credentials; settings = { listen = "[::]:4005"; allowed-hosts = cfg.hosts; api-endpoint = cfg.baseURL; database.url = "postgres://atticd?host=/run/postgresql"; require-proof-of-possession = false; # Data chunking # # Warning: If you change any of the values here, it will be # difficult to reuse existing chunks for newly-uploaded NARs # since the cutpoints will be different. As a result, the # deduplication ratio will suffer for a while after the change. chunking = { # The minimum NAR size to trigger chunking # # IUUU, chunking is disabled entirely for newly-uploaded NARs. # If 1, all NARs are chunked. nar-size-threshold = 64 * 1024; # 64 KiB # The preferred minimum size of a chunk, in bytes min-size = 16 * 1024; # 16 KiB # The preferred average size of a chunk, in bytes avg-size = 64 * 1024; # 64 KiB # The preferred maximum size of a chunk, in bytes max-size = 256 * 1024; # 256 KiB }; storage = { type = "local"; path = cfg.storagePath; }; compression = { type = "zstd"; level = 9; }; garbage-collection = { interval = "12 hours"; default-retention-period = "4 weeks"; }; }; }; services.postgresql.ensureDatabases = [ "atticd" ]; services.postgresql.ensureUsers = [ { name = "atticd"; ensureDBOwnership = true; } ]; services.nginx.virtualHosts = builtins.listToAttrs (map (host: { name = host; value = { extraConfig = '' client_max_body_size 0; proxy_read_timeout 300s; proxy_send_timeout 300s; ''; }; }) cfg.hosts); }) (mkIf cfg.watchStore { systemd.services.atticd-watch-store = mkIf cfg.watchStore { wantedBy = [ "multi-user.target" ]; wants = [ "network-online.target" ]; serviceConfig = { DynamicUser = true; StateDirectory = "atticd-watch-store"; # NOTE: currently this expects `attic/config.toml` to be manually # generated and placed on `/var/lib/atticd-watch-store` Environment = "XDG_CONFIG_HOME=/var/lib/atticd-watch-store"; ExecStart = "${pkgs.attic-client}/bin/attic watch-store hydra"; Restart = "on-failure"; RestartSec = "5s"; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; ProtectSystem = "strict"; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; RestrictNamespaces = true; RestrictSUIDSGID = true; }; }; modules.persistence.directories = [ "/var/lib/private/atticd-watch-store" ]; }) ]; }