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}