{ pkgs, lib, config, ... }: let cfg = config.services.lychee; src = cfg.package; envConf = cfg.settings; in { options.services.lychee = { enable = lib.mkEnableOption "Whether to enable lychee"; website = lib.mkOption { type = lib.types.str; default = "localhost"; example = "www.example.com"; }; package = lib.mkOption { type = lib.types.path; }; forceSSL = lib.mkOption { type = lib.types.bool; default = true; description = "Whether to force SSL for the nginx virtual host"; }; enableACME = lib.mkOption { type = lib.types.bool; default = true; description = "Whether to enableACME for the nginx virtual host"; }; upload_max_filesize = lib.mkOption { type = lib.types.ints.positive; default = 30; description = "Max uploaded file size"; }; post_max_size = lib.mkOption { type = lib.types.ints.positive; default = 100; description = "Max post request size"; }; user = lib.mkOption { type = lib.types.str; default = "lychee"; description = "The user that will operate on mutable files"; }; stateDirectory = lib.mkOption { type = lib.types.path; default = "/var/lib/lychee"; }; settings = lib.mkOption { default = {}; type = lib.types.submodule { freeformType = with lib.types; attrsOf str; options = { DB_DATABASE= lib.mkOption { type = lib.types.str; default = "${cfg.stateDirectory}/db.sqlite"; }; APP_NAME= lib.mkOption { type = lib.types.str; default = "Lychee"; }; APP_ENV = lib.mkOption { type = lib.types.str; default = "production"; }; APP_DEBUG = lib.mkOption { type = lib.types.str; default = "\"false\""; }; APP_URL = lib.mkOption { type = lib.types.str; default = "https://${cfg.website}"; }; DEBUGBAR_ENABLED = lib.mkOption { type = lib.types.str; default = "\"false\""; }; DB_CONNECTION = lib.mkOption { type = lib.types.str; default = "sqlite"; }; DB_LOG_SQL = lib.mkOption { type = lib.types.str; default = "\"false\""; }; LYCHEE_UPLOADS = lib.mkOption { type = lib.types.path; default = "${cfg.stateDirectory}/www/public/uploads"; }; CACHE_DRIVER = lib.mkOption { type = lib.types.str; default = "file"; }; SESSION_DRIVER = lib.mkOption { type = lib.types.str; default = "file"; }; SESSION_LIFETIME = lib.mkOption { type = lib.types.str; default = "120"; }; SECURITY_HEADER_HSTS_ENABLE = lib.mkOption { type = lib.types.str; default = "\"false\""; }; SESSION_SECURE_COOKIE = lib.mkOption { type = lib.types.str; default = "\"false\""; }; REDIS_PASSWORD = lib.mkOption { type = lib.types.str; default = "\"null\""; }; REDIS_PORT = lib.mkOption { type = lib.types.str; default = "6379"; }; MAIL_DRIVER = lib.mkOption { type = lib.types.str; default = "smtp"; }; TRUSTED_PROXIES = lib.mkOption { type = lib.types.str; default = "\"null\""; }; }; }; }; }; config = let srcDirsToBindMount = [ "app" "bootstrap" "config" "resources" "routes" "scripts" "vendor" ]; in lib.mkIf cfg.enable { services.nginx = { enable = true; virtualHosts.${cfg.website} = { root = cfg.stateDirectory + "/www/public/"; forceSSL = lib.mkDefault cfg.forceSSL; enableACME = lib.mkDefault cfg.enableACME; locations = { "^~ /index.php" = { fastcgiParams = { SCRIPT_FILENAME = "$document_root$fastcgi_script_name"; }; extraConfig = '' fastcgi_split_path_info ^(.+?\.php)(/.*)$; fastcgi_pass unix:${config.services.phpfpm.pools."${cfg.website}".socket}; fastcgi_index index.php; client_max_body_size ${builtins.toString cfg.upload_max_filesize}M; ''; }; "~ [^/]\.php(/|$)" = { return = "403"; }; "/uploads/" = { alias = cfg.settings.LYCHEE_UPLOADS; }; }; extraConfig = '' index index.php; if (!-e $request_filename) { rewrite ^/(.*)$ /index.php?/$1 last; break; } ''; }; }; systemd.tmpfiles.rules = let srcDirToTmpFile = dir: "d ${cfg.stateDirectory}/www/${dir} 0750 ${cfg.user} ${config.services.nginx.group}"; in [ "d ${cfg.stateDirectory} 0750 ${cfg.user} ${config.services.nginx.group}" "d ${cfg.stateDirectory}/www 0750 ${cfg.user} ${config.services.nginx.group}" "C ${cfg.stateDirectory}/public - ${cfg.user} ${config.services.nginx.group} - ${src}/public" "Z ${cfg.stateDirectory}/public 0750 ${cfg.user} ${config.services.nginx.group} - -" "C ${cfg.stateDirectory}/database - ${cfg.user} ${config.services.nginx.group} - ${src}/database" "Z ${cfg.stateDirectory}/database 0750 ${cfg.user} ${config.services.nginx.group} - -" "C ${cfg.stateDirectory}/bootstrap-cache - ${cfg.user} ${config.services.nginx.group} - ${src}/bootstrap/cache" "Z ${cfg.stateDirectory}/bootstrap-cache 0750 ${cfg.user} ${config.services.nginx.group} - -" "C ${cfg.stateDirectory}/storage - ${cfg.user} ${config.services.nginx.group} - ${src}/storage" "Z ${cfg.stateDirectory}/storage 0750 ${cfg.user} ${config.services.nginx.group} - -" "C ${cfg.settings.LYCHEE_UPLOADS} - ${cfg.user} ${config.services.nginx.group} - ${src}/public/uploads" "Z ${cfg.settings.LYCHEE_UPLOADS} 0750 ${cfg.user} ${config.services.nginx.group} - -" "f ${cfg.settings.DB_DATABASE} 0750 ${cfg.user} ${cfg.user}" "L ${cfg.stateDirectory}/www/artisan - - - - ${src}/artisan" "L ${cfg.stateDirectory}/www/composer.json - - - - ${src}/composer.json" "L ${cfg.stateDirectory}/www/composer.lock - - - - ${src}/composer.lock" "L ${cfg.stateDirectory}/www/version.md - - - - ${src}/version.md" "L ${cfg.stateDirectory}/www/simple_error_template.html - - - - ${src}/simple_error_template.html" ] ++ (builtins.map srcDirToTmpFile srcDirsToBindMount); systemd.mounts = let sourceDirToSystemdMount = dir: { before = [ "phpfpm-${cfg.website}.service" ]; wantedBy = [ "phpfpm-${cfg.website}.service" ]; what = "${src}/${dir}"; where = cfg.stateDirectory + "/www/${dir}"; options = "bind"; }; in [{ before = [ "phpfpm-${cfg.website}.service" ]; wantedBy = [ "phpfpm-${cfg.website}.service" ]; what = cfg.stateDirectory + "/storage"; where = cfg.stateDirectory + "/www/storage"; options = "bind"; }] ++ (builtins.map sourceDirToSystemdMount srcDirsToBindMount) ++ [{ before = [ "phpfpm-${cfg.website}.service" ]; wantedBy = [ "phpfpm-${cfg.website}.service" ]; what = cfg.stateDirectory + "/bootstrap-cache"; where = cfg.stateDirectory + "/www/bootstrap/cache"; options = "bind"; } { before = [ "phpfpm-${cfg.website}.service" ]; wantedBy = [ "phpfpm-${cfg.website}.service" ]; what = cfg.stateDirectory + "/database"; where = cfg.stateDirectory + "/www/database"; options = "bind"; } { before = [ "phpfpm-${cfg.website}.service" ]; wantedBy = [ "phpfpm-${cfg.website}.service" ]; what = cfg.stateDirectory + "/public"; where = cfg.stateDirectory + "/www/public"; options = "bind"; }]; services.phpfpm.pools.${cfg.website} = { user = cfg.user; phpPackage = pkgs.php81.withExtensions ({ enabled, all }: enabled ++ [ all.imagick all.bcmath all.mbstring all.gd]); phpOptions = '' upload_max_filesize = ${builtins.toString cfg.upload_max_filesize}M post_max_size = ${builtins.toString cfg.post_max_size}M ''; settings = { "pm" = "dynamic"; "pm.max_children" = 75; "pm.start_servers" = 10; "pm.min_spare_servers" = 5; "pm.max_spare_servers" = 20; "pm.max_requests" = 500; "listen.owner" = config.services.nginx.user; "listen.group" = config.services.nginx.group; }; phpEnv = { "PATH" = lib.makeBinPath [ pkgs.ffmpeg ]; } // envConf; }; users.users.${cfg.user} = { isSystemUser = true; home = src; group = cfg.user; }; users.groups.${cfg.user} = { }; networking.firewall.allowedTCPPorts = [ 80 443 ]; }; }