at master 11 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 utils, 6 ... 7}: 8let 9 inherit (lib) 10 getExe 11 mkDefault 12 mkEnableOption 13 mkIf 14 mkMerge 15 mkOption 16 mkPackageOption 17 optional 18 types 19 ; 20 21 cfgApi = config.services.ente.api; 22 cfgWeb = config.services.ente.web; 23 24 webPackage = 25 enteApp: 26 cfgWeb.package.override { 27 inherit enteApp; 28 enteMainUrl = "https://${cfgWeb.domains.photos}"; 29 extraBuildEnv = { 30 NEXT_PUBLIC_ENTE_ENDPOINT = "https://${cfgWeb.domains.api}"; 31 NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT = "https://${cfgWeb.domains.albums}"; 32 NEXT_TELEMETRY_DISABLED = "1"; 33 }; 34 }; 35 36 defaultUser = "ente"; 37 defaultGroup = "ente"; 38 dataDir = "/var/lib/ente"; 39 40 yamlFormat = pkgs.formats.yaml { }; 41in 42{ 43 options.services.ente = { 44 web = { 45 enable = mkEnableOption "Ente web frontend (Photos, Albums)"; 46 package = mkPackageOption pkgs "ente-web" { }; 47 48 domains = { 49 api = mkOption { 50 type = types.str; 51 example = "api.ente.example.com"; 52 description = '' 53 The domain under which the api is served. This will NOT serve the api itself, 54 but is a required setting to host the frontends! This will automatically be set 55 for you if you enable both the api server and web frontends. 56 ''; 57 }; 58 59 accounts = mkOption { 60 type = types.str; 61 example = "accounts.ente.example.com"; 62 description = "The domain under which the accounts frontend will be served."; 63 }; 64 65 cast = mkOption { 66 type = types.str; 67 example = "cast.ente.example.com"; 68 description = "The domain under which the cast frontend will be served."; 69 }; 70 71 albums = mkOption { 72 type = types.str; 73 example = "albums.ente.example.com"; 74 description = "The domain under which the albums frontend will be served."; 75 }; 76 77 photos = mkOption { 78 type = types.str; 79 example = "photos.ente.example.com"; 80 description = "The domain under which the photos frontend will be served."; 81 }; 82 }; 83 }; 84 85 api = { 86 enable = mkEnableOption "Museum (API server for ente.io)"; 87 package = mkPackageOption pkgs "museum" { }; 88 nginx.enable = mkEnableOption "nginx proxy for the API server"; 89 90 user = mkOption { 91 type = types.str; 92 default = defaultUser; 93 description = "User under which museum runs. If you set this option you must make sure the user exists."; 94 }; 95 96 group = mkOption { 97 type = types.str; 98 default = defaultGroup; 99 description = "Group under which museum runs. If you set this option you must make sure the group exists."; 100 }; 101 102 domain = mkOption { 103 type = types.str; 104 example = "api.ente.example.com"; 105 description = "The domain under which the api will be served."; 106 }; 107 108 enableLocalDB = mkEnableOption "the automatic creation of a local postgres database for museum."; 109 110 settings = mkOption { 111 description = '' 112 Museum yaml configuration. Refer to upstream [local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml) for more information. 113 You can specify secret values in this configuration by setting `somevalue._secret = "/path/to/file"` instead of setting `somevalue` directly. 114 ''; 115 default = { }; 116 type = types.submodule { 117 freeformType = yamlFormat.type; 118 options = { 119 apps = { 120 public-albums = mkOption { 121 type = types.str; 122 default = "https://albums.ente.io"; 123 description = '' 124 If you're running a self hosted instance and wish to serve public links, 125 set this to the URL where your albums web app is running. 126 ''; 127 }; 128 129 cast = mkOption { 130 type = types.str; 131 default = "https://cast.ente.io"; 132 description = '' 133 Set this to the URL where your cast page is running. 134 This is for browser and chromecast casting support. 135 ''; 136 }; 137 138 accounts = mkOption { 139 type = types.str; 140 default = "https://accounts.ente.io"; 141 description = '' 142 Set this to the URL where your accounts page is running. 143 This is primarily for passkey support. 144 ''; 145 }; 146 }; 147 148 db = { 149 host = mkOption { 150 type = types.str; 151 description = "The database host"; 152 }; 153 154 port = mkOption { 155 type = types.port; 156 default = 5432; 157 description = "The database port"; 158 }; 159 160 name = mkOption { 161 type = types.str; 162 description = "The database name"; 163 }; 164 165 user = mkOption { 166 type = types.str; 167 description = "The database user"; 168 }; 169 }; 170 }; 171 }; 172 }; 173 }; 174 }; 175 176 config = mkMerge [ 177 (mkIf cfgApi.enable { 178 services.postgresql = mkIf cfgApi.enableLocalDB { 179 enable = true; 180 ensureUsers = [ 181 { 182 name = "ente"; 183 ensureDBOwnership = true; 184 } 185 ]; 186 ensureDatabases = [ "ente" ]; 187 }; 188 189 services.ente.web.domains.api = mkIf cfgWeb.enable cfgApi.domain; 190 services.ente.api.settings = { 191 # This will cause logs to be written to stdout/err, which then end up in the journal 192 log-file = mkDefault ""; 193 db = mkIf cfgApi.enableLocalDB { 194 host = "/run/postgresql"; 195 port = 5432; 196 name = "ente"; 197 user = "ente"; 198 }; 199 }; 200 201 systemd.services.ente = { 202 description = "Ente.io Museum API Server"; 203 after = [ "network.target" ] ++ optional cfgApi.enableLocalDB "postgresql.service"; 204 requires = optional cfgApi.enableLocalDB "postgresql.service"; 205 wantedBy = [ "multi-user.target" ]; 206 207 preStart = '' 208 # Generate config including secret values. YAML is a superset of JSON, so we can use this here. 209 ${utils.genJqSecretsReplacementSnippet cfgApi.settings "/run/ente/local.yaml"} 210 211 # Setup paths 212 mkdir -p ${dataDir}/configurations 213 ln -sTf /run/ente/local.yaml ${dataDir}/configurations/local.yaml 214 ''; 215 216 serviceConfig = { 217 ExecStart = getExe cfgApi.package; 218 Type = "simple"; 219 Restart = "on-failure"; 220 221 AmbientCapabilities = [ ]; 222 CapabilityBoundingSet = [ ]; 223 LockPersonality = true; 224 MemoryDenyWriteExecute = true; 225 NoNewPrivileges = true; 226 PrivateMounts = true; 227 PrivateTmp = true; 228 PrivateUsers = false; 229 ProcSubset = "pid"; 230 ProtectClock = true; 231 ProtectControlGroups = true; 232 ProtectHome = true; 233 ProtectHostname = true; 234 ProtectKernelLogs = true; 235 ProtectKernelModules = true; 236 ProtectKernelTunables = true; 237 ProtectProc = "invisible"; 238 ProtectSystem = "strict"; 239 RestrictAddressFamilies = [ 240 "AF_INET" 241 "AF_INET6" 242 "AF_NETLINK" 243 "AF_UNIX" 244 ]; 245 RestrictNamespaces = true; 246 RestrictRealtime = true; 247 RestrictSUIDSGID = true; 248 SystemCallArchitectures = "native"; 249 SystemCallFilter = "@system-service"; 250 UMask = "077"; 251 252 BindReadOnlyPaths = [ 253 "${cfgApi.package}/share/museum/migrations:${dataDir}/migrations" 254 "${cfgApi.package}/share/museum/mail-templates:${dataDir}/mail-templates" 255 "${cfgApi.package}/share/museum/web-templates:${dataDir}/web-templates" 256 ]; 257 258 User = cfgApi.user; 259 Group = cfgApi.group; 260 261 SyslogIdentifier = "ente"; 262 StateDirectory = "ente"; 263 WorkingDirectory = dataDir; 264 RuntimeDirectory = "ente"; 265 }; 266 267 # Environment MUST be called local, otherwise we cannot log to stdout 268 environment = { 269 ENVIRONMENT = "local"; 270 GIN_MODE = "release"; 271 }; 272 }; 273 274 users = { 275 users = mkIf (cfgApi.user == defaultUser) { 276 ${defaultUser} = { 277 description = "ente.io museum service user"; 278 inherit (cfgApi) group; 279 isSystemUser = true; 280 home = dataDir; 281 }; 282 }; 283 groups = mkIf (cfgApi.group == defaultGroup) { ${defaultGroup} = { }; }; 284 }; 285 286 services.nginx = mkIf cfgApi.nginx.enable { 287 enable = true; 288 upstreams.museum = { 289 servers."localhost:8080" = { }; 290 extraConfig = '' 291 zone museum 64k; 292 keepalive 20; 293 ''; 294 }; 295 296 virtualHosts.${cfgApi.domain} = { 297 forceSSL = mkDefault true; 298 locations."/".proxyPass = "http://museum"; 299 extraConfig = '' 300 client_max_body_size 4M; 301 ''; 302 }; 303 }; 304 }) 305 (mkIf cfgWeb.enable { 306 services.ente.api.settings = mkIf cfgApi.enable { 307 apps = { 308 accounts = "https://${cfgWeb.domains.accounts}"; 309 cast = "https://${cfgWeb.domains.cast}"; 310 public-albums = "https://${cfgWeb.domains.albums}"; 311 }; 312 313 webauthn = { 314 rpid = cfgWeb.domains.accounts; 315 rporigins = [ "https://${cfgWeb.domains.accounts}" ]; 316 }; 317 }; 318 319 services.nginx = 320 let 321 domainFor = app: cfgWeb.domains.${app}; 322 in 323 { 324 enable = true; 325 virtualHosts.${domainFor "accounts"} = { 326 forceSSL = mkDefault true; 327 locations."/" = { 328 root = webPackage "accounts"; 329 tryFiles = "$uri $uri.html /index.html"; 330 extraConfig = '' 331 add_header Access-Control-Allow-Origin 'https://${cfgWeb.domains.api}'; 332 ''; 333 }; 334 }; 335 virtualHosts.${domainFor "cast"} = { 336 forceSSL = mkDefault true; 337 locations."/" = { 338 root = webPackage "cast"; 339 tryFiles = "$uri $uri.html /index.html"; 340 extraConfig = '' 341 add_header Access-Control-Allow-Origin 'https://${cfgWeb.domains.api}'; 342 ''; 343 }; 344 }; 345 virtualHosts.${domainFor "photos"} = { 346 serverAliases = [ 347 (domainFor "albums") # the albums app is shared with the photos frontend 348 ]; 349 forceSSL = mkDefault true; 350 locations."/" = { 351 root = webPackage "photos"; 352 tryFiles = "$uri $uri.html /index.html"; 353 extraConfig = '' 354 add_header Access-Control-Allow-Origin 'https://${cfgWeb.domains.api}'; 355 ''; 356 }; 357 }; 358 }; 359 }) 360 ]; 361 362 meta.maintainers = with lib.maintainers; [ oddlama ]; 363}