diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index a71c804428d..de39be883c0 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -865,6 +865,7 @@ ./services/web-apps/atlassian/confluence.nix ./services/web-apps/atlassian/crowd.nix ./services/web-apps/atlassian/jira.nix + ./services/web-apps/bookstack.nix ./services/web-apps/convos.nix ./services/web-apps/cryptpad.nix ./services/web-apps/documize.nix diff --git a/nixos/modules/services/web-apps/bookstack.nix b/nixos/modules/services/web-apps/bookstack.nix new file mode 100644 index 00000000000..83d05ffbad9 --- /dev/null +++ b/nixos/modules/services/web-apps/bookstack.nix @@ -0,0 +1,365 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.bookstack; + bookstack = pkgs.bookstack.override { + dataDir = cfg.dataDir; + }; + db = cfg.database; + mail = cfg.mail; + + user = cfg.user; + group = cfg.group; + + # shell script for local administration + artisan = pkgs.writeScriptBin "bookstack" '' + #! ${pkgs.runtimeShell} + cd ${bookstack} + sudo=exec + if [[ "$USER" != ${user} ]]; then + sudo='exec /run/wrappers/bin/sudo -u ${user}' + fi + $sudo ${pkgs.php}/bin/php artisan $* + ''; + + +in { + options.services.bookstack = { + + enable = mkEnableOption "BookStack"; + + user = mkOption { + default = "bookstack"; + description = "User bookstack runs as."; + type = types.str; + }; + + group = mkOption { + default = "bookstack"; + description = "Group bookstack runs as."; + type = types.str; + }; + + appKeyFile = mkOption { + description = '' + A file containing the AppKey. + Used for encryption where needed. Can be generated with head -c 32 /dev/urandom| base64 and must be prefixed with base64:. + ''; + example = "/run/keys/bookstack-appkey"; + type = types.path; + }; + + appURL = mkOption { + description = '' + The root URL that you want to host BookStack on. All URLs in BookStack will be generated using this value. + If you change this in the future you may need to run a command to update stored URLs in the database. Command example: php artisan bookstack:update-url https://old.example.com https://new.example.com + ''; + example = "https://example.com"; + type = types.str; + }; + + cacheDir = mkOption { + description = "BookStack cache directory"; + default = "/var/cache/bookstack"; + type = types.path; + }; + + dataDir = mkOption { + description = "BookStack data directory"; + default = "/var/lib/bookstack"; + type = types.path; + }; + + database = { + host = mkOption { + type = types.str; + default = "localhost"; + description = "Database host address."; + }; + port = mkOption { + type = types.port; + default = 3306; + description = "Database host port."; + }; + name = mkOption { + type = types.str; + default = "bookstack"; + description = "Database name."; + }; + user = mkOption { + type = types.str; + default = user; + defaultText = "\${user}"; + description = "Database username."; + }; + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/bookstack-dbpassword"; + description = '' + A file containing the password corresponding to + . + ''; + }; + createLocally = mkOption { + type = types.bool; + default = false; + description = "Create the database and database user locally."; + }; + }; + + mail = { + driver = mkOption { + type = types.enum [ "smtp" "sendmail" ]; + default = "smtp"; + description = "Mail driver to use."; + }; + host = mkOption { + type = types.str; + default = "localhost"; + description = "Mail host address."; + }; + port = mkOption { + type = types.port; + default = 1025; + description = "Mail host port."; + }; + fromName = mkOption { + type = types.str; + default = "BookStack"; + description = "Mail \"from\" name."; + }; + from = mkOption { + type = types.str; + default = "mail@bookstackapp.com"; + description = "Mail \"from\" email."; + }; + user = mkOption { + type = with types; nullOr str; + default = null; + example = "bookstack"; + description = "Mail username."; + }; + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/bookstack-mailpassword"; + description = '' + A file containing the password corresponding to + . + ''; + }; + encryption = mkOption { + type = with types; nullOr (enum [ "tls" ]); + default = null; + description = "SMTP encryption mechanism to use."; + }; + }; + + maxUploadSize = mkOption { + type = types.str; + default = "18M"; + example = "1G"; + description = "The maximum size for uploads (e.g. images)."; + }; + + poolConfig = mkOption { + type = with types; attrsOf (oneOf [ str int bool ]); + default = { + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 500; + }; + description = '' + Options for the bookstack PHP pool. See the documentation on php-fpm.conf + for details on configuration directives. + ''; + }; + + nginx = mkOption { + type = types.submodule ( + recursiveUpdate + (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {} + ); + default = {}; + example = { + serverAliases = [ + "bookstack.\${config.networking.domain}" + ]; + # To enable encryption and let let's encrypt take care of certificate + forceSSL = true; + enableACME = true; + }; + description = '' + With this option, you can customize the nginx virtualHost settings. + ''; + }; + + extraConfig = mkOption { + type = types.nullOr types.lines; + default = null; + example = '' + ALLOWED_IFRAME_HOSTS="https://example.com" + WKHTMLTOPDF=/home/user/bins/wkhtmltopdf + ''; + description = '' + Lines to be appended verbatim to the BookStack configuration. + Refer to for details on supported values. + ''; + }; + + }; + + config = mkIf cfg.enable { + + assertions = [ + { assertion = db.createLocally -> db.user == user; + message = "services.bookstack.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true."; + } + { assertion = db.createLocally -> db.passwordFile == null; + message = "services.bookstack.database.passwordFile cannot be specified if services.bookstack.database.createLocally is set to true."; + } + ]; + + environment.systemPackages = [ artisan ]; + + services.mysql = mkIf db.createLocally { + enable = true; + package = mkDefault pkgs.mariadb; + ensureDatabases = [ db.name ]; + ensureUsers = [ + { name = db.user; + ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; }; + } + ]; + }; + + services.phpfpm.pools.bookstack = { + inherit user; + inherit group; + phpOptions = '' + log_errors = on + post_max_size = ${cfg.maxUploadSize} + upload_max_filesize = ${cfg.maxUploadSize} + ''; + settings = { + "listen.mode" = "0660"; + "listen.owner" = user; + "listen.group" = group; + } // cfg.poolConfig; + }; + + services.nginx = { + enable = mkDefault true; + virtualHosts.bookstack = mkMerge [ cfg.nginx { + root = mkForce "${bookstack}/public"; + extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"; + locations = { + "/" = { + index = "index.php"; + extraConfig = ''try_files $uri $uri/ /index.php?$query_string;''; + }; + "~ \.php$" = { + extraConfig = '' + try_files $uri $uri/ /index.php?$query_string; + include ${pkgs.nginx}/conf/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_pass unix:${config.services.phpfpm.pools."bookstack".socket}; + ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"} + ''; + }; + "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = { + extraConfig = "expires 365d;"; + }; + }; + }]; + }; + + systemd.services.bookstack-setup = { + description = "Preperation tasks for BookStack"; + before = [ "phpfpm-bookstack.service" ]; + after = optional db.createLocally "mysql.service"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + User = user; + WorkingDirectory = "${bookstack}"; + }; + script = '' + # create .env file + echo " + APP_KEY=base64:$(head -n1 ${cfg.appKeyFile}) + APP_URL=${cfg.appURL} + DB_HOST=${db.host} + DB_PORT=${toString db.port} + DB_DATABASE=${db.name} + DB_USERNAME=${db.user} + MAIL_DRIVER=${mail.driver} + MAIL_FROM_NAME=\"${mail.fromName}\" + MAIL_FROM=${mail.from} + MAIL_HOST=${mail.host} + MAIL_PORT=${toString mail.port} + ${optionalString (mail.user != null) "MAIL_USERNAME=${mail.user};"} + ${optionalString (mail.encryption != null) "MAIL_ENCRYPTION=${mail.encryption};"} + ${optionalString (db.passwordFile != null) "DB_PASSWORD=$(head -n1 ${db.passwordFile})"} + ${optionalString (mail.passwordFile != null) "MAIL_PASSWORD=$(head -n1 ${mail.passwordFile})"} + APP_SERVICES_CACHE=${cfg.cacheDir}/services.php + APP_PACKAGES_CACHE=${cfg.cacheDir}/packages.php + APP_CONFIG_CACHE=${cfg.cacheDir}/config.php + APP_ROUTES_CACHE=${cfg.cacheDir}/routes-v7.php + APP_EVENTS_CACHE=${cfg.cacheDir}/events.php + ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "SESSION_SECURE_COOKIE=true"} + ${toString cfg.extraConfig} + " > "${cfg.dataDir}/.env" + # set permissions + chmod 700 "${cfg.dataDir}/.env" + + # migrate db + ${pkgs.php}/bin/php artisan migrate --force + + # create caches + ${pkgs.php}/bin/php artisan config:cache + ${pkgs.php}/bin/php artisan route:cache + ${pkgs.php}/bin/php artisan view:cache + ''; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.cacheDir} 0700 ${user} ${group} - -" + "d ${cfg.dataDir} 0710 ${user} ${group} - -" + "d ${cfg.dataDir}/public 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -" + ]; + + users = { + users = mkIf (user == "bookstack") { + bookstack = { + inherit group; + isSystemUser = true; + }; + "${config.services.nginx.user}".extraGroups = [ group ]; + }; + groups = mkIf (group == "bookstack") { + bookstack = {}; + }; + }; + + }; + + meta.maintainers = with maintainers; [ ymarkus ]; +}