at 21.11-pre 12 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.bookstack; 7 bookstack = pkgs.bookstack.override { 8 dataDir = cfg.dataDir; 9 }; 10 db = cfg.database; 11 mail = cfg.mail; 12 13 user = cfg.user; 14 group = cfg.group; 15 16 # shell script for local administration 17 artisan = pkgs.writeScriptBin "bookstack" '' 18 #! ${pkgs.runtimeShell} 19 cd ${bookstack} 20 sudo=exec 21 if [[ "$USER" != ${user} ]]; then 22 sudo='exec /run/wrappers/bin/sudo -u ${user}' 23 fi 24 $sudo ${pkgs.php}/bin/php artisan $* 25 ''; 26 27 28in { 29 options.services.bookstack = { 30 31 enable = mkEnableOption "BookStack"; 32 33 user = mkOption { 34 default = "bookstack"; 35 description = "User bookstack runs as."; 36 type = types.str; 37 }; 38 39 group = mkOption { 40 default = "bookstack"; 41 description = "Group bookstack runs as."; 42 type = types.str; 43 }; 44 45 appKeyFile = mkOption { 46 description = '' 47 A file containing the AppKey. 48 Used for encryption where needed. Can be generated with <code>head -c 32 /dev/urandom| base64</code> and must be prefixed with <literal>base64:</literal>. 49 ''; 50 example = "/run/keys/bookstack-appkey"; 51 type = types.path; 52 }; 53 54 appURL = mkOption { 55 description = '' 56 The root URL that you want to host BookStack on. All URLs in BookStack will be generated using this value. 57 If you change this in the future you may need to run a command to update stored URLs in the database. Command example: <code>php artisan bookstack:update-url https://old.example.com https://new.example.com</code> 58 ''; 59 example = "https://example.com"; 60 type = types.str; 61 }; 62 63 cacheDir = mkOption { 64 description = "BookStack cache directory"; 65 default = "/var/cache/bookstack"; 66 type = types.path; 67 }; 68 69 dataDir = mkOption { 70 description = "BookStack data directory"; 71 default = "/var/lib/bookstack"; 72 type = types.path; 73 }; 74 75 database = { 76 host = mkOption { 77 type = types.str; 78 default = "localhost"; 79 description = "Database host address."; 80 }; 81 port = mkOption { 82 type = types.port; 83 default = 3306; 84 description = "Database host port."; 85 }; 86 name = mkOption { 87 type = types.str; 88 default = "bookstack"; 89 description = "Database name."; 90 }; 91 user = mkOption { 92 type = types.str; 93 default = user; 94 defaultText = "\${user}"; 95 description = "Database username."; 96 }; 97 passwordFile = mkOption { 98 type = with types; nullOr path; 99 default = null; 100 example = "/run/keys/bookstack-dbpassword"; 101 description = '' 102 A file containing the password corresponding to 103 <option>database.user</option>. 104 ''; 105 }; 106 createLocally = mkOption { 107 type = types.bool; 108 default = false; 109 description = "Create the database and database user locally."; 110 }; 111 }; 112 113 mail = { 114 driver = mkOption { 115 type = types.enum [ "smtp" "sendmail" ]; 116 default = "smtp"; 117 description = "Mail driver to use."; 118 }; 119 host = mkOption { 120 type = types.str; 121 default = "localhost"; 122 description = "Mail host address."; 123 }; 124 port = mkOption { 125 type = types.port; 126 default = 1025; 127 description = "Mail host port."; 128 }; 129 fromName = mkOption { 130 type = types.str; 131 default = "BookStack"; 132 description = "Mail \"from\" name."; 133 }; 134 from = mkOption { 135 type = types.str; 136 default = "mail@bookstackapp.com"; 137 description = "Mail \"from\" email."; 138 }; 139 user = mkOption { 140 type = with types; nullOr str; 141 default = null; 142 example = "bookstack"; 143 description = "Mail username."; 144 }; 145 passwordFile = mkOption { 146 type = with types; nullOr path; 147 default = null; 148 example = "/run/keys/bookstack-mailpassword"; 149 description = '' 150 A file containing the password corresponding to 151 <option>mail.user</option>. 152 ''; 153 }; 154 encryption = mkOption { 155 type = with types; nullOr (enum [ "tls" ]); 156 default = null; 157 description = "SMTP encryption mechanism to use."; 158 }; 159 }; 160 161 maxUploadSize = mkOption { 162 type = types.str; 163 default = "18M"; 164 example = "1G"; 165 description = "The maximum size for uploads (e.g. images)."; 166 }; 167 168 poolConfig = mkOption { 169 type = with types; attrsOf (oneOf [ str int bool ]); 170 default = { 171 "pm" = "dynamic"; 172 "pm.max_children" = 32; 173 "pm.start_servers" = 2; 174 "pm.min_spare_servers" = 2; 175 "pm.max_spare_servers" = 4; 176 "pm.max_requests" = 500; 177 }; 178 description = '' 179 Options for the bookstack PHP pool. See the documentation on <literal>php-fpm.conf</literal> 180 for details on configuration directives. 181 ''; 182 }; 183 184 nginx = mkOption { 185 type = types.submodule ( 186 recursiveUpdate 187 (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {} 188 ); 189 default = {}; 190 example = { 191 serverAliases = [ 192 "bookstack.\${config.networking.domain}" 193 ]; 194 # To enable encryption and let let's encrypt take care of certificate 195 forceSSL = true; 196 enableACME = true; 197 }; 198 description = '' 199 With this option, you can customize the nginx virtualHost settings. 200 ''; 201 }; 202 203 extraConfig = mkOption { 204 type = types.nullOr types.lines; 205 default = null; 206 example = '' 207 ALLOWED_IFRAME_HOSTS="https://example.com" 208 WKHTMLTOPDF=/home/user/bins/wkhtmltopdf 209 ''; 210 description = '' 211 Lines to be appended verbatim to the BookStack configuration. 212 Refer to <link xlink:href="https://www.bookstackapp.com/docs/"/> for details on supported values. 213 ''; 214 }; 215 216 }; 217 218 config = mkIf cfg.enable { 219 220 assertions = [ 221 { assertion = db.createLocally -> db.user == user; 222 message = "services.bookstack.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true."; 223 } 224 { assertion = db.createLocally -> db.passwordFile == null; 225 message = "services.bookstack.database.passwordFile cannot be specified if services.bookstack.database.createLocally is set to true."; 226 } 227 ]; 228 229 environment.systemPackages = [ artisan ]; 230 231 services.mysql = mkIf db.createLocally { 232 enable = true; 233 package = mkDefault pkgs.mariadb; 234 ensureDatabases = [ db.name ]; 235 ensureUsers = [ 236 { name = db.user; 237 ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; }; 238 } 239 ]; 240 }; 241 242 services.phpfpm.pools.bookstack = { 243 inherit user; 244 inherit group; 245 phpOptions = '' 246 log_errors = on 247 post_max_size = ${cfg.maxUploadSize} 248 upload_max_filesize = ${cfg.maxUploadSize} 249 ''; 250 settings = { 251 "listen.mode" = "0660"; 252 "listen.owner" = user; 253 "listen.group" = group; 254 } // cfg.poolConfig; 255 }; 256 257 services.nginx = { 258 enable = mkDefault true; 259 virtualHosts.bookstack = mkMerge [ cfg.nginx { 260 root = mkForce "${bookstack}/public"; 261 extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"; 262 locations = { 263 "/" = { 264 index = "index.php"; 265 extraConfig = ''try_files $uri $uri/ /index.php?$query_string;''; 266 }; 267 "~ \.php$" = { 268 extraConfig = '' 269 try_files $uri $uri/ /index.php?$query_string; 270 include ${pkgs.nginx}/conf/fastcgi_params; 271 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 272 fastcgi_param REDIRECT_STATUS 200; 273 fastcgi_pass unix:${config.services.phpfpm.pools."bookstack".socket}; 274 ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"} 275 ''; 276 }; 277 "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = { 278 extraConfig = "expires 365d;"; 279 }; 280 }; 281 }]; 282 }; 283 284 systemd.services.bookstack-setup = { 285 description = "Preperation tasks for BookStack"; 286 before = [ "phpfpm-bookstack.service" ]; 287 after = optional db.createLocally "mysql.service"; 288 wantedBy = [ "multi-user.target" ]; 289 serviceConfig = { 290 Type = "oneshot"; 291 User = user; 292 WorkingDirectory = "${bookstack}"; 293 }; 294 script = '' 295 # set permissions 296 umask 077 297 # create .env file 298 echo " 299 APP_KEY=base64:$(head -n1 ${cfg.appKeyFile}) 300 APP_URL=${cfg.appURL} 301 DB_HOST=${db.host} 302 DB_PORT=${toString db.port} 303 DB_DATABASE=${db.name} 304 DB_USERNAME=${db.user} 305 MAIL_DRIVER=${mail.driver} 306 MAIL_FROM_NAME=\"${mail.fromName}\" 307 MAIL_FROM=${mail.from} 308 MAIL_HOST=${mail.host} 309 MAIL_PORT=${toString mail.port} 310 ${optionalString (mail.user != null) "MAIL_USERNAME=${mail.user};"} 311 ${optionalString (mail.encryption != null) "MAIL_ENCRYPTION=${mail.encryption};"} 312 ${optionalString (db.passwordFile != null) "DB_PASSWORD=$(head -n1 ${db.passwordFile})"} 313 ${optionalString (mail.passwordFile != null) "MAIL_PASSWORD=$(head -n1 ${mail.passwordFile})"} 314 APP_SERVICES_CACHE=${cfg.cacheDir}/services.php 315 APP_PACKAGES_CACHE=${cfg.cacheDir}/packages.php 316 APP_CONFIG_CACHE=${cfg.cacheDir}/config.php 317 APP_ROUTES_CACHE=${cfg.cacheDir}/routes-v7.php 318 APP_EVENTS_CACHE=${cfg.cacheDir}/events.php 319 ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "SESSION_SECURE_COOKIE=true"} 320 ${toString cfg.extraConfig} 321 " > "${cfg.dataDir}/.env" 322 323 # migrate db 324 ${pkgs.php}/bin/php artisan migrate --force 325 326 # clear & create caches (needed in case of update) 327 ${pkgs.php}/bin/php artisan cache:clear 328 ${pkgs.php}/bin/php artisan config:clear 329 ${pkgs.php}/bin/php artisan view:clear 330 ${pkgs.php}/bin/php artisan config:cache 331 ${pkgs.php}/bin/php artisan route:cache 332 ${pkgs.php}/bin/php artisan view:cache 333 ''; 334 }; 335 336 systemd.tmpfiles.rules = [ 337 "d ${cfg.cacheDir} 0700 ${user} ${group} - -" 338 "d ${cfg.dataDir} 0710 ${user} ${group} - -" 339 "d ${cfg.dataDir}/public 0750 ${user} ${group} - -" 340 "d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -" 341 "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" 342 "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" 343 "d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -" 344 "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" 345 "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" 346 "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" 347 "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" 348 "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" 349 "d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -" 350 ]; 351 352 users = { 353 users = mkIf (user == "bookstack") { 354 bookstack = { 355 inherit group; 356 isSystemUser = true; 357 }; 358 "${config.services.nginx.user}".extraGroups = [ group ]; 359 }; 360 groups = mkIf (group == "bookstack") { 361 bookstack = {}; 362 }; 363 }; 364 365 }; 366 367 meta.maintainers = with maintainers; [ ymarkus ]; 368}