at master 15 kB view raw
1{ 2 pkgs, 3 config, 4 lib, 5 ... 6}: 7 8let 9 cfg = config.services.bookstack; 10 11 user = cfg.user; 12 group = cfg.group; 13 14 defaultUser = "bookstack"; 15 defaultGroup = "bookstack"; 16 17 artisan = "${cfg.package}/artisan"; 18 19 env-file-values = lib.mapAttrs' (n: v: { 20 name = lib.removeSuffix "_FILE" n; 21 value = v; 22 }) (lib.filterAttrs (n: v: v != null && lib.match ".+_FILE" n != null) cfg.settings); 23 24 env-nonfile-values = lib.filterAttrs (n: v: lib.match ".+_FILE" n == null) cfg.settings; 25 26 bookstack-maintenance = pkgs.writeShellScript "bookstack-maintenance.sh" '' 27 set -a 28 ${lib.toShellVars env-nonfile-values} 29 ${lib.concatLines (lib.mapAttrsToList (n: v: "${n}=\"$(< ${v})\"") env-file-values)} 30 set +a 31 ${artisan} optimize:clear 32 rm ${cfg.dataDir}/cache/*.php 33 ${artisan} package:discover 34 ${artisan} migrate --force 35 ${artisan} view:cache 36 ${artisan} route:cache 37 ${artisan} config:cache 38 ''; 39 40 commonServiceConfig = { 41 Type = "oneshot"; 42 User = user; 43 Group = group; 44 StateDirectory = "bookstack"; 45 ReadWritePaths = [ cfg.dataDir ]; 46 WorkingDirectory = cfg.package; 47 PrivateTmp = true; 48 PrivateDevices = true; 49 CapabilityBoundingSet = ""; 50 AmbientCapabilities = ""; 51 ProtectSystem = "strict"; 52 ProtectKernelTunables = true; 53 ProtectKernelModules = true; 54 ProtectControlGroups = true; 55 ProtectClock = true; 56 ProtectHostname = true; 57 ProtectHome = "tmpfs"; 58 ProtectKernelLogs = true; 59 ProtectProc = "invisible"; 60 ProcSubset = "pid"; 61 PrivateNetwork = false; 62 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; 63 SystemCallArchitectures = "native"; 64 SystemCallFilter = [ 65 "@system-service @resources" 66 "~@obsolete @privileged" 67 ]; 68 RestrictSUIDSGID = true; 69 RemoveIPC = true; 70 NoNewPrivileges = true; 71 RestrictRealtime = true; 72 RestrictNamespaces = true; 73 LockPersonality = true; 74 PrivateUsers = true; 75 }; 76 77in 78{ 79 imports = [ 80 (lib.mkRemovedOptionModule [ 81 "services" 82 "bookstack" 83 "extraConfig" 84 ] "Use services.bookstack.settings instead.") 85 (lib.mkRemovedOptionModule [ 86 "services" 87 "bookstack" 88 "config" 89 ] "Use services.bookstack.settings instead.") 90 (lib.mkRemovedOptionModule [ 91 "services" 92 "bookstack" 93 "cacheDir" 94 ] "The cache directory is now handled automatically.") 95 (lib.mkRemovedOptionModule [ 96 "services" 97 "bookstack" 98 "appKeyFile" 99 ] "Use services.bookstack.settings.APP_KEY_FILE instead.") 100 (lib.mkRemovedOptionModule [ 101 "services" 102 "bookstack" 103 "appURL" 104 ] "Use services.bookstack.settings.APP_URL instead.") 105 (lib.mkRemovedOptionModule [ 106 "services" 107 "bookstack" 108 "database" 109 "host" 110 ] "Use services.bookstack.settings.DB_HOST instead.") 111 (lib.mkRemovedOptionModule [ 112 "services" 113 "bookstack" 114 "database" 115 "port" 116 ] "Use services.bookstack.settings.DB_PORT instead.") 117 (lib.mkRemovedOptionModule [ 118 "services" 119 "bookstack" 120 "database" 121 "passwordFile" 122 ] "Use services.bookstack.settings.DB_PASSWORD_FILE instead.") 123 (lib.mkRemovedOptionModule [ 124 "services" 125 "bookstack" 126 "database" 127 "name" 128 ] "Use services.bookstack.settings.DB_DATABASE instead.") 129 (lib.mkRemovedOptionModule [ 130 "services" 131 "bookstack" 132 "database" 133 "user" 134 ] "Use services.bookstack.settings.DB_USERNAME instead.") 135 (lib.mkRemovedOptionModule [ 136 "services" 137 "bookstack" 138 "database" 139 "createLocally" 140 ] "Use services.mysql.ensureDatabases and services.mysql.ensureUsers instead.") 141 (lib.mkRemovedOptionModule [ 142 "services" 143 "bookstack" 144 "mail" 145 "host" 146 ] "Use services.bookstack.settings.MAIL_HOST instead.") 147 (lib.mkRemovedOptionModule [ 148 "services" 149 "bookstack" 150 "mail" 151 "port" 152 ] "Use services.bookstack.settings.MAIL_PORT instead.") 153 (lib.mkRemovedOptionModule [ 154 "services" 155 "bookstack" 156 "mail" 157 "passwordFile" 158 ] "Use services.bookstack.settings.MAIL_PASSWORD_FILE instead.") 159 (lib.mkRemovedOptionModule [ 160 "services" 161 "bookstack" 162 "mail" 163 "name" 164 ] "Use services.bookstack.settings.MAIL_DATABASE instead.") 165 (lib.mkRemovedOptionModule [ 166 "services" 167 "bookstack" 168 "mail" 169 "user" 170 ] "Use services.bookstack.settings.MAIL_USERNAME instead.") 171 (lib.mkRemovedOptionModule [ 172 "services" 173 "bookstack" 174 "mail" 175 "driver" 176 ] "Use services.bookstack.settings.MAIL_DRIVER instead.") 177 (lib.mkRemovedOptionModule [ 178 "services" 179 "bookstack" 180 "mail" 181 "fromName" 182 ] "Use services.bookstack.settings.MAIL_FROM_NAME instead.") 183 (lib.mkRemovedOptionModule [ 184 "services" 185 "bookstack" 186 "mail" 187 "from" 188 ] "Use services.bookstack.settings.MAIL_FROM instead.") 189 (lib.mkRemovedOptionModule [ 190 "services" 191 "bookstack" 192 "mail" 193 "encryption" 194 ] "Use services.bookstack.settings.MAIL_ENCRYPTION instead.") 195 ]; 196 197 options.services.bookstack = { 198 enable = lib.mkEnableOption "BookStack: A platform to create documentation/wiki content built with PHP & Laravel"; 199 200 package = 201 lib.mkPackageOption pkgs "bookstack" { } 202 // lib.mkOption { 203 apply = 204 bookstack: 205 bookstack.override (prev: { 206 dataDir = cfg.dataDir; 207 }); 208 }; 209 210 user = lib.mkOption { 211 default = defaultUser; 212 description = "User bookstack runs as"; 213 type = lib.types.str; 214 }; 215 216 group = lib.mkOption { 217 default = if (cfg.nginx != null) then config.services.nginx.group else defaultGroup; 218 defaultText = "If `services.bookstack.nginx` has any attributes then `nginx` else ${defaultGroup}"; 219 description = "Group bookstack runs as"; 220 type = lib.types.str; 221 }; 222 223 hostname = lib.mkOption { 224 type = lib.types.str; 225 default = config.networking.fqdnOrHostName; 226 defaultText = lib.literalExpression "config.networking.fqdnOrHostName"; 227 example = "bookstack.example.com"; 228 description = '' 229 The hostname to serve BookStack on. 230 ''; 231 }; 232 233 dataDir = lib.mkOption { 234 description = "BookStack data directory"; 235 default = "/var/lib/bookstack"; 236 type = lib.types.path; 237 }; 238 239 settings = lib.mkOption { 240 default = { }; 241 description = '' 242 Options for Bookstack configuration. Refer to 243 <https://github.com/BookStackApp/BookStack/blob/development/.env.example> for 244 details on supported values. For passing secrets, append "_FILE" to the 245 setting name. For example, you may create a file `/var/secrets/db_pass.txt` 246 and set `services.bookstack.settings.DB_PASSWORD_FILE` to `/var/secrets/db_pass.txt` 247 instead of providing a plaintext password using `services.bookstack.settings.DB_PASSWORD`. 248 ''; 249 example = lib.literalExpression '' 250 { 251 APP_ENV = "production"; 252 APP_KEY_FILE = "/var/secrets/bookstack-app-key.txt"; 253 DB_HOST = "db"; 254 DB_PORT = 3306; 255 DB_DATABASE = "bookstack"; 256 DB_USERNAME = "bookstack"; 257 DB_PASSWORD_FILE = "/var/secrets/bookstack-mysql-password.txt"; 258 } 259 ''; 260 type = lib.types.submodule { 261 freeformType = lib.types.attrsOf ( 262 lib.types.oneOf [ 263 lib.types.str 264 lib.types.int 265 lib.types.bool 266 ] 267 ); 268 options = { 269 DB_PORT = lib.mkOption { 270 type = lib.types.port; 271 default = 3306; 272 description = '' 273 The port your database is listening at. 274 ''; 275 }; 276 DB_HOST = lib.mkOption { 277 type = lib.types.str; 278 default = "localhost"; 279 description = '' 280 The IP or hostname which hosts your database. 281 ''; 282 }; 283 DB_PASSWORD_FILE = lib.mkOption { 284 type = lib.types.nullOr lib.types.path; 285 description = '' 286 The file containing your mysql/mariadb database password. 287 ''; 288 example = "/var/secrets/bookstack-mysql-pass.txt"; 289 default = null; 290 }; 291 APP_KEY_FILE = lib.mkOption { 292 type = lib.types.path; 293 description = '' 294 The path to your appkey. 295 The file should contain a 32 character random app key. 296 This may be set using `echo "base64:$(head -c 32 /dev/urandom | base64)" > /path/to/key-file`. 297 ''; 298 }; 299 APP_URL = lib.mkOption { 300 type = lib.types.str; 301 default = 302 if cfg.hostname == "localhost" then "http://${cfg.hostname}" else "https://${cfg.hostname}"; 303 defaultText = ''http(s)://''${config.services.bookstack.hostname}''; 304 description = '' 305 The root URL that you want to host BookStack on. All URLs in BookStack 306 will be generated using this value. It is used to validate specific 307 requests and to generate URLs in emails. 308 ''; 309 example = "https://example.com"; 310 }; 311 }; 312 }; 313 }; 314 315 maxUploadSize = lib.mkOption { 316 type = lib.types.str; 317 default = "18M"; 318 example = "1G"; 319 description = "The maximum size for uploads (e.g. images)."; 320 }; 321 322 poolConfig = lib.mkOption { 323 type = lib.types.attrsOf ( 324 lib.types.oneOf [ 325 lib.types.str 326 lib.types.int 327 lib.types.bool 328 ] 329 ); 330 default = { }; 331 defaultText = '' 332 { 333 "pm" = "dynamic"; 334 "pm.max_children" = 32; 335 "pm.start_servers" = 2; 336 "pm.min_spare_servers" = 2; 337 "pm.max_spare_servers" = 4; 338 "pm.max_requests" = 500; 339 } 340 ''; 341 description = '' 342 Options for the Bookstack PHP pool. See the documentation on `php-fpm.conf` 343 for details on configuration directives. 344 ''; 345 }; 346 347 nginx = lib.mkOption { 348 type = lib.types.nullOr ( 349 lib.types.submodule ( 350 lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { } 351 ) 352 ); 353 default = null; 354 example = lib.literalExpression '' 355 { 356 serverAliases = [ 357 "bookstack.''${config.networking.domain}" 358 ]; 359 # To enable encryption and let let's encrypt take care of certificate 360 forceSSL = true; 361 enableACME = true; 362 } 363 ''; 364 description = '' 365 With this option, you can customize the nginx virtualHost settings. 366 ''; 367 }; 368 }; 369 370 config = lib.mkIf cfg.enable { 371 372 services.phpfpm.pools.bookstack = { 373 inherit user group; 374 phpPackage = cfg.package.phpPackage; 375 phpOptions = '' 376 log_errors = on 377 post_max_size = ${cfg.maxUploadSize} 378 upload_max_filesize = ${cfg.maxUploadSize} 379 ''; 380 settings = { 381 "listen.mode" = lib.mkDefault "0660"; 382 "listen.owner" = lib.mkDefault user; 383 "listen.group" = lib.mkDefault group; 384 "pm" = lib.mkDefault "dynamic"; 385 "pm.max_children" = lib.mkDefault 32; 386 "pm.start_servers" = lib.mkDefault 2; 387 "pm.min_spare_servers" = lib.mkDefault 2; 388 "pm.max_spare_servers" = lib.mkDefault 4; 389 "pm.max_requests" = lib.mkDefault 500; 390 } 391 // cfg.poolConfig; 392 }; 393 394 services.nginx = lib.mkIf (cfg.nginx != null) { 395 enable = true; 396 recommendedTlsSettings = true; 397 recommendedOptimisation = true; 398 recommendedGzipSettings = true; 399 virtualHosts.${cfg.hostname} = lib.mkMerge [ 400 cfg.nginx 401 { 402 locations = { 403 "/" = { 404 root = "${cfg.package}/public"; 405 index = "index.php"; 406 tryFiles = "$uri $uri/ /index.php?$query_string"; 407 extraConfig = '' 408 sendfile off; 409 ''; 410 }; 411 "~ \\.php$" = { 412 root = "${cfg.package}/public"; 413 extraConfig = '' 414 include ${config.services.nginx.package}/conf/fastcgi_params; 415 fastcgi_param SCRIPT_FILENAME $request_filename; 416 fastcgi_param modHeadersAvailable true; # Avoid sending the security headers twice 417 fastcgi_pass unix:${config.services.phpfpm.pools."bookstack".socket}; 418 ''; 419 }; 420 "~ \\.(js|css|gif|png|ico|jpg|jpeg)$" = { 421 root = "${cfg.package}/public"; 422 extraConfig = "expires 365d;"; 423 }; 424 }; 425 } 426 ]; 427 }; 428 429 systemd.services.bookstack-setup = { 430 after = [ "mysql.service" ]; 431 requiredBy = [ "phpfpm-bookstack.service" ]; 432 before = [ "phpfpm-bookstack.service" ]; 433 serviceConfig = { 434 ExecStart = bookstack-maintenance; 435 RemainAfterExit = true; 436 } 437 // commonServiceConfig; 438 unitConfig.JoinsNamespaceOf = "phpfpm-bookstack.service"; 439 restartTriggers = [ cfg.package ]; 440 partOf = [ "phpfpm-bookstack.service" ]; 441 }; 442 443 systemd.tmpfiles.settings."10-bookstack" = 444 let 445 defaultConfig = { 446 inherit user group; 447 mode = "0700"; 448 }; 449 in 450 { 451 "${cfg.dataDir}".d = defaultConfig // { 452 mode = "0710"; 453 }; 454 "${cfg.dataDir}/public".d = defaultConfig // { 455 mode = "0750"; 456 }; 457 "${cfg.dataDir}/public/uploads".d = defaultConfig // { 458 mode = "0750"; 459 }; 460 "${cfg.dataDir}/storage".d = defaultConfig; 461 "${cfg.dataDir}/storage/app".d = defaultConfig; 462 "${cfg.dataDir}/storage/fonts".d = defaultConfig; 463 "${cfg.dataDir}/storage/framework".d = defaultConfig; 464 "${cfg.dataDir}/storage/framework/cache".d = defaultConfig; 465 "${cfg.dataDir}/storage/framework/sessions".d = defaultConfig; 466 "${cfg.dataDir}/storage/framework/views".d = defaultConfig; 467 "${cfg.dataDir}/storage/logs".d = defaultConfig; 468 "${cfg.dataDir}/storage/uploads".d = defaultConfig; 469 "${cfg.dataDir}/cache".d = defaultConfig; 470 "${cfg.dataDir}/themes".d = defaultConfig; 471 }; 472 473 users = { 474 users = lib.mkIf (user == defaultUser) { 475 bookstack = { 476 inherit group; 477 isSystemUser = true; 478 home = cfg.dataDir; 479 }; 480 }; 481 groups = lib.mkIf (group == defaultGroup) { 482 bookstack = { }; 483 }; 484 }; 485 }; 486 487 meta.maintainers = with lib.maintainers; [ 488 ymarkus 489 savyajha 490 ]; 491}