at 22.05-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 = literalExpression "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 = literalExpression '' 191 { 192 serverAliases = [ 193 "bookstack.''${config.networking.domain}" 194 ]; 195 # To enable encryption and let let's encrypt take care of certificate 196 forceSSL = true; 197 enableACME = true; 198 } 199 ''; 200 description = '' 201 With this option, you can customize the nginx virtualHost settings. 202 ''; 203 }; 204 205 extraConfig = mkOption { 206 type = types.nullOr types.lines; 207 default = null; 208 example = '' 209 ALLOWED_IFRAME_HOSTS="https://example.com" 210 WKHTMLTOPDF=/home/user/bins/wkhtmltopdf 211 ''; 212 description = '' 213 Lines to be appended verbatim to the BookStack configuration. 214 Refer to <link xlink:href="https://www.bookstackapp.com/docs/"/> for details on supported values. 215 ''; 216 }; 217 218 }; 219 220 config = mkIf cfg.enable { 221 222 assertions = [ 223 { assertion = db.createLocally -> db.user == user; 224 message = "services.bookstack.database.user must be set to ${user} if services.bookstack.database.createLocally is set true."; 225 } 226 { assertion = db.createLocally -> db.passwordFile == null; 227 message = "services.bookstack.database.passwordFile cannot be specified if services.bookstack.database.createLocally is set to true."; 228 } 229 ]; 230 231 environment.systemPackages = [ artisan ]; 232 233 services.mysql = mkIf db.createLocally { 234 enable = true; 235 package = mkDefault pkgs.mariadb; 236 ensureDatabases = [ db.name ]; 237 ensureUsers = [ 238 { name = db.user; 239 ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; }; 240 } 241 ]; 242 }; 243 244 services.phpfpm.pools.bookstack = { 245 inherit user; 246 inherit group; 247 phpOptions = '' 248 log_errors = on 249 post_max_size = ${cfg.maxUploadSize} 250 upload_max_filesize = ${cfg.maxUploadSize} 251 ''; 252 settings = { 253 "listen.mode" = "0660"; 254 "listen.owner" = user; 255 "listen.group" = group; 256 } // cfg.poolConfig; 257 }; 258 259 services.nginx = { 260 enable = mkDefault true; 261 virtualHosts.bookstack = mkMerge [ cfg.nginx { 262 root = mkForce "${bookstack}/public"; 263 extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"; 264 locations = { 265 "/" = { 266 index = "index.php"; 267 extraConfig = ''try_files $uri $uri/ /index.php?$query_string;''; 268 }; 269 "~ \.php$" = { 270 extraConfig = '' 271 try_files $uri $uri/ /index.php?$query_string; 272 include ${pkgs.nginx}/conf/fastcgi_params; 273 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 274 fastcgi_param REDIRECT_STATUS 200; 275 fastcgi_pass unix:${config.services.phpfpm.pools."bookstack".socket}; 276 ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"} 277 ''; 278 }; 279 "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = { 280 extraConfig = "expires 365d;"; 281 }; 282 }; 283 }]; 284 }; 285 286 systemd.services.bookstack-setup = { 287 description = "Preperation tasks for BookStack"; 288 before = [ "phpfpm-bookstack.service" ]; 289 after = optional db.createLocally "mysql.service"; 290 wantedBy = [ "multi-user.target" ]; 291 serviceConfig = { 292 Type = "oneshot"; 293 User = user; 294 WorkingDirectory = "${bookstack}"; 295 }; 296 script = '' 297 # set permissions 298 umask 077 299 # create .env file 300 echo " 301 APP_KEY=base64:$(head -n1 ${cfg.appKeyFile}) 302 APP_URL=${cfg.appURL} 303 DB_HOST=${db.host} 304 DB_PORT=${toString db.port} 305 DB_DATABASE=${db.name} 306 DB_USERNAME=${db.user} 307 MAIL_DRIVER=${mail.driver} 308 MAIL_FROM_NAME=\"${mail.fromName}\" 309 MAIL_FROM=${mail.from} 310 MAIL_HOST=${mail.host} 311 MAIL_PORT=${toString mail.port} 312 ${optionalString (mail.user != null) "MAIL_USERNAME=${mail.user};"} 313 ${optionalString (mail.encryption != null) "MAIL_ENCRYPTION=${mail.encryption};"} 314 ${optionalString (db.passwordFile != null) "DB_PASSWORD=$(head -n1 ${db.passwordFile})"} 315 ${optionalString (mail.passwordFile != null) "MAIL_PASSWORD=$(head -n1 ${mail.passwordFile})"} 316 APP_SERVICES_CACHE=${cfg.cacheDir}/services.php 317 APP_PACKAGES_CACHE=${cfg.cacheDir}/packages.php 318 APP_CONFIG_CACHE=${cfg.cacheDir}/config.php 319 APP_ROUTES_CACHE=${cfg.cacheDir}/routes-v7.php 320 APP_EVENTS_CACHE=${cfg.cacheDir}/events.php 321 ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "SESSION_SECURE_COOKIE=true"} 322 ${toString cfg.extraConfig} 323 " > "${cfg.dataDir}/.env" 324 325 # migrate db 326 ${pkgs.php}/bin/php artisan migrate --force 327 328 # clear & create caches (needed in case of update) 329 ${pkgs.php}/bin/php artisan cache:clear 330 ${pkgs.php}/bin/php artisan config:clear 331 ${pkgs.php}/bin/php artisan view:clear 332 ${pkgs.php}/bin/php artisan config:cache 333 ${pkgs.php}/bin/php artisan route:cache 334 ${pkgs.php}/bin/php artisan view:cache 335 ''; 336 }; 337 338 systemd.tmpfiles.rules = [ 339 "d ${cfg.cacheDir} 0700 ${user} ${group} - -" 340 "d ${cfg.dataDir} 0710 ${user} ${group} - -" 341 "d ${cfg.dataDir}/public 0750 ${user} ${group} - -" 342 "d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -" 343 "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" 344 "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" 345 "d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -" 346 "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" 347 "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" 348 "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" 349 "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" 350 "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" 351 "d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -" 352 ]; 353 354 users = { 355 users = mkIf (user == "bookstack") { 356 bookstack = { 357 inherit group; 358 isSystemUser = true; 359 }; 360 "${config.services.nginx.user}".extraGroups = [ group ]; 361 }; 362 groups = mkIf (group == "bookstack") { 363 bookstack = {}; 364 }; 365 }; 366 367 }; 368 369 meta.maintainers = with maintainers; [ ymarkus ]; 370}