at 25.11-pre 15 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7with lib; 8let 9 cfg = config.services.monica; 10 monica = pkgs.monica.override { 11 dataDir = cfg.dataDir; 12 }; 13 db = cfg.database; 14 mail = cfg.mail; 15 16 user = cfg.user; 17 group = cfg.group; 18 19 php = lib.getExe cfg.phpPackage; 20 21 # shell script for local administration 22 artisan = pkgs.writeScriptBin "monica" '' 23 #! ${pkgs.runtimeShell} 24 cd ${monica} 25 sudo() { 26 if [[ "$USER" != ${user} ]]; then 27 exec /run/wrappers/bin/sudo -u ${user} "$@" 28 else 29 exec "$@" 30 fi 31 } 32 sudo ${php} artisan "$@" 33 ''; 34 35 tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME; 36in 37{ 38 options.services.monica = { 39 enable = mkEnableOption "monica"; 40 41 phpPackage = mkPackageOption pkgs "php83" { }; 42 43 user = mkOption { 44 default = "monica"; 45 description = "User monica runs as."; 46 type = types.str; 47 }; 48 49 group = mkOption { 50 default = "monica"; 51 description = "Group monica runs as."; 52 type = types.str; 53 }; 54 55 appKeyFile = mkOption { 56 description = '' 57 A file containing the Laravel APP_KEY - a 32 character long, 58 base64 encoded key used for encryption where needed. Can be 59 generated with <code>head -c 32 /dev/urandom | base64</code>. 60 ''; 61 example = "/run/keys/monica-appkey"; 62 type = types.path; 63 }; 64 65 hostname = lib.mkOption { 66 type = lib.types.str; 67 default = 68 if config.networking.domain != null then config.networking.fqdn else config.networking.hostName; 69 defaultText = lib.literalExpression "config.networking.fqdn"; 70 example = "monica.example.com"; 71 description = '' 72 The hostname to serve monica on. 73 ''; 74 }; 75 76 appURL = mkOption { 77 description = '' 78 The root URL that you want to host monica on. All URLs in monica will be generated using this value. 79 If you change this in the future you may need to run a command to update stored URLs in the database. 80 Command example: <code>php artisan monica:update-url https://old.example.com https://new.example.com</code> 81 ''; 82 default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}"; 83 defaultText = ''http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostname}''; 84 example = "https://example.com"; 85 type = types.str; 86 }; 87 88 dataDir = mkOption { 89 description = "monica data directory"; 90 default = "/var/lib/monica"; 91 type = types.path; 92 }; 93 94 database = { 95 host = mkOption { 96 type = types.str; 97 default = "localhost"; 98 description = "Database host address."; 99 }; 100 port = mkOption { 101 type = types.port; 102 default = 3306; 103 description = "Database host port."; 104 }; 105 name = mkOption { 106 type = types.str; 107 default = "monica"; 108 description = "Database name."; 109 }; 110 user = mkOption { 111 type = types.str; 112 default = user; 113 defaultText = lib.literalExpression "user"; 114 description = "Database username."; 115 }; 116 passwordFile = mkOption { 117 type = with types; nullOr path; 118 default = null; 119 example = "/run/keys/monica-dbpassword"; 120 description = '' 121 A file containing the password corresponding to 122 <option>database.user</option>. 123 ''; 124 }; 125 createLocally = mkOption { 126 type = types.bool; 127 default = true; 128 description = "Create the database and database user locally."; 129 }; 130 }; 131 132 mail = { 133 driver = mkOption { 134 type = types.enum [ 135 "smtp" 136 "sendmail" 137 ]; 138 default = "smtp"; 139 description = "Mail driver to use."; 140 }; 141 host = mkOption { 142 type = types.str; 143 default = "localhost"; 144 description = "Mail host address."; 145 }; 146 port = mkOption { 147 type = types.port; 148 default = 1025; 149 description = "Mail host port."; 150 }; 151 fromName = mkOption { 152 type = types.str; 153 default = "monica"; 154 description = "Mail \"from\" name."; 155 }; 156 from = mkOption { 157 type = types.str; 158 default = "mail@monica.com"; 159 description = "Mail \"from\" email."; 160 }; 161 user = mkOption { 162 type = with types; nullOr str; 163 default = null; 164 example = "monica"; 165 description = "Mail username."; 166 }; 167 passwordFile = mkOption { 168 type = with types; nullOr path; 169 default = null; 170 example = "/run/keys/monica-mailpassword"; 171 description = '' 172 A file containing the password corresponding to 173 <option>mail.user</option>. 174 ''; 175 }; 176 encryption = mkOption { 177 type = with types; nullOr (enum [ "tls" ]); 178 default = null; 179 description = "SMTP encryption mechanism to use."; 180 }; 181 }; 182 183 maxUploadSize = mkOption { 184 type = types.str; 185 default = "18M"; 186 example = "1G"; 187 description = "The maximum size for uploads (e.g. images)."; 188 }; 189 190 poolConfig = mkOption { 191 type = 192 with types; 193 attrsOf (oneOf [ 194 str 195 int 196 bool 197 ]); 198 default = { 199 "pm" = "dynamic"; 200 "pm.max_children" = 32; 201 "pm.start_servers" = 2; 202 "pm.min_spare_servers" = 2; 203 "pm.max_spare_servers" = 4; 204 "pm.max_requests" = 500; 205 }; 206 description = '' 207 Options for the monica PHP pool. See the documentation on <literal>php-fpm.conf</literal> 208 for details on configuration directives. 209 ''; 210 }; 211 212 nginx = mkOption { 213 type = types.submodule ( 214 recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { } 215 ); 216 default = { }; 217 example = '' 218 { 219 serverAliases = [ 220 "monica.''${config.networking.domain}" 221 ]; 222 # To enable encryption and let let's encrypt take care of certificate 223 forceSSL = true; 224 enableACME = true; 225 } 226 ''; 227 description = '' 228 With this option, you can customize the nginx virtualHost settings. 229 ''; 230 }; 231 232 config = mkOption { 233 type = 234 with types; 235 attrsOf ( 236 nullOr ( 237 either 238 (oneOf [ 239 bool 240 int 241 port 242 path 243 str 244 ]) 245 (submodule { 246 options = { 247 _secret = mkOption { 248 type = nullOr str; 249 description = '' 250 The path to a file containing the value the 251 option should be set to in the final 252 configuration file. 253 ''; 254 }; 255 }; 256 }) 257 ) 258 ); 259 default = { }; 260 example = '' 261 { 262 ALLOWED_IFRAME_HOSTS = "https://example.com"; 263 WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf"; 264 AUTH_METHOD = "oidc"; 265 OIDC_NAME = "MyLogin"; 266 OIDC_DISPLAY_NAME_CLAIMS = "name"; 267 OIDC_CLIENT_ID = "monica"; 268 OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"}; 269 OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm"; 270 OIDC_ISSUER_DISCOVER = true; 271 } 272 ''; 273 description = '' 274 monica configuration options to set in the 275 <filename>.env</filename> file. 276 277 Refer to <link xlink:href="https://github.com/monicahq/monica"/> 278 for details on supported values. 279 280 Settings containing secret data should be set to an attribute 281 set containing the attribute <literal>_secret</literal> - a 282 string pointing to a file containing the value the option 283 should be set to. See the example to get a better picture of 284 this: in the resulting <filename>.env</filename> file, the 285 <literal>OIDC_CLIENT_SECRET</literal> key will be set to the 286 contents of the <filename>/run/keys/oidc_secret</filename> 287 file. 288 ''; 289 }; 290 }; 291 292 config = mkIf cfg.enable { 293 assertions = [ 294 { 295 assertion = db.createLocally -> db.user == user; 296 message = "services.monica.database.user must be set to ${user} if services.monica.database.createLocally is set true."; 297 } 298 { 299 assertion = db.createLocally -> db.passwordFile == null; 300 message = "services.monica.database.passwordFile cannot be specified if services.monica.database.createLocally is set to true."; 301 } 302 ]; 303 304 services.monica.config = { 305 APP_ENV = "production"; 306 APP_KEY._secret = cfg.appKeyFile; 307 APP_URL = cfg.appURL; 308 DB_HOST = db.host; 309 DB_PORT = db.port; 310 DB_DATABASE = db.name; 311 DB_USERNAME = db.user; 312 MAIL_DRIVER = mail.driver; 313 MAIL_FROM_NAME = mail.fromName; 314 MAIL_FROM_ADDRESS = mail.from; 315 MAIL_HOST = mail.host; 316 MAIL_PORT = mail.port; 317 MAIL_USERNAME = mail.user; 318 MAIL_ENCRYPTION = mail.encryption; 319 DB_PASSWORD._secret = db.passwordFile; 320 MAIL_PASSWORD._secret = mail.passwordFile; 321 APP_SERVICES_CACHE = "/run/monica/cache/services.php"; 322 APP_PACKAGES_CACHE = "/run/monica/cache/packages.php"; 323 APP_CONFIG_CACHE = "/run/monica/cache/config.php"; 324 APP_ROUTES_CACHE = "/run/monica/cache/routes-v7.php"; 325 APP_EVENTS_CACHE = "/run/monica/cache/events.php"; 326 SESSION_SECURE_COOKIE = tlsEnabled; 327 }; 328 329 environment.systemPackages = [ artisan ]; 330 331 services.mysql = mkIf db.createLocally { 332 enable = true; 333 package = mkDefault pkgs.mariadb; 334 ensureDatabases = [ db.name ]; 335 ensureUsers = [ 336 { 337 name = db.user; 338 ensurePermissions = { 339 "${db.name}.*" = "ALL PRIVILEGES"; 340 }; 341 } 342 ]; 343 }; 344 345 services.phpfpm.pools.monica = { 346 inherit user group; 347 phpPackage = cfg.phpPackage; 348 phpOptions = '' 349 log_errors = on 350 post_max_size = ${cfg.maxUploadSize} 351 upload_max_filesize = ${cfg.maxUploadSize} 352 ''; 353 settings = { 354 "listen.mode" = "0660"; 355 "listen.owner" = user; 356 "listen.group" = group; 357 } // cfg.poolConfig; 358 }; 359 360 services.nginx = { 361 enable = mkDefault true; 362 recommendedTlsSettings = true; 363 recommendedOptimisation = true; 364 recommendedGzipSettings = true; 365 recommendedBrotliSettings = true; 366 recommendedProxySettings = true; 367 virtualHosts.${cfg.hostname} = mkMerge [ 368 cfg.nginx 369 { 370 root = mkForce "${monica}/public"; 371 locations = { 372 "/" = { 373 index = "index.php"; 374 tryFiles = "$uri $uri/ /index.php?$query_string"; 375 }; 376 "~ \\.php$".extraConfig = '' 377 fastcgi_pass unix:${config.services.phpfpm.pools."monica".socket}; 378 ''; 379 "~ \\.(js|css|gif|png|ico|jpg|jpeg)$" = { 380 extraConfig = "expires 365d;"; 381 }; 382 }; 383 } 384 ]; 385 }; 386 387 systemd.services.monica-setup = { 388 description = "Preparation tasks for monica"; 389 before = [ "phpfpm-monica.service" ]; 390 after = optional db.createLocally "mysql.service"; 391 wantedBy = [ "multi-user.target" ]; 392 serviceConfig = { 393 Type = "oneshot"; 394 RemainAfterExit = true; 395 User = user; 396 UMask = 77; 397 WorkingDirectory = "${monica}"; 398 RuntimeDirectory = "monica/cache"; 399 RuntimeDirectoryMode = 700; 400 }; 401 path = [ pkgs.replace-secret ]; 402 script = 403 let 404 isSecret = v: isAttrs v && v ? _secret && isString v._secret; 405 monicaEnvVars = lib.generators.toKeyValue { 406 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" { 407 mkValueString = 408 v: 409 with builtins; 410 if isInt v then 411 toString v 412 else if isString v then 413 v 414 else if true == v then 415 "true" 416 else if false == v then 417 "false" 418 else if isSecret v then 419 hashString "sha256" v._secret 420 else 421 throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty { }) v}"; 422 }; 423 }; 424 secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config); 425 mkSecretReplacement = file: '' 426 replace-secret ${ 427 escapeShellArgs [ 428 (builtins.hashString "sha256" file) 429 file 430 "${cfg.dataDir}/.env" 431 ] 432 } 433 ''; 434 secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; 435 filteredConfig = lib.converge (lib.filterAttrsRecursive ( 436 _: v: 437 !elem v [ 438 { } 439 null 440 ] 441 )) cfg.config; 442 monicaEnv = pkgs.writeText "monica.env" (monicaEnvVars filteredConfig); 443 in 444 '' 445 # error handling 446 set -euo pipefail 447 448 # create .env file 449 install -T -m 0600 -o ${user} ${monicaEnv} "${cfg.dataDir}/.env" 450 ${secretReplacements} 451 if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then 452 sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env" 453 fi 454 455 # migrate & seed db 456 ${php} artisan key:generate --force 457 ${php} artisan setup:production -v --force 458 ''; 459 }; 460 461 systemd.services.monica-scheduler = { 462 description = "Background tasks for monica"; 463 startAt = "minutely"; 464 after = [ "monica-setup.service" ]; 465 serviceConfig = { 466 Type = "oneshot"; 467 User = user; 468 WorkingDirectory = "${monica}"; 469 ExecStart = "${php} ${monica}/artisan schedule:run -v"; 470 }; 471 }; 472 473 systemd.tmpfiles.rules = [ 474 "d ${cfg.dataDir} 0710 ${user} ${group} - -" 475 "d ${cfg.dataDir}/public 0750 ${user} ${group} - -" 476 "d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -" 477 "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" 478 "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" 479 "d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -" 480 "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" 481 "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" 482 "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" 483 "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" 484 "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" 485 "d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -" 486 ]; 487 488 users = { 489 users = mkIf (user == "monica") { 490 monica = { 491 inherit group; 492 isSystemUser = true; 493 }; 494 "${config.services.nginx.user}".extraGroups = [ group ]; 495 }; 496 groups = mkIf (group == "monica") { 497 monica = { }; 498 }; 499 }; 500 }; 501}