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}