1 { config, lib, pkgs, ... }:
6 cfg = config.services.bookstack;
7 bookstack = pkgs.bookstack.override {
16 # shell script for local administration
17 artisan = pkgs.writeScriptBin "bookstack" ''
18 #! ${pkgs.runtimeShell}
21 if [[ "$USER" != ${user} ]]; then
22 sudo='exec /run/wrappers/bin/sudo -u ${user}'
24 $sudo ${pkgs.php}/bin/php artisan $*
27 tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
31 (mkRemovedOptionModule [ "services" "bookstack" "extraConfig" ] "Use services.bookstack.config instead.")
32 (mkRemovedOptionModule [ "services" "bookstack" "cacheDir" ] "The cache directory is now handled automatically.")
35 options.services.bookstack = {
37 enable = mkEnableOption "BookStack";
40 default = "bookstack";
41 description = "User bookstack runs as.";
46 default = "bookstack";
47 description = "Group bookstack runs as.";
51 appKeyFile = mkOption {
53 A file containing the Laravel APP_KEY - a 32 character long,
54 base64 encoded key used for encryption where needed. Can be
55 generated with `head -c 32 /dev/urandom | base64`.
57 example = "/run/keys/bookstack-appkey";
61 hostname = lib.mkOption {
63 default = config.networking.fqdnOrHostName;
64 defaultText = lib.literalExpression "config.networking.fqdnOrHostName";
65 example = "bookstack.example.com";
67 The hostname to serve BookStack on.
73 The root URL that you want to host BookStack on. All URLs in BookStack will be generated using this value.
74 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`
76 default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}";
77 defaultText = ''http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostname}'';
78 example = "https://example.com";
83 description = "BookStack data directory";
84 default = "/var/lib/bookstack";
91 default = "localhost";
92 description = "Database host address.";
97 description = "Database host port.";
101 default = "bookstack";
102 description = "Database name.";
107 defaultText = literalExpression "user";
108 description = "Database username.";
110 passwordFile = mkOption {
111 type = with types; nullOr path;
113 example = "/run/keys/bookstack-dbpassword";
115 A file containing the password corresponding to
116 {option}`database.user`.
119 createLocally = mkOption {
122 description = "Create the database and database user locally.";
128 type = types.enum [ "smtp" "sendmail" ];
130 description = "Mail driver to use.";
134 default = "localhost";
135 description = "Mail host address.";
140 description = "Mail host port.";
142 fromName = mkOption {
144 default = "BookStack";
145 description = "Mail \"from\" name.";
149 default = "mail@bookstackapp.com";
150 description = "Mail \"from\" email.";
153 type = with types; nullOr str;
155 example = "bookstack";
156 description = "Mail username.";
158 passwordFile = mkOption {
159 type = with types; nullOr path;
161 example = "/run/keys/bookstack-mailpassword";
163 A file containing the password corresponding to
167 encryption = mkOption {
168 type = with types; nullOr (enum [ "tls" ]);
170 description = "SMTP encryption mechanism to use.";
174 maxUploadSize = mkOption {
178 description = "The maximum size for uploads (e.g. images).";
181 poolConfig = mkOption {
182 type = with types; attrsOf (oneOf [ str int bool ]);
185 "pm.max_children" = 32;
186 "pm.start_servers" = 2;
187 "pm.min_spare_servers" = 2;
188 "pm.max_spare_servers" = 4;
189 "pm.max_requests" = 500;
192 Options for the bookstack PHP pool. See the documentation on `php-fpm.conf`
193 for details on configuration directives.
198 type = types.submodule (
200 (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
203 example = literalExpression ''
206 "bookstack.''${config.networking.domain}"
208 # To enable encryption and let let's encrypt take care of certificate
214 With this option, you can customize the nginx virtualHost settings.
235 The path to a file containing the value the
236 option should be set to in the final
243 example = literalExpression ''
245 ALLOWED_IFRAME_HOSTS = "https://example.com";
246 WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf";
247 AUTH_METHOD = "oidc";
248 OIDC_NAME = "MyLogin";
249 OIDC_DISPLAY_NAME_CLAIMS = "name";
250 OIDC_CLIENT_ID = "bookstack";
251 OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
252 OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
253 OIDC_ISSUER_DISCOVER = true;
257 BookStack configuration options to set in the
260 Refer to <https://www.bookstackapp.com/docs/>
261 for details on supported values.
263 Settings containing secret data should be set to an attribute
264 set containing the attribute `_secret` - a
265 string pointing to a file containing the value the option
266 should be set to. See the example to get a better picture of
267 this: in the resulting {file}`.env` file, the
268 `OIDC_CLIENT_SECRET` key will be set to the
269 contents of the {file}`/run/keys/oidc_secret`
276 config = mkIf cfg.enable {
279 { assertion = db.createLocally -> db.user == user;
280 message = "services.bookstack.database.user must be set to ${user} if services.bookstack.database.createLocally is set true.";
282 { assertion = db.createLocally -> db.passwordFile == null;
283 message = "services.bookstack.database.passwordFile cannot be specified if services.bookstack.database.createLocally is set to true.";
287 services.bookstack.config = {
288 APP_KEY._secret = cfg.appKeyFile;
289 APP_URL = cfg.appURL;
292 DB_DATABASE = db.name;
293 DB_USERNAME = db.user;
294 MAIL_DRIVER = mail.driver;
295 MAIL_FROM_NAME = mail.fromName;
296 MAIL_FROM = mail.from;
297 MAIL_HOST = mail.host;
298 MAIL_PORT = mail.port;
299 MAIL_USERNAME = mail.user;
300 MAIL_ENCRYPTION = mail.encryption;
301 DB_PASSWORD._secret = db.passwordFile;
302 MAIL_PASSWORD._secret = mail.passwordFile;
303 APP_SERVICES_CACHE = "/run/bookstack/cache/services.php";
304 APP_PACKAGES_CACHE = "/run/bookstack/cache/packages.php";
305 APP_CONFIG_CACHE = "/run/bookstack/cache/config.php";
306 APP_ROUTES_CACHE = "/run/bookstack/cache/routes-v7.php";
307 APP_EVENTS_CACHE = "/run/bookstack/cache/events.php";
308 SESSION_SECURE_COOKIE = tlsEnabled;
311 environment.systemPackages = [ artisan ];
313 services.mysql = mkIf db.createLocally {
315 package = mkDefault pkgs.mariadb;
316 ensureDatabases = [ db.name ];
319 ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; };
324 services.phpfpm.pools.bookstack = {
329 post_max_size = ${cfg.maxUploadSize}
330 upload_max_filesize = ${cfg.maxUploadSize}
333 "listen.mode" = "0660";
334 "listen.owner" = user;
335 "listen.group" = group;
340 enable = mkDefault true;
341 recommendedTlsSettings = true;
342 recommendedOptimisation = true;
343 recommendedGzipSettings = true;
344 virtualHosts.${cfg.hostname} = mkMerge [ cfg.nginx {
345 root = mkForce "${bookstack}/public";
349 tryFiles = "$uri $uri/ /index.php?$query_string";
351 "~ \.php$".extraConfig = ''
352 fastcgi_pass unix:${config.services.phpfpm.pools."bookstack".socket};
354 "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
355 extraConfig = "expires 365d;";
361 systemd.services.bookstack-setup = {
362 description = "Preparation tasks for BookStack";
363 before = [ "phpfpm-bookstack.service" ];
364 after = optional db.createLocally "mysql.service";
365 wantedBy = [ "multi-user.target" ];
368 RemainAfterExit = true;
370 WorkingDirectory = "${bookstack}";
371 RuntimeDirectory = "bookstack/cache";
372 RuntimeDirectoryMode = "0700";
374 path = [ pkgs.replace-secret ];
377 isSecret = v: isAttrs v && v ? _secret && isString v._secret;
378 bookstackEnvVars = lib.generators.toKeyValue {
379 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
380 mkValueString = v: with builtins;
381 if isInt v then toString v
382 else if isString v then v
383 else if true == v then "true"
384 else if false == v then "false"
385 else if isSecret v then hashString "sha256" v._secret
386 else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
389 secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
390 mkSecretReplacement = file: ''
391 replace-secret ${escapeShellArgs [ (builtins.hashString "sha256" file) file "${cfg.dataDir}/.env" ]}
393 secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
394 filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ {} null ])) cfg.config;
395 bookstackEnv = pkgs.writeText "bookstack.env" (bookstackEnvVars filteredConfig);
404 install -T -m 0600 -o ${user} ${bookstackEnv} "${cfg.dataDir}/.env"
405 ${secretReplacements}
406 if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
407 sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
411 ${pkgs.php}/bin/php artisan migrate --force
415 systemd.tmpfiles.settings."10-bookstack" = let
421 "${cfg.dataDir}".d = defaultConfig // { mode = "0710"; };
422 "${cfg.dataDir}/public".d = defaultConfig // { mode = "0750"; };
423 "${cfg.dataDir}/public/uploads".d = defaultConfig // { mode = "0750"; };
424 "${cfg.dataDir}/storage".d = defaultConfig;
425 "${cfg.dataDir}/storage/app".d = defaultConfig;
426 "${cfg.dataDir}/storage/fonts".d = defaultConfig;
427 "${cfg.dataDir}/storage/framework".d = defaultConfig;
428 "${cfg.dataDir}/storage/framework/cache".d = defaultConfig;
429 "${cfg.dataDir}/storage/framework/sessions".d = defaultConfig;
430 "${cfg.dataDir}/storage/framework/views".d = defaultConfig;
431 "${cfg.dataDir}/storage/logs".d = defaultConfig;
432 "${cfg.dataDir}/storage/uploads".d = defaultConfig;
436 users = mkIf (user == "bookstack") {
441 "${config.services.nginx.user}".extraGroups = [ group ];
443 groups = mkIf (group == "bookstack") {
450 meta.maintainers = with maintainers; [ ymarkus ];