1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.hedgedoc;
7
8 # 21.03 will not be an official release - it was instead 21.05. This
9 # versionAtLeast statement remains set to 21.03 for backwards compatibility.
10 # See https://github.com/NixOS/nixpkgs/pull/108899 and
11 # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
12 name = if versionAtLeast config.system.stateVersion "21.03"
13 then "hedgedoc"
14 else "codimd";
15
16 prettyJSON = conf:
17 pkgs.runCommandLocal "hedgedoc-config.json" {
18 nativeBuildInputs = [ pkgs.jq ];
19 } ''
20 echo '${builtins.toJSON conf}' | jq \
21 '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out
22 '';
23in
24{
25 imports = [
26 (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
27 ];
28
29 options.services.hedgedoc = {
30 enable = mkEnableOption "the HedgeDoc Markdown Editor";
31
32 groups = mkOption {
33 type = types.listOf types.str;
34 default = [];
35 description = ''
36 Groups to which the user ${name} should be added.
37 '';
38 };
39
40 workDir = mkOption {
41 type = types.path;
42 default = "/var/lib/${name}";
43 description = ''
44 Working directory for the HedgeDoc service.
45 '';
46 };
47
48 configuration = {
49 debug = mkEnableOption "debug mode";
50 domain = mkOption {
51 type = types.nullOr types.str;
52 default = null;
53 example = "hedgedoc.org";
54 description = ''
55 Domain name for the HedgeDoc instance.
56 '';
57 };
58 urlPath = mkOption {
59 type = types.nullOr types.str;
60 default = null;
61 example = "/url/path/to/hedgedoc";
62 description = ''
63 Path under which HedgeDoc is accessible.
64 '';
65 };
66 host = mkOption {
67 type = types.str;
68 default = "localhost";
69 description = ''
70 Address to listen on.
71 '';
72 };
73 port = mkOption {
74 type = types.int;
75 default = 3000;
76 example = "80";
77 description = ''
78 Port to listen on.
79 '';
80 };
81 path = mkOption {
82 type = types.nullOr types.str;
83 default = null;
84 example = "/run/hedgedoc.sock";
85 description = ''
86 Specify where a UNIX domain socket should be placed.
87 '';
88 };
89 allowOrigin = mkOption {
90 type = types.listOf types.str;
91 default = [];
92 example = [ "localhost" "hedgedoc.org" ];
93 description = ''
94 List of domains to whitelist.
95 '';
96 };
97 useSSL = mkOption {
98 type = types.bool;
99 default = false;
100 description = ''
101 Enable to use SSL server. This will also enable
102 <option>protocolUseSSL</option>.
103 '';
104 };
105 hsts = {
106 enable = mkOption {
107 type = types.bool;
108 default = true;
109 description = ''
110 Whether to enable HSTS if HTTPS is also enabled.
111 '';
112 };
113 maxAgeSeconds = mkOption {
114 type = types.int;
115 default = 31536000;
116 description = ''
117 Max duration for clients to keep the HSTS status.
118 '';
119 };
120 includeSubdomains = mkOption {
121 type = types.bool;
122 default = true;
123 description = ''
124 Whether to include subdomains in HSTS.
125 '';
126 };
127 preload = mkOption {
128 type = types.bool;
129 default = true;
130 description = ''
131 Whether to allow preloading of the site's HSTS status.
132 '';
133 };
134 };
135 csp = mkOption {
136 type = types.nullOr types.attrs;
137 default = null;
138 example = literalExample ''
139 {
140 enable = true;
141 directives = {
142 scriptSrc = "trustworthy.scripts.example.com";
143 };
144 upgradeInsecureRequest = "auto";
145 addDefaults = true;
146 }
147 '';
148 description = ''
149 Specify the Content Security Policy which is passed to Helmet.
150 For configuration details see <link xlink:href="https://helmetjs.github.io/docs/csp/"
151 >https://helmetjs.github.io/docs/csp/</link>.
152 '';
153 };
154 protocolUseSSL = mkOption {
155 type = types.bool;
156 default = false;
157 description = ''
158 Enable to use TLS for resource paths.
159 This only applies when <option>domain</option> is set.
160 '';
161 };
162 urlAddPort = mkOption {
163 type = types.bool;
164 default = false;
165 description = ''
166 Enable to add the port to callback URLs.
167 This only applies when <option>domain</option> is set
168 and only for ports other than 80 and 443.
169 '';
170 };
171 useCDN = mkOption {
172 type = types.bool;
173 default = false;
174 description = ''
175 Whether to use CDN resources or not.
176 '';
177 };
178 allowAnonymous = mkOption {
179 type = types.bool;
180 default = true;
181 description = ''
182 Whether to allow anonymous usage.
183 '';
184 };
185 allowAnonymousEdits = mkOption {
186 type = types.bool;
187 default = false;
188 description = ''
189 Whether to allow guests to edit existing notes with the `freely' permission,
190 when <option>allowAnonymous</option> is enabled.
191 '';
192 };
193 allowFreeURL = mkOption {
194 type = types.bool;
195 default = false;
196 description = ''
197 Whether to allow note creation by accessing a nonexistent note URL.
198 '';
199 };
200 defaultPermission = mkOption {
201 type = types.enum [ "freely" "editable" "limited" "locked" "private" ];
202 default = "editable";
203 description = ''
204 Default permissions for notes.
205 This only applies for signed-in users.
206 '';
207 };
208 dbURL = mkOption {
209 type = types.nullOr types.str;
210 default = null;
211 example = ''
212 postgres://user:pass@host:5432/dbname
213 '';
214 description = ''
215 Specify which database to use.
216 HedgeDoc supports mysql, postgres, sqlite and mssql.
217 See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
218 https://sequelize.readthedocs.io/en/v3/</link> for more information.
219 Note: This option overrides <option>db</option>.
220 '';
221 };
222 db = mkOption {
223 type = types.attrs;
224 default = {};
225 example = literalExample ''
226 {
227 dialect = "sqlite";
228 storage = "/var/lib/${name}/db.${name}.sqlite";
229 }
230 '';
231 description = ''
232 Specify the configuration for sequelize.
233 HedgeDoc supports mysql, postgres, sqlite and mssql.
234 See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
235 https://sequelize.readthedocs.io/en/v3/</link> for more information.
236 Note: This option overrides <option>db</option>.
237 '';
238 };
239 sslKeyPath= mkOption {
240 type = types.nullOr types.str;
241 default = null;
242 example = "/var/lib/hedgedoc/hedgedoc.key";
243 description = ''
244 Path to the SSL key. Needed when <option>useSSL</option> is enabled.
245 '';
246 };
247 sslCertPath = mkOption {
248 type = types.nullOr types.str;
249 default = null;
250 example = "/var/lib/hedgedoc/hedgedoc.crt";
251 description = ''
252 Path to the SSL cert. Needed when <option>useSSL</option> is enabled.
253 '';
254 };
255 sslCAPath = mkOption {
256 type = types.listOf types.str;
257 default = [];
258 example = [ "/var/lib/hedgedoc/ca.crt" ];
259 description = ''
260 SSL ca chain. Needed when <option>useSSL</option> is enabled.
261 '';
262 };
263 dhParamPath = mkOption {
264 type = types.nullOr types.str;
265 default = null;
266 example = "/var/lib/hedgedoc/dhparam.pem";
267 description = ''
268 Path to the SSL dh params. Needed when <option>useSSL</option> is enabled.
269 '';
270 };
271 tmpPath = mkOption {
272 type = types.str;
273 default = "/tmp";
274 description = ''
275 Path to the temp directory HedgeDoc should use.
276 Note that <option>serviceConfig.PrivateTmp</option> is enabled for
277 the HedgeDoc systemd service by default.
278 (Non-canonical paths are relative to HedgeDoc's base directory)
279 '';
280 };
281 defaultNotePath = mkOption {
282 type = types.nullOr types.str;
283 default = "./public/default.md";
284 description = ''
285 Path to the default Note file.
286 (Non-canonical paths are relative to HedgeDoc's base directory)
287 '';
288 };
289 docsPath = mkOption {
290 type = types.nullOr types.str;
291 default = "./public/docs";
292 description = ''
293 Path to the docs directory.
294 (Non-canonical paths are relative to HedgeDoc's base directory)
295 '';
296 };
297 indexPath = mkOption {
298 type = types.nullOr types.str;
299 default = "./public/views/index.ejs";
300 description = ''
301 Path to the index template file.
302 (Non-canonical paths are relative to HedgeDoc's base directory)
303 '';
304 };
305 hackmdPath = mkOption {
306 type = types.nullOr types.str;
307 default = "./public/views/hackmd.ejs";
308 description = ''
309 Path to the hackmd template file.
310 (Non-canonical paths are relative to HedgeDoc's base directory)
311 '';
312 };
313 errorPath = mkOption {
314 type = types.nullOr types.str;
315 default = null;
316 defaultText = "./public/views/error.ejs";
317 description = ''
318 Path to the error template file.
319 (Non-canonical paths are relative to HedgeDoc's base directory)
320 '';
321 };
322 prettyPath = mkOption {
323 type = types.nullOr types.str;
324 default = null;
325 defaultText = "./public/views/pretty.ejs";
326 description = ''
327 Path to the pretty template file.
328 (Non-canonical paths are relative to HedgeDoc's base directory)
329 '';
330 };
331 slidePath = mkOption {
332 type = types.nullOr types.str;
333 default = null;
334 defaultText = "./public/views/slide.hbs";
335 description = ''
336 Path to the slide template file.
337 (Non-canonical paths are relative to HedgeDoc's base directory)
338 '';
339 };
340 uploadsPath = mkOption {
341 type = types.str;
342 default = "${cfg.workDir}/uploads";
343 defaultText = "/var/lib/${name}/uploads";
344 description = ''
345 Path under which uploaded files are saved.
346 '';
347 };
348 sessionName = mkOption {
349 type = types.str;
350 default = "connect.sid";
351 description = ''
352 Specify the name of the session cookie.
353 '';
354 };
355 sessionSecret = mkOption {
356 type = types.nullOr types.str;
357 default = null;
358 description = ''
359 Specify the secret used to sign the session cookie.
360 If unset, one will be generated on startup.
361 '';
362 };
363 sessionLife = mkOption {
364 type = types.int;
365 default = 1209600000;
366 description = ''
367 Session life time in milliseconds.
368 '';
369 };
370 heartbeatInterval = mkOption {
371 type = types.int;
372 default = 5000;
373 description = ''
374 Specify the socket.io heartbeat interval.
375 '';
376 };
377 heartbeatTimeout = mkOption {
378 type = types.int;
379 default = 10000;
380 description = ''
381 Specify the socket.io heartbeat timeout.
382 '';
383 };
384 documentMaxLength = mkOption {
385 type = types.int;
386 default = 100000;
387 description = ''
388 Specify the maximum document length.
389 '';
390 };
391 email = mkOption {
392 type = types.bool;
393 default = true;
394 description = ''
395 Whether to enable email sign-in.
396 '';
397 };
398 allowEmailRegister = mkOption {
399 type = types.bool;
400 default = true;
401 description = ''
402 Whether to enable email registration.
403 '';
404 };
405 allowGravatar = mkOption {
406 type = types.bool;
407 default = true;
408 description = ''
409 Whether to use gravatar as profile picture source.
410 '';
411 };
412 imageUploadType = mkOption {
413 type = types.enum [ "imgur" "s3" "minio" "filesystem" ];
414 default = "filesystem";
415 description = ''
416 Specify where to upload images.
417 '';
418 };
419 minio = mkOption {
420 type = types.nullOr (types.submodule {
421 options = {
422 accessKey = mkOption {
423 type = types.str;
424 description = ''
425 Minio access key.
426 '';
427 };
428 secretKey = mkOption {
429 type = types.str;
430 description = ''
431 Minio secret key.
432 '';
433 };
434 endpoint = mkOption {
435 type = types.str;
436 description = ''
437 Minio endpoint.
438 '';
439 };
440 port = mkOption {
441 type = types.int;
442 default = 9000;
443 description = ''
444 Minio listen port.
445 '';
446 };
447 secure = mkOption {
448 type = types.bool;
449 default = true;
450 description = ''
451 Whether to use HTTPS for Minio.
452 '';
453 };
454 };
455 });
456 default = null;
457 description = "Configure the minio third-party integration.";
458 };
459 s3 = mkOption {
460 type = types.nullOr (types.submodule {
461 options = {
462 accessKeyId = mkOption {
463 type = types.str;
464 description = ''
465 AWS access key id.
466 '';
467 };
468 secretAccessKey = mkOption {
469 type = types.str;
470 description = ''
471 AWS access key.
472 '';
473 };
474 region = mkOption {
475 type = types.str;
476 description = ''
477 AWS S3 region.
478 '';
479 };
480 };
481 });
482 default = null;
483 description = "Configure the s3 third-party integration.";
484 };
485 s3bucket = mkOption {
486 type = types.nullOr types.str;
487 default = null;
488 description = ''
489 Specify the bucket name for upload types <literal>s3</literal> and <literal>minio</literal>.
490 '';
491 };
492 allowPDFExport = mkOption {
493 type = types.bool;
494 default = true;
495 description = ''
496 Whether to enable PDF exports.
497 '';
498 };
499 imgur.clientId = mkOption {
500 type = types.nullOr types.str;
501 default = null;
502 description = ''
503 Imgur API client ID.
504 '';
505 };
506 azure = mkOption {
507 type = types.nullOr (types.submodule {
508 options = {
509 connectionString = mkOption {
510 type = types.str;
511 description = ''
512 Azure Blob Storage connection string.
513 '';
514 };
515 container = mkOption {
516 type = types.str;
517 description = ''
518 Azure Blob Storage container name.
519 It will be created if non-existent.
520 '';
521 };
522 };
523 });
524 default = null;
525 description = "Configure the azure third-party integration.";
526 };
527 oauth2 = mkOption {
528 type = types.nullOr (types.submodule {
529 options = {
530 authorizationURL = mkOption {
531 type = types.str;
532 description = ''
533 Specify the OAuth authorization URL.
534 '';
535 };
536 tokenURL = mkOption {
537 type = types.str;
538 description = ''
539 Specify the OAuth token URL.
540 '';
541 };
542 clientID = mkOption {
543 type = types.str;
544 description = ''
545 Specify the OAuth client ID.
546 '';
547 };
548 clientSecret = mkOption {
549 type = types.str;
550 description = ''
551 Specify the OAuth client secret.
552 '';
553 };
554 };
555 });
556 default = null;
557 description = "Configure the OAuth integration.";
558 };
559 facebook = mkOption {
560 type = types.nullOr (types.submodule {
561 options = {
562 clientID = mkOption {
563 type = types.str;
564 description = ''
565 Facebook API client ID.
566 '';
567 };
568 clientSecret = mkOption {
569 type = types.str;
570 description = ''
571 Facebook API client secret.
572 '';
573 };
574 };
575 });
576 default = null;
577 description = "Configure the facebook third-party integration";
578 };
579 twitter = mkOption {
580 type = types.nullOr (types.submodule {
581 options = {
582 consumerKey = mkOption {
583 type = types.str;
584 description = ''
585 Twitter API consumer key.
586 '';
587 };
588 consumerSecret = mkOption {
589 type = types.str;
590 description = ''
591 Twitter API consumer secret.
592 '';
593 };
594 };
595 });
596 default = null;
597 description = "Configure the Twitter third-party integration.";
598 };
599 github = mkOption {
600 type = types.nullOr (types.submodule {
601 options = {
602 clientID = mkOption {
603 type = types.str;
604 description = ''
605 GitHub API client ID.
606 '';
607 };
608 clientSecret = mkOption {
609 type = types.str;
610 description = ''
611 Github API client secret.
612 '';
613 };
614 };
615 });
616 default = null;
617 description = "Configure the GitHub third-party integration.";
618 };
619 gitlab = mkOption {
620 type = types.nullOr (types.submodule {
621 options = {
622 baseURL = mkOption {
623 type = types.str;
624 default = "";
625 description = ''
626 GitLab API authentication endpoint.
627 Only needed for other endpoints than gitlab.com.
628 '';
629 };
630 clientID = mkOption {
631 type = types.str;
632 description = ''
633 GitLab API client ID.
634 '';
635 };
636 clientSecret = mkOption {
637 type = types.str;
638 description = ''
639 GitLab API client secret.
640 '';
641 };
642 scope = mkOption {
643 type = types.enum [ "api" "read_user" ];
644 default = "api";
645 description = ''
646 GitLab API requested scope.
647 GitLab snippet import/export requires api scope.
648 '';
649 };
650 };
651 });
652 default = null;
653 description = "Configure the GitLab third-party integration.";
654 };
655 mattermost = mkOption {
656 type = types.nullOr (types.submodule {
657 options = {
658 baseURL = mkOption {
659 type = types.str;
660 description = ''
661 Mattermost authentication endpoint.
662 '';
663 };
664 clientID = mkOption {
665 type = types.str;
666 description = ''
667 Mattermost API client ID.
668 '';
669 };
670 clientSecret = mkOption {
671 type = types.str;
672 description = ''
673 Mattermost API client secret.
674 '';
675 };
676 };
677 });
678 default = null;
679 description = "Configure the Mattermost third-party integration.";
680 };
681 dropbox = mkOption {
682 type = types.nullOr (types.submodule {
683 options = {
684 clientID = mkOption {
685 type = types.str;
686 description = ''
687 Dropbox API client ID.
688 '';
689 };
690 clientSecret = mkOption {
691 type = types.str;
692 description = ''
693 Dropbox API client secret.
694 '';
695 };
696 appKey = mkOption {
697 type = types.str;
698 description = ''
699 Dropbox app key.
700 '';
701 };
702 };
703 });
704 default = null;
705 description = "Configure the Dropbox third-party integration.";
706 };
707 google = mkOption {
708 type = types.nullOr (types.submodule {
709 options = {
710 clientID = mkOption {
711 type = types.str;
712 description = ''
713 Google API client ID.
714 '';
715 };
716 clientSecret = mkOption {
717 type = types.str;
718 description = ''
719 Google API client secret.
720 '';
721 };
722 };
723 });
724 default = null;
725 description = "Configure the Google third-party integration.";
726 };
727 ldap = mkOption {
728 type = types.nullOr (types.submodule {
729 options = {
730 providerName = mkOption {
731 type = types.str;
732 default = "";
733 description = ''
734 Optional name to be displayed at login form, indicating the LDAP provider.
735 '';
736 };
737 url = mkOption {
738 type = types.str;
739 example = "ldap://localhost";
740 description = ''
741 URL of LDAP server.
742 '';
743 };
744 bindDn = mkOption {
745 type = types.str;
746 description = ''
747 Bind DN for LDAP access.
748 '';
749 };
750 bindCredentials = mkOption {
751 type = types.str;
752 description = ''
753 Bind credentials for LDAP access.
754 '';
755 };
756 searchBase = mkOption {
757 type = types.str;
758 example = "o=users,dc=example,dc=com";
759 description = ''
760 LDAP directory to begin search from.
761 '';
762 };
763 searchFilter = mkOption {
764 type = types.str;
765 example = "(uid={{username}})";
766 description = ''
767 LDAP filter to search with.
768 '';
769 };
770 searchAttributes = mkOption {
771 type = types.listOf types.str;
772 example = [ "displayName" "mail" ];
773 description = ''
774 LDAP attributes to search with.
775 '';
776 };
777 userNameField = mkOption {
778 type = types.str;
779 default = "";
780 description = ''
781 LDAP field which is used as the username on HedgeDoc.
782 By default <option>useridField</option> is used.
783 '';
784 };
785 useridField = mkOption {
786 type = types.str;
787 example = "uid";
788 description = ''
789 LDAP field which is a unique identifier for users on HedgeDoc.
790 '';
791 };
792 tlsca = mkOption {
793 type = types.str;
794 example = "server-cert.pem,root.pem";
795 description = ''
796 Root CA for LDAP TLS in PEM format.
797 '';
798 };
799 };
800 });
801 default = null;
802 description = "Configure the LDAP integration.";
803 };
804 saml = mkOption {
805 type = types.nullOr (types.submodule {
806 options = {
807 idpSsoUrl = mkOption {
808 type = types.str;
809 example = "https://idp.example.com/sso";
810 description = ''
811 IdP authentication endpoint.
812 '';
813 };
814 idpCert = mkOption {
815 type = types.path;
816 example = "/path/to/cert.pem";
817 description = ''
818 Path to IdP certificate file in PEM format.
819 '';
820 };
821 issuer = mkOption {
822 type = types.str;
823 default = "";
824 description = ''
825 Optional identity of the service provider.
826 This defaults to the server URL.
827 '';
828 };
829 identifierFormat = mkOption {
830 type = types.str;
831 default = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
832 description = ''
833 Optional name identifier format.
834 '';
835 };
836 groupAttribute = mkOption {
837 type = types.str;
838 default = "";
839 example = "memberOf";
840 description = ''
841 Optional attribute name for group list.
842 '';
843 };
844 externalGroups = mkOption {
845 type = types.listOf types.str;
846 default = [];
847 example = [ "Temporary-staff" "External-users" ];
848 description = ''
849 Excluded group names.
850 '';
851 };
852 requiredGroups = mkOption {
853 type = types.listOf types.str;
854 default = [];
855 example = [ "Hedgedoc-Users" ];
856 description = ''
857 Required group names.
858 '';
859 };
860 attribute = {
861 id = mkOption {
862 type = types.str;
863 default = "";
864 description = ''
865 Attribute map for `id'.
866 Defaults to `NameID' of SAML response.
867 '';
868 };
869 username = mkOption {
870 type = types.str;
871 default = "";
872 description = ''
873 Attribute map for `username'.
874 Defaults to `NameID' of SAML response.
875 '';
876 };
877 email = mkOption {
878 type = types.str;
879 default = "";
880 description = ''
881 Attribute map for `email'.
882 Defaults to `NameID' of SAML response if
883 <option>identifierFormat</option> has
884 the default value.
885 '';
886 };
887 };
888 };
889 });
890 default = null;
891 description = "Configure the SAML integration.";
892 };
893 };
894
895 environmentFile = mkOption {
896 type = with types; nullOr path;
897 default = null;
898 example = "/var/lib/hedgedoc/hedgedoc.env";
899 description = ''
900 Environment file as defined in <citerefentry>
901 <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
902 </citerefentry>.
903
904 Secrets may be passed to the service without adding them to the world-readable
905 Nix store, by specifying placeholder variables as the option value in Nix and
906 setting these variables accordingly in the environment file.
907
908 <programlisting>
909 # snippet of HedgeDoc-related config
910 services.hedgedoc.configuration.dbURL = "postgres://hedgedoc:\''${DB_PASSWORD}@db-host:5432/hedgedocdb";
911 services.hedgedoc.configuration.minio.secretKey = "$MINIO_SECRET_KEY";
912 </programlisting>
913
914 <programlisting>
915 # content of the environment file
916 DB_PASSWORD=verysecretdbpassword
917 MINIO_SECRET_KEY=verysecretminiokey
918 </programlisting>
919
920 Note that this file needs to be available on the host on which
921 <literal>HedgeDoc</literal> is running.
922 '';
923 };
924
925 package = mkOption {
926 type = types.package;
927 default = pkgs.hedgedoc;
928 description = ''
929 Package that provides HedgeDoc.
930 '';
931 };
932 };
933
934 config = mkIf cfg.enable {
935 assertions = [
936 { assertion = cfg.configuration.db == {} -> (
937 cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null
938 );
939 message = "Database configuration for HedgeDoc missing."; }
940 ];
941 users.groups.${name} = {};
942 users.users.${name} = {
943 description = "HedgeDoc service user";
944 group = name;
945 extraGroups = cfg.groups;
946 home = cfg.workDir;
947 createHome = true;
948 isSystemUser = true;
949 };
950
951 systemd.services.hedgedoc = {
952 description = "HedgeDoc Service";
953 wantedBy = [ "multi-user.target" ];
954 after = [ "networking.target" ];
955 preStart = ''
956 ${pkgs.envsubst}/bin/envsubst \
957 -o ${cfg.workDir}/config.json \
958 -i ${prettyJSON cfg.configuration}
959 '';
960 serviceConfig = {
961 WorkingDirectory = cfg.workDir;
962 ExecStart = "${cfg.package}/bin/hedgedoc";
963 EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
964 Environment = [
965 "CMD_CONFIG_FILE=${cfg.workDir}/config.json"
966 "NODE_ENV=production"
967 ];
968 Restart = "always";
969 User = name;
970 PrivateTmp = true;
971 };
972 };
973 };
974}