{ config, lib, pkgs, ... }: let cfg = config.services.reudy-panel; forgejoServer = config.services.forgejo.settings.server; routesFile = "${cfg.baseDir}/routes/routes.caddy"; in { options.services.reudy-panel = { enable = lib.mkEnableOption "the panel for deploying Podman compose apps behind Caddy"; package = lib.mkOption { type = lib.types.package; default = pkgs.callPackage ./package.nix { }; defaultText = lib.literalExpression "pkgs.callPackage ./package.nix { }"; description = "The panel package to use."; }; user = lib.mkOption { type = lib.types.str; default = "reudy"; description = '' Existing user the panel runs as. Apps are deployed as rootless Podman containers of this user, so it should have lingering enabled. ''; }; group = lib.mkOption { type = lib.types.str; default = "panelroutes"; description = "Group shared by the panel and Caddy for the generated routes file."; }; baseDir = lib.mkOption { type = lib.types.path; default = "/var/lib/containers"; description = "Where stacks, volumes, routes, state and backups are kept."; }; listenAddress = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; description = "Address the panel API listens on."; }; port = lib.mkOption { type = lib.types.port; default = 9911; description = "Port the panel API listens on."; }; domain = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "panel.example.com"; description = "Serve the panel on this domain through Caddy. Null disables the virtual host."; }; autheliaAddress = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "127.0.0.1:9091"; description = '' Authelia instance that protects the panel's virtual host with forward_auth. The panel has no login of its own, so leave this null only if something else guards it. ''; }; forgejo.enable = lib.mkOption { type = lib.types.bool; default = config.services.forgejo.enable; defaultText = lib.literalExpression "config.services.forgejo.enable"; description = '' Point the panel at the local Forgejo instance (repo picker, private clones, commit links). URLs are taken from services.forgejo.settings. ''; }; environment = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { }; description = "Extra environment variables for the panel API service."; }; }; config = lib.mkIf cfg.enable { environment.systemPackages = [ cfg.package ]; users.groups.${cfg.group} = { }; users.users.${cfg.user}.extraGroups = [ cfg.group ]; users.users.caddy.extraGroups = lib.mkIf config.services.caddy.enable [ cfg.group ]; systemd.tmpfiles.rules = map (dir: "d ${cfg.baseDir}${dir} 0750 ${cfg.user} ${cfg.group} -") [ "" "/stacks" "/volumes" "/routes" "/state" "/state/apps" "/backups" ] ++ [ "f ${routesFile} 0640 ${cfg.user} ${cfg.group} -" ]; systemd.services.panel-api = { description = "Panel API"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; Restart = "always"; RestartSec = 3; WorkingDirectory = cfg.baseDir; ExecStart = lib.getExe cfg.package; }; environment = { PANEL_API_BIND = cfg.listenAddress; PANEL_API_PORT = toString cfg.port; PANEL_BASE_DIR = toString cfg.baseDir; PANEL_USER = cfg.user; PANEL_GROUP = cfg.group; PANELCTL_PATH = lib.getExe' cfg.package "panelctl"; } // lib.optionalAttrs cfg.forgejo.enable { # The API is reached on localhost; clones use the public URLs. PANEL_FORGEJO_URL = lib.removeSuffix "/" forgejoServer.ROOT_URL; PANEL_FORGEJO_API_URL = "http://${forgejoServer.HTTP_ADDR}:${toString forgejoServer.HTTP_PORT}"; PANEL_FORGEJO_SSH_URL = "ssh://${ forgejoServer.BUILTIN_SSH_SERVER_USER or config.services.forgejo.user }@${forgejoServer.DOMAIN}:${toString forgejoServer.SSH_PORT}"; } // cfg.environment; }; services.caddy = lib.mkIf config.services.caddy.enable { # App routes generated by panelctl. extraConfig = '' import ${routesFile} ''; virtualHosts = lib.mkIf (cfg.domain != null) { ${cfg.domain}.extraConfig = lib.optionalString (cfg.autheliaAddress != null) '' forward_auth ${cfg.autheliaAddress} { uri /api/authz/forward-auth copy_headers Remote-User Remote-Groups Remote-Email Remote-Name } '' + '' reverse_proxy ${cfg.listenAddress}:${toString cfg.port} ''; }; }; # Reload Caddy whenever panelctl rewrites the routes file. systemd.paths.caddy-routes-reload = lib.mkIf config.services.caddy.enable { wantedBy = [ "multi-user.target" ]; pathConfig.PathChanged = routesFile; }; systemd.services.caddy-routes-reload = lib.mkIf config.services.caddy.enable { serviceConfig = { Type = "oneshot"; ExecStart = "${config.systemd.package}/bin/systemctl reload caddy.service"; }; }; }; }