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