diff --git a/configs/sputnik/nginx.nix b/configs/sputnik/nginx.nix index de5ae9e..e382554 100644 --- a/configs/sputnik/nginx.nix +++ b/configs/sputnik/nginx.nix @@ -147,14 +147,25 @@ ]; forceSSL = true; enableACME = true; - locations."/" = { - proxyPass = "http://nextcloud.workhorse.private"; - extraConfig = '' - sub_filter "http://nextcloud.ingolf-wagner.de" "https://nextcloud.ingolf-wagner.de"; - sub_filter "nextcloud.workhorse.private" "nextcloud.ingolf-wagner.de"; - # used for view/edit office file via Office Online Server - client_max_body_size 0; - ''; + locations = { + "/" = { + proxyPass = "http://nextcloud.workhorse.private"; + extraConfig = '' + sub_filter "http://nextcloud.ingolf-wagner.de" "https://nextcloud.ingolf-wagner.de"; + sub_filter "nextcloud.workhorse.private" "nextcloud.ingolf-wagner.de"; + # used for view/edit office file via Office Online Server + client_max_body_size 0; + ''; + }; + "= /.well-known/carddav" = { + priority = 210; + extraConfig = "return 301 $scheme://$host/remote.php/dav;"; + }; + "= /.well-known/caldav" = { + priority = 210; + extraConfig = "return 301 $scheme://$host/remote.php/dav;"; + }; + }; }; diff --git a/configs/workhorse/nextcloud.nix b/configs/workhorse/nextcloud.nix index d983e5b..f16a143 100644 --- a/configs/workhorse/nextcloud.nix +++ b/configs/workhorse/nextcloud.nix @@ -13,6 +13,11 @@ mountPoint = "/var/lib/nextcloud"; isReadOnly = false; }; + modules = { + mountPoint = toString ; + hostPath = toString ; + isReadOnly = true; + }; }; privateNetwork = true; @@ -23,6 +28,8 @@ config = { config, pkgs, ... }: { + imports = [ ]; + # don't forget the database backup before doing this # https://docs.nextcloud.com/server/stable/admin_manual/maintenance/backup.html # https://docs.nextcloud.com/server/stable/admin_manual/maintenance/upgrade.html @@ -42,7 +49,7 @@ networking.firewall.allowedTCPPorts = [ 80 ]; networking.firewall.allowedUDPPorts = [ 80 ]; - services.nextcloud = { + later.services.nextcloud = { enable = true; autoUpdateApps.enable = true; config.adminpassFile = toString ; @@ -50,11 +57,7 @@ hostName = "nextcloud.ingolf-wagner.de"; #logLevel = 0; config.overwriteProtocol = "https"; - config.extraTrustedDomains = [ - "nextcloud.ingolf-wagner.de" - #"nextcloud.gaykraft.com" - "192.168.100.11" - ]; + config.trustedProxies = [ "195.201.134.247" "192.168.100.11" ]; }; environment.systemPackages = [ pkgs.smbclient ]; diff --git a/modules/default.nix b/modules/default.nix index d1a8c03..fcc351d 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -3,6 +3,7 @@ imports = [ ./later/syncthing.nix + ./later/nextcloud.nix ./services/castget.nix ./services/home-assistant.nix diff --git a/modules/later/nextcloud.nix b/modules/later/nextcloud.nix new file mode 100644 index 0000000..0c6bff2 --- /dev/null +++ b/modules/later/nextcloud.nix @@ -0,0 +1,624 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.later.services.nextcloud; + fpm = config.services.phpfpm.pools.nextcloud; + + phpPackage = pkgs.php73; + phpPackages = pkgs.php73Packages; + + toKeyValue = generators.toKeyValue { + mkKeyValue = generators.mkKeyValueDefault { } " = "; + }; + + phpOptionsExtensions = '' + ${optionalString cfg.caching.apcu + "extension=${phpPackages.apcu}/lib/php/extensions/apcu.so"} + ${optionalString cfg.caching.redis + "extension=${phpPackages.redis}/lib/php/extensions/redis.so"} + ${optionalString cfg.caching.memcached + "extension=${phpPackages.memcached}/lib/php/extensions/memcached.so"} + extension=${phpPackages.imagick}/lib/php/extensions/imagick.so + zend_extension = opcache.so + opcache.enable = 1 + ''; + phpOptions = { + upload_max_filesize = cfg.maxUploadSize; + post_max_size = cfg.maxUploadSize; + memory_limit = cfg.maxUploadSize; + } // cfg.phpOptions; + phpOptionsStr = phpOptionsExtensions + (toKeyValue phpOptions); + + occ = pkgs.writeScriptBin "nextcloud-occ" '' + #! ${pkgs.stdenv.shell} + cd ${pkgs.nextcloud} + sudo=exec + if [[ "$USER" != nextcloud ]]; then + sudo='exec /run/wrappers/bin/sudo -u nextcloud --preserve-env=NEXTCLOUD_CONFIG_DIR' + fi + export NEXTCLOUD_CONFIG_DIR="${cfg.home}/config" + $sudo \ + ${phpPackage}/bin/php \ + -c ${pkgs.writeText "php.ini" phpOptionsStr}\ + occ $* + ''; + +in { + options.later.services.nextcloud = { + enable = mkEnableOption "nextcloud"; + hostName = mkOption { + type = types.str; + description = "FQDN for the nextcloud instance."; + }; + home = mkOption { + type = types.str; + default = "/var/lib/nextcloud"; + description = "Storage path of nextcloud."; + }; + logLevel = mkOption { + type = types.ints.between 0 4; + default = 2; + description = "Log level value between 0 (DEBUG) and 4 (FATAL)."; + }; + https = mkOption { + type = types.bool; + default = false; + description = "Use https for generated links."; + }; + + maxUploadSize = mkOption { + default = "512M"; + type = types.str; + description = '' + Defines the upload limit for files. This changes the relevant options + in php.ini and nginx if enabled. + ''; + }; + + skeletonDirectory = mkOption { + default = ""; + type = types.str; + description = '' + The directory where the skeleton files are located. These files will be + copied to the data directory of new users. Leave empty to not copy any + skeleton files. + ''; + }; + + nginx.enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable nginx virtual host management. + Further nginx configuration can be done by adapting services.nginx.virtualHosts.<name>. + See for further information. + ''; + }; + + webfinger = mkOption { + type = types.bool; + default = false; + description = '' + Enable this option if you plan on using the webfinger plugin. + The appropriate nginx rewrite rules will be added to your configuration. + ''; + }; + + phpOptions = mkOption { + type = types.attrsOf types.str; + default = { + short_open_tag = "Off"; + expose_php = "Off"; + error_reporting = "E_ALL & ~E_DEPRECATED & ~E_STRICT"; + display_errors = "stderr"; + "opcache.enable_cli" = "1"; + "opcache.interned_strings_buffer" = "8"; + "opcache.max_accelerated_files" = "10000"; + "opcache.memory_consumption" = "128"; + "opcache.revalidate_freq" = "1"; + "opcache.fast_shutdown" = "1"; + "openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt"; + catch_workers_output = "yes"; + }; + description = '' + Options for PHP's php.ini file for nextcloud. + ''; + }; + + poolSettings = mkOption { + type = with types; attrsOf (oneOf [ str int bool ]); + default = { + "pm" = "dynamic"; + "pm.max_children" = "32"; + "pm.start_servers" = "2"; + "pm.min_spare_servers" = "2"; + "pm.max_spare_servers" = "4"; + "pm.max_requests" = "500"; + }; + description = '' + Options for nextcloud's PHP pool. See the documentation on php-fpm.conf for details on configuration directives. + ''; + }; + + poolConfig = mkOption { + type = types.nullOr types.lines; + default = null; + description = '' + Options for nextcloud's PHP pool. See the documentation on php-fpm.conf for details on configuration directives. + ''; + }; + + config = { + dbtype = mkOption { + type = types.enum [ "sqlite" "pgsql" "mysql" ]; + default = "sqlite"; + description = "Database type."; + }; + dbname = mkOption { + type = types.nullOr types.str; + default = "nextcloud"; + description = "Database name."; + }; + dbuser = mkOption { + type = types.nullOr types.str; + default = "nextcloud"; + description = "Database user."; + }; + dbpass = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Database password. Use dbpassFile to avoid this + being world-readable in the /nix/store. + ''; + }; + dbpassFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The full path to a file that contains the database password. + ''; + }; + dbhost = mkOption { + type = types.nullOr types.str; + default = "localhost"; + description = '' + Database host. + + Note: for using Unix authentication with PostgreSQL, this should be + set to /run/postgresql. + ''; + }; + dbport = mkOption { + type = with types; nullOr (either int str); + default = null; + description = "Database port."; + }; + dbtableprefix = mkOption { + type = types.nullOr types.str; + default = null; + description = "Table prefix in Nextcloud database."; + }; + adminuser = mkOption { + type = types.str; + default = "root"; + description = "Admin username."; + }; + adminpass = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Admin password. Use adminpassFile to avoid this + being world-readable in the /nix/store. + ''; + }; + adminpassFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The full path to a file that contains the admin's password. + ''; + }; + + extraTrustedDomains = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Trusted domains, from which the nextcloud installation will be + acessible. You don't need to add + services.nextcloud.hostname here. + ''; + }; + + trustedProxies = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Trusted proxies, to provide if the nextcloud installation is being + proxied to secure against e.g. spoofing. + ''; + }; + + overwriteProtocol = mkOption { + type = types.nullOr (types.enum [ "http" "https" ]); + default = null; + example = "https"; + + description = '' + Force Nextcloud to always use HTTPS i.e. for link generation. Nextcloud + uses the currently used protocol by default, but when behind a reverse-proxy, + it may use http for everything although Nextcloud + may be served via HTTPS. + ''; + }; + }; + + caching = { + apcu = mkOption { + type = types.bool; + default = true; + description = '' + Whether to load the APCu module into PHP. + ''; + }; + redis = mkOption { + type = types.bool; + default = false; + description = '' + Whether to load the Redis module into PHP. + You still need to enable Redis in your config.php. + See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html + ''; + }; + memcached = mkOption { + type = types.bool; + default = false; + description = '' + Whether to load the Memcached module into PHP. + You still need to enable Memcached in your config.php. + See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html + ''; + }; + }; + autoUpdateApps = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Run regular auto update of all apps installed from the nextcloud app store. + ''; + }; + startAt = mkOption { + type = with types; either str (listOf str); + default = "05:00:00"; + example = "Sun 14:00:00"; + description = '' + When to run the update. See `systemd.services.<name>.startAt`. + ''; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + assertions = let acfg = cfg.config; + in [ + { + assertion = !(acfg.dbpass != null && acfg.dbpassFile != null); + message = "Please specify no more than one of dbpass or dbpassFile"; + } + { + assertion = ((acfg.adminpass != null || acfg.adminpassFile != null) + && !(acfg.adminpass != null && acfg.adminpassFile != null)); + message = "Please specify exactly one of adminpass or adminpassFile"; + } + ]; + + warnings = optional (cfg.poolConfig != null) '' + Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release. + Please migrate your configuration to config.services.nextcloud.poolSettings. + ''; + } + + { + systemd.timers.nextcloud-cron = { + wantedBy = [ "timers.target" ]; + timerConfig.OnBootSec = "5m"; + timerConfig.OnUnitActiveSec = "15m"; + timerConfig.Unit = "nextcloud-cron.service"; + }; + + systemd.services = { + nextcloud-setup = let + c = cfg.config; + writePhpArrary = a: + "[${concatMapStringsSep "," (val: ''"${toString val}"'') a}]"; + overrideConfig = pkgs.writeText "nextcloud-config.php" '' + [ + [ 'path' => '${cfg.home}/apps', 'url' => '/apps', 'writable' => false ], + [ 'path' => '${cfg.home}/store-apps', 'url' => '/store-apps', 'writable' => true ], + ], + 'datadirectory' => '${cfg.home}/data', + 'skeletondirectory' => '${cfg.skeletonDirectory}', + ${ + optionalString cfg.caching.apcu + "'memcache.local' => '\\OC\\Memcache\\APCu'," + } + 'log_type' => 'syslog', + 'log_level' => '${builtins.toString cfg.logLevel}', + ${ + optionalString (c.overwriteProtocol != null) + "'overwriteprotocol' => '${c.overwriteProtocol}'," + } + ${optionalString (c.dbname != null) "'dbname' => '${c.dbname}',"} + ${optionalString (c.dbhost != null) "'dbhost' => '${c.dbhost}',"} + ${ + optionalString (c.dbport != null) + "'dbport' => '${toString c.dbport}'," + } + ${optionalString (c.dbuser != null) "'dbuser' => '${c.dbuser}',"} + ${ + optionalString (c.dbtableprefix != null) + "'dbtableprefix' => '${toString c.dbtableprefix}'," + } + ${ + optionalString (c.dbpass != null) + "'dbpassword' => '${c.dbpass}'," + } + ${ + optionalString (c.dbpassFile != null) + "'dbpassword' => nix_read_pwd()," + } + 'dbtype' => '${c.dbtype}', + 'trusted_domains' => ${ + writePhpArrary ([ cfg.hostName ] ++ c.extraTrustedDomains) + }, + 'trusted_proxies' => ${writePhpArrary (c.trustedProxies)}, + ]; + ''; + occInstallCmd = let + dbpass = if c.dbpassFile != null then + ''"$(<"${toString c.dbpassFile}")"'' + else if c.dbpass != null then + ''"${toString c.dbpass}"'' + else + null; + adminpass = if c.adminpassFile != null then + ''"$(<"${toString c.adminpassFile}")"'' + else + ''"${toString c.adminpass}"''; + installFlags = concatStringsSep " \\\n " + (mapAttrsToList (k: v: "${k} ${toString v}") { + "--database" = ''"${c.dbtype}"''; + # The following attributes are optional depending on the type of + # database. Those that evaluate to null on the left hand side + # will be omitted. + ${if c.dbname != null then "--database-name" else null} = + ''"${c.dbname}"''; + ${if c.dbhost != null then "--database-host" else null} = + ''"${c.dbhost}"''; + ${if c.dbport != null then "--database-port" else null} = + ''"${toString c.dbport}"''; + ${if c.dbuser != null then "--database-user" else null} = + ''"${c.dbuser}"''; + ${ + if (any (x: x != null) [ c.dbpass c.dbpassFile ]) then + "--database-pass" + else + null + } = dbpass; + ${ + if c.dbtableprefix != null then + "--database-table-prefix" + else + null + } = ''"${toString c.dbtableprefix}"''; + "--admin-user" = ''"${c.adminuser}"''; + "--admin-pass" = adminpass; + "--data-dir" = ''"${cfg.home}/data"''; + }); + in '' + ${occ}/bin/nextcloud-occ maintenance:install \ + ${installFlags} + ''; + occSetTrustedDomainsCmd = concatStringsSep "\n" (imap0 (i: v: '' + ${occ}/bin/nextcloud-occ config:system:set trusted_domains \ + ${toString i} --value="${toString v}" + '') ([ cfg.hostName ] ++ cfg.config.extraTrustedDomains)); + + in { + wantedBy = [ "multi-user.target" ]; + before = [ "phpfpm-nextcloud.service" ]; + path = [ occ ]; + script = '' + chmod og+x ${cfg.home} + ln -sf ${pkgs.nextcloud}/apps ${cfg.home}/ + mkdir -p ${cfg.home}/config ${cfg.home}/data ${cfg.home}/store-apps + ln -sf ${overrideConfig} ${cfg.home}/config/override.config.php + + chown -R nextcloud:nginx ${cfg.home}/config ${cfg.home}/data ${cfg.home}/store-apps + + # Do not install if already installed + if [[ ! -e ${cfg.home}/config/config.php ]]; then + ${occInstallCmd} + fi + + ${occ}/bin/nextcloud-occ upgrade + + ${occ}/bin/nextcloud-occ config:system:delete trusted_domains + ${occSetTrustedDomainsCmd} + ''; + serviceConfig.Type = "oneshot"; + }; + nextcloud-cron = { + environment.NEXTCLOUD_CONFIG_DIR = "${cfg.home}/config"; + serviceConfig.Type = "oneshot"; + serviceConfig.User = "nextcloud"; + serviceConfig.ExecStart = + "${phpPackage}/bin/php -f ${pkgs.nextcloud}/cron.php"; + }; + nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable { + serviceConfig.Type = "oneshot"; + serviceConfig.ExecStart = "${occ}/bin/nextcloud-occ app:update --all"; + serviceConfig.User = "nextcloud"; + startAt = cfg.autoUpdateApps.startAt; + }; + }; + + services.phpfpm = { + pools.nextcloud = { + user = "nextcloud"; + group = "nginx"; + phpOptions = phpOptionsStr; + phpPackage = phpPackage; + phpEnv = { + NEXTCLOUD_CONFIG_DIR = "${cfg.home}/config"; + PATH = + "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin"; + }; + settings = mapAttrs (name: mkDefault) { + "listen.owner" = "nginx"; + "listen.group" = "nginx"; + } // cfg.poolSettings; + extraConfig = cfg.poolConfig; + }; + }; + + users.extraUsers.nextcloud = { + home = "${cfg.home}"; + group = "nginx"; + createHome = true; + }; + + environment.systemPackages = [ occ ]; + } + + (mkIf cfg.nginx.enable { + services.nginx = { + enable = true; + virtualHosts = { + ${cfg.hostName} = { + root = pkgs.nextcloud; + locations = { + "= /robots.txt" = { + priority = 100; + extraConfig = '' + allow all; + log_not_found off; + access_log off; + ''; + }; + "/" = { + priority = 200; + extraConfig = "rewrite ^ /index.php;"; + }; + "~ ^/store-apps" = { + priority = 201; + extraConfig = "root ${cfg.home};"; + }; + "= /.well-known/carddav" = { + priority = 210; + extraConfig = "return 301 $scheme://$host/remote.php/dav;"; + }; + "= /.well-known/caldav" = { + priority = 210; + extraConfig = "return 301 $scheme://$host/remote.php/dav;"; + }; + "~ ^\\/(?:build|tests|config|lib|3rdparty|templates|data)\\/" = { + priority = 300; + extraConfig = "deny all;"; + }; + "~ ^\\/(?:\\.|autotest|occ|issue|indie|db_|console)" = { + priority = 300; + extraConfig = "deny all;"; + }; + "~ ^\\/(?:index|remote|public|cron|core/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|ocs-provider\\/.+|ocm-provider\\/.+)\\.php(?:$|\\/)" = + { + priority = 500; + extraConfig = '' + include ${config.services.nginx.package}/conf/fastcgi.conf; + fastcgi_split_path_info ^(.+\.php)(\\/.*)$; + try_files $fastcgi_script_name =404; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTPS ${if cfg.https then "on" else "off"}; + fastcgi_param modHeadersAvailable true; + fastcgi_param front_controller_active true; + fastcgi_pass unix:${fpm.socket}; + fastcgi_intercept_errors on; + fastcgi_request_buffering off; + fastcgi_read_timeout 120s; + ''; + }; + "~ ^\\/(?:updater|ocs-provider|ocm-provider)(?:$|\\/)".extraConfig = + '' + try_files $uri/ =404; + index index.php; + ''; + "~ \\.(?:css|js|woff2?|svg|gif)$".extraConfig = '' + try_files $uri /index.php$request_uri; + add_header Cache-Control "public, max-age=15778463"; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Robots-Tag none; + add_header X-Download-Options noopen; + add_header X-Permitted-Cross-Domain-Policies none; + add_header X-Frame-Options sameorigin; + add_header Referrer-Policy no-referrer; + access_log off; + ''; + "~ \\.(?:png|html|ttf|ico|jpg|jpeg)$".extraConfig = '' + try_files $uri /index.php$request_uri; + access_log off; + ''; + }; + extraConfig = '' + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Robots-Tag none; + add_header X-Download-Options noopen; + add_header X-Permitted-Cross-Domain-Policies none; + add_header X-Frame-Options sameorigin; + add_header Referrer-Policy no-referrer; + add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; + error_page 403 /core/templates/403.php; + error_page 404 /core/templates/404.php; + client_max_body_size ${cfg.maxUploadSize}; + fastcgi_buffers 64 4K; + fastcgi_hide_header X-Powered-By; + gzip on; + gzip_vary on; + gzip_comp_level 4; + gzip_min_length 256; + gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; + gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; + + ${optionalString cfg.webfinger '' + rewrite ^/.well-known/host-meta /public.php?service=host-meta last; + rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last; + ''} + ''; + }; + }; + }; + }) + ]); + + # meta.doc = ./nextcloud.xml; +} diff --git a/system/all/nginx.nix b/system/all/nginx.nix index c7cf959..dc7c124 100644 --- a/system/all/nginx.nix +++ b/system/all/nginx.nix @@ -1,28 +1,37 @@ -{ pkgs, ... }: +{ pkgs, lib, ... }: let access_log_sink = "workhorse.private:12304"; error_log_sink = "workhorse.private:12305"; in { - # for graylog logging - services.nginx.commonHttpConfig = '' - log_format graylog2_json escape=json '{ "timestamp": "$time_iso8601", ' - '"facility": "nginx", ' - '"remote_addr": "$remote_addr", ' - '"body_bytes_sent": $body_bytes_sent, ' - '"request_time": $request_time, ' - '"response_status": $status, ' - '"request": "$request", ' - '"request_method": "$request_method", ' - '"host": "$host",' - '"upstream_cache_status": "$upstream_cache_status",' - '"upstream_addr": "$upstream_addr",' - '"http_x_forwarded_for": "$http_x_forwarded_for",' - '"http_referrer": "$http_referer", ' - '"http_user_agent": "$http_user_agent" }'; - access_log syslog:server=${access_log_sink} graylog2_json; - error_log syslog:server=${error_log_sink}; - ''; + # for graylog logging + services.nginx = { + # Use recommended settings + recommendedGzipSettings = lib.mkDefault true; + recommendedOptimisation = lib.mkDefault true; + recommendedProxySettings = lib.mkDefault true; + recommendedTlsSettings = lib.mkDefault true; + + commonHttpConfig = '' + log_format graylog2_json escape=json '{ "timestamp": "$time_iso8601", ' + '"facility": "nginx", ' + '"remote_addr": "$remote_addr", ' + '"body_bytes_sent": $body_bytes_sent, ' + '"request_time": $request_time, ' + '"response_status": $status, ' + '"request": "$request", ' + '"request_method": "$request_method", ' + '"host": "$host",' + '"upstream_cache_status": "$upstream_cache_status",' + '"upstream_addr": "$upstream_addr",' + '"http_x_forwarded_for": "$http_x_forwarded_for",' + '"http_referrer": "$http_referer", ' + '"http_user_agent": "$http_user_agent" }'; + + access_log syslog:server=${access_log_sink} graylog2_json; + error_log syslog:server=${error_log_sink}; + ''; + }; services.nginx.package = pkgs.nginxMainline; }