1 { config, options, lib, pkgs, utils, ... }:
4 json = pkgs.formats.json {};
6 cfg = config.services.discourse;
7 opt = options.services.discourse;
9 # Keep in sync with https://github.com/discourse/discourse_docker/blob/main/image/base/slim.Dockerfile#L5
10 upstreamPostgresqlVersion = lib.getVersion pkgs.postgresql_13;
12 postgresqlPackage = if config.services.postgresql.enable then
13 config.services.postgresql.package
17 postgresqlVersion = lib.getVersion postgresqlPackage;
19 # We only want to create a database if we're actually going to connect to it.
20 databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == null;
22 tlsEnabled = cfg.enableACME
23 || cfg.sslCertificate != null
24 || cfg.sslCertificateKey != null;
28 services.discourse = {
29 enable = lib.mkEnableOption "Discourse, an open source discussion platform";
31 package = lib.mkOption {
32 type = lib.types.package;
33 default = pkgs.discourse;
34 apply = p: p.override {
35 plugins = lib.unique (p.enabledPlugins ++ cfg.plugins);
37 defaultText = lib.literalExpression "pkgs.discourse";
39 The discourse package to use.
43 hostname = lib.mkOption {
45 default = config.networking.fqdnOrHostName;
46 defaultText = lib.literalExpression "config.networking.fqdnOrHostName";
47 example = "discourse.example.com";
49 The hostname to serve Discourse on.
53 secretKeyBaseFile = lib.mkOption {
54 type = with lib.types; nullOr path;
56 example = "/run/keys/secret_key_base";
58 The path to a file containing the
59 `secret_key_base` secret.
61 Discourse uses `secret_key_base` to encrypt
62 the cookie store, which contains session data, and to digest
65 Needs to be a 64 byte long string of hexadecimal
66 characters. You can generate one by running
69 openssl rand -hex 64 >/path/to/secret_key_base_file
72 This should be a string, not a nix path, since nix paths are
73 copied into the world-readable nix store.
77 sslCertificate = lib.mkOption {
78 type = with lib.types; nullOr path;
80 example = "/run/keys/ssl.cert";
82 The path to the server SSL certificate. Set this to enable
87 sslCertificateKey = lib.mkOption {
88 type = with lib.types; nullOr path;
90 example = "/run/keys/ssl.key";
92 The path to the server SSL certificate key. Set this to
97 enableACME = lib.mkOption {
98 type = lib.types.bool;
99 default = cfg.sslCertificate == null && cfg.sslCertificateKey == null;
100 defaultText = lib.literalMD ''
101 `true`, unless {option}`services.discourse.sslCertificate`
102 and {option}`services.discourse.sslCertificateKey` are set.
105 Whether an ACME certificate should be used to secure
106 connections to the server.
110 backendSettings = lib.mkOption {
111 type = with lib.types; attrsOf (nullOr (oneOf [ str int bool float ]));
113 example = lib.literalExpression ''
115 max_reqs_per_ip_per_minute = 300;
116 max_reqs_per_ip_per_10_seconds = 60;
117 max_asset_reqs_per_ip_per_10_seconds = 250;
118 max_reqs_per_ip_mode = "warn+block";
122 Additional settings to put in the
123 {file}`discourse.conf` file.
126 [discourse_defaults.conf](https://github.com/discourse/discourse/blob/master/config/discourse_defaults.conf)
127 file in the upstream distribution to find available options.
129 Setting an option to `null` means
130 “define variable, but leave right-hand side empty”.
134 siteSettings = lib.mkOption {
137 example = lib.literalExpression ''
141 site_description = "Discuss My Cats (and be nice plz)";
144 enable_github_logins = true;
145 github_client_id = "a2f6dfe838cb3206ce20";
146 github_client_secret._secret = /run/keys/discourse_github_client_secret;
151 Discourse site settings. These are the settings that can be
152 changed from the UI. This only defines their default values:
153 they can still be overridden from the UI.
155 Available settings can be found by looking in the
156 [site_settings.yml](https://github.com/discourse/discourse/blob/master/config/site_settings.yml)
157 file of the upstream distribution. To find a setting's path,
158 you only need to care about the first two levels; i.e. its
159 category and name. See the example.
161 Settings containing secret data should be set to an
162 attribute set containing the attribute
163 `_secret` - a string pointing to a file
164 containing the value the option should be set to. See the
165 example to get a better picture of this: in the resulting
166 {file}`config/nixos_site_settings.json` file,
167 the `login.github_client_secret` key will
168 be set to the contents of the
169 {file}`/run/keys/discourse_github_client_secret`
175 skipCreate = lib.mkOption {
176 type = lib.types.bool;
179 Do not create the admin account, instead rely on other
180 existing admin accounts.
184 email = lib.mkOption {
185 type = lib.types.str;
186 example = "admin@example.com";
188 The admin user email address.
192 username = lib.mkOption {
193 type = lib.types.str;
196 The admin user username.
200 fullName = lib.mkOption {
201 type = lib.types.str;
203 The admin user's full name.
207 passwordFile = lib.mkOption {
208 type = lib.types.path;
210 A path to a file containing the admin user's password.
212 This should be a string, not a nix path, since nix paths are
213 copied into the world-readable nix store.
218 nginx.enable = lib.mkOption {
219 type = lib.types.bool;
222 Whether an `nginx` virtual host should be
223 set up to serve Discourse. Only disable if you're planning
224 to use a different web server, which is not recommended.
229 pool = lib.mkOption {
230 type = lib.types.int;
233 Database connection pool size.
237 host = lib.mkOption {
238 type = with lib.types; nullOr str;
241 Discourse database hostname. `null` means
242 “prefer local unix socket connection”.
246 passwordFile = lib.mkOption {
247 type = with lib.types; nullOr path;
250 File containing the Discourse database user password.
252 This should be a string, not a nix path, since nix paths are
253 copied into the world-readable nix store.
257 createLocally = lib.mkOption {
258 type = lib.types.bool;
261 Whether a database should be automatically created on the
262 local host. Set this to `false` if you plan
263 on provisioning a local database yourself. This has no effect
264 if {option}`services.discourse.database.host` is customized.
268 name = lib.mkOption {
269 type = lib.types.str;
270 default = "discourse";
272 Discourse database name.
276 username = lib.mkOption {
277 type = lib.types.str;
278 default = "discourse";
280 Discourse database user.
284 ignorePostgresqlVersion = lib.mkOption {
285 type = lib.types.bool;
288 Whether to allow other versions of PostgreSQL than the
289 recommended one. Only effective when
290 {option}`services.discourse.database.createLocally`
297 host = lib.mkOption {
298 type = lib.types.str;
299 default = "localhost";
301 Redis server hostname.
305 passwordFile = lib.mkOption {
306 type = with lib.types; nullOr path;
309 File containing the Redis password.
311 This should be a string, not a nix path, since nix paths are
312 copied into the world-readable nix store.
316 dbNumber = lib.mkOption {
317 type = lib.types.int;
320 Redis database number.
324 useSSL = lib.mkOption {
325 type = lib.types.bool;
326 default = cfg.redis.host != "localhost";
327 defaultText = lib.literalExpression ''config.${opt.redis.host} != "localhost"'';
329 Connect to Redis with SSL.
335 notificationEmailAddress = lib.mkOption {
336 type = lib.types.str;
337 default = "${if cfg.mail.incoming.enable then "notifications" else "noreply"}@${cfg.hostname}";
338 defaultText = lib.literalExpression ''
339 "''${if config.services.discourse.mail.incoming.enable then "notifications" else "noreply"}@''${config.services.discourse.hostname}"
342 The `from:` email address used when
343 sending all essential system emails. The domain specified
344 here must have SPF, DKIM and reverse PTR records set
345 correctly for email to arrive.
349 contactEmailAddress = lib.mkOption {
350 type = lib.types.str;
353 Email address of key contact responsible for this
354 site. Used for critical notifications, as well as on the
355 `/about` contact form for urgent matters.
360 serverAddress = lib.mkOption {
361 type = lib.types.str;
362 default = "localhost";
364 The address of the SMTP server Discourse should use to
369 port = lib.mkOption {
370 type = lib.types.port;
373 The port of the SMTP server Discourse should use to
378 username = lib.mkOption {
379 type = with lib.types; nullOr str;
382 The username of the SMTP server.
386 passwordFile = lib.mkOption {
387 type = lib.types.nullOr lib.types.path;
390 A file containing the password of the SMTP server account.
392 This should be a string, not a nix path, since nix paths
393 are copied into the world-readable nix store.
397 domain = lib.mkOption {
398 type = lib.types.str;
399 default = cfg.hostname;
400 defaultText = lib.literalExpression "config.${opt.hostname}";
402 HELO domain to use for outgoing mail.
406 authentication = lib.mkOption {
407 type = with lib.types; nullOr (enum ["plain" "login" "cram_md5"]);
410 Authentication type to use, see https://api.rubyonrails.org/classes/ActionMailer/Base.html
414 enableStartTLSAuto = lib.mkOption {
415 type = lib.types.bool;
418 Whether to try to use StartTLS.
422 opensslVerifyMode = lib.mkOption {
423 type = lib.types.str;
426 How OpenSSL checks the certificate, see https://api.rubyonrails.org/classes/ActionMailer/Base.html
430 forceTLS = lib.mkOption {
431 type = lib.types.bool;
434 Force implicit TLS as per RFC 8314 3.3.
440 enable = lib.mkOption {
441 type = lib.types.bool;
444 Whether to set up Postfix to receive incoming mail.
448 replyEmailAddress = lib.mkOption {
449 type = lib.types.str;
450 default = "%{reply_key}@${cfg.hostname}";
451 defaultText = lib.literalExpression ''"%{reply_key}@''${config.services.discourse.hostname}"'';
453 Template for reply by email incoming email address, for
454 example: %{reply_key}@reply.example.com or
455 replies+%{reply_key}@example.com
459 mailReceiverPackage = lib.mkOption {
460 type = lib.types.package;
461 default = pkgs.discourse-mail-receiver;
462 defaultText = lib.literalExpression "pkgs.discourse-mail-receiver";
464 The discourse-mail-receiver package to use.
468 apiKeyFile = lib.mkOption {
469 type = lib.types.nullOr lib.types.path;
472 A file containing the Discourse API key used to add
473 posts and messages from mail. If left at its default
474 value `null`, one will be automatically
477 This should be a string, not a nix path, since nix paths
478 are copied into the world-readable nix store.
484 plugins = lib.mkOption {
485 type = lib.types.listOf lib.types.package;
487 example = lib.literalExpression ''
488 with config.services.discourse.package.plugins; [
489 discourse-canned-replies
494 Plugins to install as part of Discourse, expressed as a list of derivations.
498 sidekiqProcesses = lib.mkOption {
499 type = lib.types.int;
502 How many Sidekiq processes should be spawned.
506 unicornTimeout = lib.mkOption {
507 type = lib.types.int;
510 Time in seconds before a request to Unicorn times out.
512 This can be raised if the system Discourse is running on is
513 too slow to handle many requests within 30 seconds.
519 config = lib.mkIf cfg.enable {
522 assertion = (cfg.database.host != null) -> (cfg.database.passwordFile != null);
523 message = "When services.gitlab.database.host is customized, services.discourse.database.passwordFile must be set!";
526 assertion = cfg.hostname != "";
527 message = "Could not automatically determine hostname, set service.discourse.hostname manually.";
530 assertion = cfg.database.ignorePostgresqlVersion || (databaseActuallyCreateLocally -> upstreamPostgresqlVersion == postgresqlVersion);
531 message = "The PostgreSQL version recommended for use with Discourse is ${upstreamPostgresqlVersion}, you're using ${postgresqlVersion}. "
532 + "Either update your PostgreSQL package to the correct version or set services.discourse.database.ignorePostgresqlVersion. "
533 + "See https://nixos.org/manual/nixos/stable/index.html#module-postgresql for details on how to upgrade PostgreSQL.";
538 # Default config values are from `config/discourse_defaults.conf`
540 services.discourse.backendSettings = lib.mapAttrs (_: lib.mkDefault) {
541 db_pool = cfg.database.pool;
543 db_connect_timeout = 5;
545 db_host = cfg.database.host;
546 db_backup_host = null;
548 db_backup_port = 5432;
549 db_name = cfg.database.name;
550 db_username = if databaseActuallyCreateLocally then "discourse" else cfg.database.username;
551 db_password = cfg.database.passwordFile;
552 db_prepared_statements = false;
553 db_replica_host = null;
554 db_replica_port = null;
555 db_advisory_locks = true;
557 inherit (cfg) hostname;
558 backup_hostname = null;
560 smtp_address = cfg.mail.outgoing.serverAddress;
561 smtp_port = cfg.mail.outgoing.port;
562 smtp_domain = cfg.mail.outgoing.domain;
563 smtp_user_name = cfg.mail.outgoing.username;
564 smtp_password = cfg.mail.outgoing.passwordFile;
565 smtp_authentication = cfg.mail.outgoing.authentication;
566 smtp_enable_start_tls = cfg.mail.outgoing.enableStartTLSAuto;
567 smtp_openssl_verify_mode = cfg.mail.outgoing.opensslVerifyMode;
568 smtp_force_tls = cfg.mail.outgoing.forceTLS;
570 load_mini_profiler = true;
571 mini_profiler_snapshots_period = 0;
572 mini_profiler_snapshots_transport_url = null;
573 mini_profiler_snapshots_transport_auth_key = null;
576 cdn_origin_hostname = null;
577 developer_emails = null;
579 redis_host = cfg.redis.host;
581 redis_replica_host = null;
582 redis_replica_port = 6379;
583 redis_db = cfg.redis.dbNumber;
584 redis_password = cfg.redis.passwordFile;
585 redis_skip_client_commands = false;
586 redis_use_ssl = cfg.redis.useSSL;
588 message_bus_redis_enabled = false;
589 message_bus_redis_host = "localhost";
590 message_bus_redis_port = 6379;
591 message_bus_redis_replica_host = null;
592 message_bus_redis_replica_port = 6379;
593 message_bus_redis_db = 0;
594 message_bus_redis_password = null;
595 message_bus_redis_skip_client_commands = false;
599 serve_static_assets = false;
601 connection_reaper_age = 30;
602 connection_reaper_interval = 30;
603 relative_url_root = null;
604 message_bus_max_backlog_size = 100;
605 message_bus_clear_every = 50;
606 secret_key_base = cfg.secretKeyBaseFile;
607 fallback_assets_path = null;
611 s3_access_key_id = null;
612 s3_secret_access_key = null;
613 s3_use_iam_profile = null;
616 s3_http_continue_timeout = null;
617 s3_install_cors_rule = null;
618 s3_asset_cdn_url = null;
620 max_user_api_reqs_per_minute = 20;
621 max_user_api_reqs_per_day = 2880;
622 max_admin_api_reqs_per_minute = 60;
623 max_reqs_per_ip_per_minute = 200;
624 max_reqs_per_ip_per_10_seconds = 50;
625 max_asset_reqs_per_ip_per_10_seconds = 200;
626 max_reqs_per_ip_mode = "block";
627 max_reqs_rate_limit_on_private = false;
628 skip_per_ip_rate_limit_trust_level = 1;
629 force_anonymous_min_queue_seconds = 1;
630 force_anonymous_min_per_10_seconds = 3;
631 background_requests_max_queue_length = 0.5;
632 reject_message_bus_queue_seconds = 0.1;
633 disable_search_queue_threshold = 1;
634 max_old_rebakes_per_15_minutes = 300;
635 max_logster_logs = 1000;
636 refresh_maxmind_db_during_precompile_days = 2;
637 maxmind_backup_path = null;
638 maxmind_license_key = null;
639 enable_performance_http_headers = false;
640 enable_js_error_reporting = true;
641 mini_scheduler_workers = 5;
642 compress_anon_cache = false;
643 anon_cache_store_threshold = 2;
644 allowed_theme_repos = null;
645 enable_email_sync_demon = false;
646 max_digests_enqueued_per_30_mins_per_site = 10000;
648 multisite_config_path = "config/multisite.yml";
649 enable_long_polling = null;
650 long_polling_interval = null;
651 preload_link_header = false;
652 redirect_avatar_requests = false;
653 pg_force_readonly_mode = false;
654 dns_query_timeout_secs = null;
655 regex_timeout_seconds = 2;
656 allow_impersonation = true;
659 services.redis.servers.discourse =
660 lib.mkIf (lib.elem cfg.redis.host [ "localhost" "127.0.0.1" ]) {
662 bind = cfg.redis.host;
663 port = cfg.backendSettings.redis_port;
666 services.postgresql = lib.mkIf databaseActuallyCreateLocally {
668 ensureUsers = [{ name = "discourse"; }];
671 # The postgresql module doesn't currently support concepts like
672 # objects owners and extensions; for now we tack on what's needed
674 systemd.services.discourse-postgresql =
676 pgsql = config.services.postgresql;
678 lib.mkIf databaseActuallyCreateLocally {
679 after = [ "postgresql.service" ];
680 bindsTo = [ "postgresql.service" ];
681 wantedBy = [ "discourse.service" ];
682 partOf = [ "discourse.service" ];
687 set -o errexit -o pipefail -o nounset -o errtrace
688 shopt -s inherit_errexit
690 psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'discourse'" | grep -q 1 || psql -tAc 'CREATE DATABASE "discourse" OWNER "discourse"'
691 psql '${cfg.database.name}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
692 psql '${cfg.database.name}' -tAc "CREATE EXTENSION IF NOT EXISTS hstore"
696 User = pgsql.superUser;
698 RemainAfterExit = true;
702 systemd.services.discourse = {
703 wantedBy = [ "multi-user.target" ];
705 "redis-discourse.service"
707 "discourse-postgresql.service"
710 "redis-discourse.service"
711 ] ++ lib.optionals (cfg.database.host == null) [
713 "discourse-postgresql.service"
715 path = cfg.package.runtimeDeps ++ [
720 environment = cfg.package.runtimeEnv // {
721 UNICORN_TIMEOUT = builtins.toString cfg.unicornTimeout;
722 UNICORN_SIDEKIQS = builtins.toString cfg.sidekiqProcesses;
723 MALLOC_ARENA_MAX = "2";
728 discourseKeyValue = lib.generators.toKeyValue {
729 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " = " {
730 mkValueString = v: with builtins;
731 if isInt v then toString v
732 else if isString v then ''"${v}"''
733 else if true == v then "true"
734 else if false == v then "false"
735 else if null == v then ""
736 else if isFloat v then lib.strings.floatToString v
737 else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
741 discourseConf = pkgs.writeText "discourse.conf" (discourseKeyValue cfg.backendSettings);
743 mkSecretReplacement = file:
744 lib.optionalString (file != null) ''
745 replace-secret '${file}' '${file}' /run/discourse/config/discourse.conf
749 export ADMIN_EMAIL="${cfg.admin.email}"
750 export ADMIN_NAME="${cfg.admin.fullName}"
751 export ADMIN_USERNAME="${cfg.admin.username}"
752 ADMIN_PASSWORD="$(<${cfg.admin.passwordFile})"
753 export ADMIN_PASSWORD
754 discourse-rake admin:create_noninteractively
758 set -o errexit -o pipefail -o nounset -o errtrace
759 shopt -s inherit_errexit
763 rm -rf /var/lib/discourse/tmp/*
765 cp -r ${cfg.package}/share/discourse/config.dist/* /run/discourse/config/
766 cp -r ${cfg.package}/share/discourse/public.dist/* /run/discourse/public/
767 ln -sf /var/lib/discourse/uploads /run/discourse/public/uploads
768 ln -sf /var/lib/discourse/backups /run/discourse/public/backups
773 ${utils.genJqSecretsReplacementSnippet
775 "/run/discourse/config/nixos_site_settings.json"
777 install -T -m 0600 -o discourse ${discourseConf} /run/discourse/config/discourse.conf
778 ${mkSecretReplacement cfg.database.passwordFile}
779 ${mkSecretReplacement cfg.mail.outgoing.passwordFile}
780 ${mkSecretReplacement cfg.redis.passwordFile}
781 ${mkSecretReplacement cfg.secretKeyBaseFile}
782 chmod 0400 /run/discourse/config/discourse.conf
785 discourse-rake db:migrate >>/var/log/discourse/db_migration.log
786 chmod -R u+w /var/lib/discourse/tmp/
788 ${lib.optionalString (!cfg.admin.skipCreate) mkAdmin}
790 discourse-rake themes:update
791 discourse-rake uploads:regenerate_missing_optimized
798 RuntimeDirectory = map (p: "discourse/" + p) [
801 "assets/javascripts/plugins"
805 RuntimeDirectoryMode = "0750";
806 StateDirectory = map (p: "discourse/" + p) [
811 StateDirectoryMode = "0750";
812 LogsDirectory = "discourse";
813 TimeoutSec = "infinity";
814 Restart = "on-failure";
815 WorkingDirectory = "${cfg.package}/share/discourse";
819 NoNewPrivileges = true;
820 RestrictSUIDSGID = true;
821 ProtectSystem = "strict";
822 ProtectHome = "read-only";
824 ExecStart = "${cfg.package.rubyEnv}/bin/bundle exec config/unicorn_launcher -E production -c config/unicorn.conf.rb";
828 services.nginx = lib.mkIf cfg.nginx.enable {
831 recommendedTlsSettings = true;
832 recommendedOptimisation = true;
833 recommendedBrotliSettings = true;
834 recommendedGzipSettings = true;
835 recommendedProxySettings = true;
837 upstreams.discourse.servers."unix:/run/discourse/sockets/unicorn.sock" = {};
839 appendHttpConfig = ''
840 # inactive means we keep stuff around for 1440m minutes regardless of last access (1 week)
841 # levels means it is a 2 deep hierarchy cause we can have lots of files
842 # max_size limits the size of the cache
843 proxy_cache_path /var/cache/nginx inactive=1440m levels=1:2 keys_zone=discourse:10m max_size=600m;
845 # see: https://meta.discourse.org/t/x/74060
846 proxy_buffer_size 8k;
849 virtualHosts.${cfg.hostname} = {
850 inherit (cfg) sslCertificate sslCertificateKey enableACME;
851 forceSSL = lib.mkDefault tlsEnabled;
853 root = "${cfg.package}/share/discourse/public";
857 proxy = { extraConfig ? "" }: {
858 proxyPass = "http://discourse";
859 extraConfig = extraConfig + ''
860 proxy_set_header X-Request-Start "t=''${msec}";
865 add_header Cache-Control public,immutable;
867 cache_1y = cache "1y";
868 cache_1d = cache "1d";
871 "/".tryFiles = "$uri @discourse";
872 "@discourse" = proxy {};
873 "^~ /backups/".extraConfig = ''
883 "~ ^/uploads/short-url/" = proxy {};
884 "~ ^/secure-media-uploads/" = proxy {};
885 "~* (fonts|assets|plugins|uploads)/.*\.(eot|ttf|woff|woff2|ico|otf)$".extraConfig = cache_1y + ''
886 add_header Access-Control-Allow-Origin *;
888 "/srv/status" = proxy {
894 "~ ^/javascripts/".extraConfig = cache_1d;
895 "~ ^/assets/(?<asset_path>.+)$".extraConfig = cache_1y + ''
896 # asset pipeline enables this
900 "~ ^/plugins/".extraConfig = cache_1y;
901 "~ /images/emoji/".extraConfig = cache_1y;
902 "~ ^/uploads/" = proxy {
903 extraConfig = cache_1y + ''
904 proxy_set_header X-Sendfile-Type X-Accel-Redirect;
905 proxy_set_header X-Accel-Mapping ${cfg.package}/share/discourse/public/=/downloads/;
908 location ~ /stylesheet-cache/ {
911 # this allows us to bypass rails
912 location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp)$ {
915 # SVG needs an extra header attached
916 location ~* \.(svg)$ {
918 # thumbnails & optimized images
919 location ~ /_?optimized/ {
924 "~ ^/admin/backups/" = proxy {
926 proxy_set_header X-Sendfile-Type X-Accel-Redirect;
927 proxy_set_header X-Accel-Mapping ${cfg.package}/share/discourse/public/=/downloads/;
930 "~ ^/(svg-sprite/|letter_avatar/|letter_avatar_proxy/|user_avatar|highlight-js|stylesheets|theme-javascripts|favicon/proxied|service-worker)" = proxy {
932 # if Set-Cookie is in the response nothing gets cached
933 # this is double bad cause we are not passing last modified in
934 proxy_ignore_headers "Set-Cookie";
935 proxy_hide_header "Set-Cookie";
936 proxy_hide_header "X-Discourse-Username";
937 proxy_hide_header "X-Runtime";
939 # note x-accel-redirect can not be used with proxy_cache
940 proxy_cache discourse;
941 proxy_cache_key "$scheme,$host,$request_uri";
942 proxy_cache_valid 200 301 302 7d;
945 "/message-bus/" = proxy {
947 proxy_http_version 1.1;
951 "/downloads/".extraConfig = ''
953 alias ${cfg.package}/share/discourse/public/;
959 systemd.services.discourse-mail-receiver-setup = lib.mkIf cfg.mail.incoming.enable (
961 mail-receiver-environment = {
962 MAIL_DOMAIN = cfg.hostname;
963 DISCOURSE_BASE_URL = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}";
964 DISCOURSE_API_KEY = "@api-key@";
965 DISCOURSE_API_USERNAME = "system";
967 mail-receiver-json = json.generate "mail-receiver.json" mail-receiver-environment;
970 before = [ "postfix.service" ];
971 after = [ "discourse.service" ];
972 wantedBy = [ "discourse.service" ];
973 partOf = [ "discourse.service" ];
978 preStart = lib.optionalString (cfg.mail.incoming.apiKeyFile == null) ''
979 set -o errexit -o pipefail -o nounset -o errtrace
980 shopt -s inherit_errexit
982 if [[ ! -e /var/lib/discourse-mail-receiver/api_key ]]; then
983 discourse-rake api_key:create_master[email-receiver] >/var/lib/discourse-mail-receiver/api_key
989 if cfg.mail.incoming.apiKeyFile == null then
990 "/var/lib/discourse-mail-receiver/api_key"
992 cfg.mail.incoming.apiKeyFile;
994 set -o errexit -o pipefail -o nounset -o errtrace
995 shopt -s inherit_errexit
997 api_key=$(<'${apiKeyPath}')
1000 jq <${mail-receiver-json} \
1001 '.DISCOURSE_API_KEY = $ENV.api_key' \
1002 >'/run/discourse-mail-receiver/mail-receiver-environment.json'
1007 RemainAfterExit = true;
1008 RuntimeDirectory = "discourse-mail-receiver";
1009 RuntimeDirectoryMode = "0700";
1010 StateDirectory = "discourse-mail-receiver";
1012 Group = "discourse";
1016 services.discourse.siteSettings = {
1018 notification_email = cfg.mail.notificationEmailAddress;
1019 contact_email = cfg.mail.contactEmailAddress;
1021 security.force_https = tlsEnabled;
1023 manual_polling_enabled = cfg.mail.incoming.enable;
1024 reply_by_email_enabled = cfg.mail.incoming.enable;
1025 reply_by_email_address = cfg.mail.incoming.replyEmailAddress;
1029 services.postfix = lib.mkIf cfg.mail.incoming.enable {
1031 sslCert = lib.optionalString (cfg.sslCertificate != null) cfg.sslCertificate;
1032 sslKey = lib.optionalString (cfg.sslCertificateKey != null) cfg.sslCertificateKey;
1034 origin = cfg.hostname;
1035 relayDomains = [ cfg.hostname ];
1037 smtpd_recipient_restrictions = "check_policy_service unix:private/discourse-policy";
1038 append_dot_mydomain = lib.mkDefault false;
1039 compatibility_level = "2";
1040 smtputf8_enable = false;
1041 smtpd_banner = lib.mkDefault "ESMTP server";
1042 myhostname = lib.mkDefault cfg.hostname;
1043 mydestination = lib.mkDefault "localhost";
1046 ${cfg.hostname} discourse-mail-receiver:
1049 "discourse-mail-receiver" = {
1056 "argv=${cfg.mail.incoming.mailReceiverPackage}/bin/receive-mail"
1060 "discourse-policy" = {
1067 "argv=${cfg.mail.incoming.mailReceiverPackage}/bin/discourse-smtp-fast-rejection"
1075 group = "discourse";
1076 isSystemUser = true;
1078 } // (lib.optionalAttrs cfg.nginx.enable {
1079 ${config.services.nginx.user}.extraGroups = [ "discourse" ];
1086 environment.systemPackages = [
1091 meta.doc = ./discourse.md;
1092 meta.maintainers = [ lib.maintainers.talyz ];