import Config import Plausible.ConfigHelpers require Logger if config_env() in [:dev, :test] do Envy.load(["config/.env.#{config_env()}"]) end if config_env() == :ce_dev do Envy.load(["config/.env.dev"]) end if config_env() == :ce_test do Envy.load(["config/.env.test"]) end config_dir = System.get_env("CONFIG_DIR", "/run/secrets") log_format = get_var_from_path_or_env(config_dir, "LOG_FORMAT", "standard") default_log_level = if config_env() == :ce, do: "notice", else: "warning" log_level = config_dir |> get_var_from_path_or_env("LOG_LEVEL", default_log_level) |> String.to_existing_atom() config :logger, level: log_level, backends: [:console] config :logger, Sentry.LoggerBackend, capture_log_messages: true, level: :error case String.downcase(log_format) do "standard" -> config :logger, :console, format: "$time $metadata[$level] $message\n", metadata: [:request_id] "json" -> config :logger, :console, format: {ExJsonLogger, :format}, metadata: [ :request_id ] end # Listen IP supports IPv4 and IPv6 addresses. listen_ip = ( str = get_var_from_path_or_env(config_dir, "LISTEN_IP") || "127.0.0.1" case :inet.parse_address(String.to_charlist(str)) do {:ok, ip_addr} -> ip_addr {:error, reason} -> raise "Invalid LISTEN_IP '#{str}' error: #{inspect(reason)}" end ) # System.get_env does not accept a non string default http_port = get_int_from_path_or_env(config_dir, "HTTP_PORT") || get_int_from_path_or_env(config_dir, "PORT", 8000) https_port = get_int_from_path_or_env(config_dir, "HTTPS_PORT") base_url = get_var_from_path_or_env(config_dir, "BASE_URL") if !base_url do raise "BASE_URL configuration option is required. See https://github.com/plausible/community-edition/wiki/configuration#base_url" end base_url = URI.parse(base_url) if base_url.scheme not in ["http", "https"] do raise "BASE_URL must start with `http` or `https`. Currently configured as `#{System.get_env("BASE_URL")}`" end secret_key_base = get_var_from_path_or_env(config_dir, "SECRET_KEY_BASE", nil) case secret_key_base do nil -> raise "SECRET_KEY_BASE configuration option is required. See https://github.com/plausible/community-edition/wiki/configuration#secret_key_base" key when byte_size(key) < 32 -> raise "SECRET_KEY_BASE must be at least 32 bytes long. See https://github.com/plausible/community-edition/wiki/configuration#secret_key_base" _ -> nil end super_admin_user_ids = get_var_from_path_or_env(config_dir, "ADMIN_USER_IDS", "") |> String.split(",") |> Enum.map(fn id -> Integer.parse(id) end) |> Enum.map(fn {int, ""} -> int _ -> nil end) |> Enum.filter(& &1) env = get_var_from_path_or_env(config_dir, "ENVIRONMENT", "prod") mailer_adapter = get_var_from_path_or_env(config_dir, "MAILER_ADAPTER", "Bamboo.Mua") mailer_email = get_var_from_path_or_env(config_dir, "MAILER_EMAIL", "plausible@#{base_url.host}") mailer_email = if mailer_name = get_var_from_path_or_env(config_dir, "MAILER_NAME") do {mailer_name, mailer_email} else mailer_email end app_version = get_var_from_path_or_env(config_dir, "APP_VERSION", "0.0.1") ch_db_url = get_var_from_path_or_env( config_dir, "CLICKHOUSE_DATABASE_URL", "http://plausible_events_db:8123/plausible_events_db" ) {ingest_pool_size, ""} = get_var_from_path_or_env( config_dir, "CLICKHOUSE_INGEST_POOL_SIZE", "5" ) |> Integer.parse() {ch_flush_interval_ms, ""} = config_dir |> get_var_from_path_or_env("CLICKHOUSE_FLUSH_INTERVAL_MS", "5000") |> Integer.parse() if get_var_from_path_or_env(config_dir, "CLICKHOUSE_MAX_BUFFER_SIZE") do Logger.warning( "CLICKHOUSE_MAX_BUFFER_SIZE is deprecated, please use CLICKHOUSE_MAX_BUFFER_SIZE_BYTES instead" ) end {ch_max_buffer_size, ""} = config_dir |> get_var_from_path_or_env("CLICKHOUSE_MAX_BUFFER_SIZE_BYTES", "100000") |> Integer.parse() # Can be generated with `Base.encode64(:crypto.strong_rand_bytes(32))` from # iex shell or `openssl rand -base64 32` from command line. totp_vault_key = if totp_vault_key_base64 = get_var_from_path_or_env(config_dir, "TOTP_VAULT_KEY") do case Base.decode64(totp_vault_key_base64) do {:ok, totp_vault_key} -> if byte_size(totp_vault_key) == 32 do totp_vault_key else raise ArgumentError, """ TOTP_VAULT_KEY must be Base64 encoded 32 bytes, e.g. `openssl rand -base64 32`. Got Base64 encoded #{byte_size(totp_vault_key)} bytes. More info: https://github.com/plausible/community-edition/wiki/configuration#totp_vault_key """ end :error -> raise ArgumentError, """ TOTP_VAULT_KEY must be Base64 encoded 32 bytes, e.g. `openssl rand -base64 32` More info: https://github.com/plausible/community-edition/wiki/configuration#totp_vault_key """ end else Plug.Crypto.KeyGenerator.generate(secret_key_base, "totp", length: 32, iterations: 100_000) end config :plausible, Plausible.Auth.TOTP, vault_key: totp_vault_key build_metadata_raw = get_var_from_path_or_env(config_dir, "BUILD_METADATA", "{}") build_metadata = case Jason.decode(build_metadata_raw) do {:ok, build_metadata} -> build_metadata {:error, error} -> error = Exception.format(:error, error) Logger.warning(""" failed to parse $BUILD_METADATA: #{error} $BUILD_METADATA is set to #{build_metadata_raw}\ """) Logger.warning("falling back to empty build metadata, as if $BUILD_METADATA was set to {}") _fallback = %{} end app_host = get_var_from_path_or_env(config_dir, "APP_HOST") runtime_metadata = [ version: get_in(build_metadata, ["labels", "org.opencontainers.image.version"]), commit: get_in(build_metadata, ["labels", "org.opencontainers.image.revision"]), created: get_in(build_metadata, ["labels", "org.opencontainers.image.created"]), tags: get_in(build_metadata, ["tags"]), app_host: app_host ] config :plausible, :runtime_metadata, runtime_metadata sentry_dsn = get_var_from_path_or_env(config_dir, "SENTRY_DSN") honeycomb_api_key = get_var_from_path_or_env(config_dir, "HONEYCOMB_API_KEY") honeycomb_dataset = get_var_from_path_or_env(config_dir, "HONEYCOMB_DATASET") paddle_auth_code = get_var_from_path_or_env(config_dir, "PADDLE_VENDOR_AUTH_CODE") paddle_vendor_id = get_var_from_path_or_env(config_dir, "PADDLE_VENDOR_ID") google_cid = get_var_from_path_or_env(config_dir, "GOOGLE_CLIENT_ID") google_secret = get_var_from_path_or_env(config_dir, "GOOGLE_CLIENT_SECRET") postmark_api_key = get_var_from_path_or_env(config_dir, "POSTMARK_API_KEY") help_scout_app_id = get_var_from_path_or_env(config_dir, "HELP_SCOUT_APP_ID") help_scout_app_secret = get_var_from_path_or_env(config_dir, "HELP_SCOUT_APP_SECRET") help_scout_signature_key = get_var_from_path_or_env(config_dir, "HELP_SCOUT_SIGNATURE_KEY") help_scout_vault_key = get_var_from_path_or_env(config_dir, "HELP_SCOUT_VAULT_KEY") {otel_sampler_ratio, ""} = config_dir |> get_var_from_path_or_env("OTEL_SAMPLER_RATIO", "0.5") |> Float.parse() geolite2_country_db = get_var_from_path_or_env( config_dir, "GEOLITE2_COUNTRY_DB", Application.app_dir(:plausible, "/priv/geodb/dbip-country.mmdb.gz") ) ip_geolocation_db = get_var_from_path_or_env(config_dir, "IP_GEOLOCATION_DB", geolite2_country_db) geonames_source_file = get_var_from_path_or_env(config_dir, "GEONAMES_SOURCE_FILE") maxmind_license_key = get_var_from_path_or_env(config_dir, "MAXMIND_LICENSE_KEY") maxmind_edition = get_var_from_path_or_env(config_dir, "MAXMIND_EDITION", "GeoLite2-City") data_dir = get_var_from_path_or_env(config_dir, "DATA_DIR") persistent_cache_dir = get_var_from_path_or_env(config_dir, "PERSISTENT_CACHE_DIR") # DEFAULT_DATA_DIR comes from the container image, please see our Dockerfile data_dir = data_dir || persistent_cache_dir || System.get_env("DEFAULT_DATA_DIR") persistent_cache_dir = persistent_cache_dir || data_dir enable_email_verification = get_bool_from_path_or_env(config_dir, "ENABLE_EMAIL_VERIFICATION", false) is_selfhost = get_bool_from_path_or_env(config_dir, "SELFHOST", true) # by default, only registration from invites is enabled in CE disable_registration_default = if config_env() == :ce do "invite_only" else "false" end disable_registration = config_dir |> get_var_from_path_or_env("DISABLE_REGISTRATION", disable_registration_default) |> String.to_existing_atom() if disable_registration not in [true, false, :invite_only] do raise "DISABLE_REGISTRATION must be one of `true`, `false`, or `invite_only`. See https://github.com/plausible/community-edition/wiki/configuration#disable_registration" end hcaptcha_sitekey = get_var_from_path_or_env(config_dir, "HCAPTCHA_SITEKEY") hcaptcha_secret = get_var_from_path_or_env(config_dir, "HCAPTCHA_SECRET") custom_script_name = config_dir |> get_var_from_path_or_env("CUSTOM_SCRIPT_NAME", "script") disable_cron = get_bool_from_path_or_env(config_dir, "DISABLE_CRON", false) log_failed_login_attempts = get_bool_from_path_or_env(config_dir, "LOG_FAILED_LOGIN_ATTEMPTS", false) websocket_url = get_var_from_path_or_env(config_dir, "WEBSOCKET_URL", "") if byte_size(websocket_url) > 0 and not String.ends_with?(URI.new!(websocket_url).host, base_url.host) do raise """ Cross-domain websocket authentication is not supported for this server. WEBSOCKET_URL=#{websocket_url} - host must be: '#{base_url.host}', because BASE_URL=#{base_url}. """ end secure_cookie_default = case base_url.scheme do "http" -> "false" "https" -> "true" end secure_cookie = config_dir |> get_var_from_path_or_env("SECURE_COOKIE", secure_cookie_default) |> String.to_existing_atom() license_key = get_var_from_path_or_env(config_dir, "LICENSE_KEY", "") config :plausible, environment: env, mailer_email: mailer_email, super_admin_user_ids: super_admin_user_ids, is_selfhost: is_selfhost, custom_script_name: custom_script_name, log_failed_login_attempts: log_failed_login_attempts, license_key: license_key, data_dir: data_dir config :plausible, :selfhost, enable_email_verification: enable_email_verification, disable_registration: disable_registration default_http_opts = [ transport_options: [max_connections: :infinity], protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192] ] config :plausible, PlausibleWeb.Endpoint, url: [scheme: base_url.scheme, host: base_url.host, path: base_url.path, port: base_url.port], http: [port: http_port, ip: listen_ip] ++ default_http_opts, secret_key_base: secret_key_base, websocket_url: websocket_url, secure_cookie: secure_cookie # maybe enable HTTPS in CE if config_env() in [:ce, :ce_dev, :ce_test] do if https_port do # the following configuration is based on https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28recommended.29 # except we enforce the cipher and ecc order and only use ciphers with support # for ecdsa certificates since that's what certbot generates by default https_opts = [ port: https_port, ip: listen_ip, transport_options: [socket_opts: [log_level: :warning]], versions: [:"tlsv1.2", :"tlsv1.3"], honor_cipher_order: true, honor_ecc_order: true, eccs: [:x25519, :secp256r1, :secp384r1], supported_groups: [:x25519, :secp256r1, :secp384r1], ciphers: [ # Mozilla recommended cipher suites (TLS 1.3) ~c"TLS_AES_128_GCM_SHA256", ~c"TLS_AES_256_GCM_SHA384", ~c"TLS_CHACHA20_POLY1305_SHA256", # Mozilla recommended cipher suites (TLS 1.2) ~c"ECDHE-ECDSA-AES128-GCM-SHA256", ~c"ECDHE-ECDSA-AES256-GCM-SHA384", ~c"ECDHE-ECDSA-CHACHA20-POLY1305" ] ] https_opts = Config.Reader.merge(default_http_opts, https_opts) config :plausible, PlausibleWeb.Endpoint, https: https_opts domain = base_url.host # do stricter checking in CE prod if config_env() == :ce do domain_is_ip? = case :inet.parse_address(to_charlist(domain)) do {:ok, _address} -> true _other -> false end if domain_is_ip? do raise ArgumentError, "Cannot generate TLS certificates for IP address #{inspect(domain)}" end domain_is_local? = domain == "localhost" or not String.contains?(domain, ".") if domain_is_local? do raise ArgumentError, "Cannot generate TLS certificates for local domain #{inspect(domain)}" end unless http_port == 80 do Logger.warning(""" HTTPS is enabled but the HTTP port is not 80. \ This will prevent automatic TLS certificate issuance as ACME validates the domain on port 80.\ """) end end acme_directory_url = get_var_from_path_or_env( config_dir, "ACME_DIRECTORY_URL", "https://acme-v02.api.letsencrypt.org/directory" ) db_folder = Path.join(data_dir || System.tmp_dir!(), "site_encrypt") email = case mailer_email do {_, email} -> email email when is_binary(email) -> email end config :plausible, :selfhost, site_encrypt: [ domain: domain, email: email, db_folder: db_folder, directory_url: acme_directory_url ] end end db_maybe_ipv6 = if get_bool_from_path_or_env(config_dir, "ECTO_IPV6") do if config_env() in [:ce, :ce_dev, :ce_test] do Logger.warning( "ECTO_IPV6 is no longer necessary as all TCP connections now try IPv6 automatically with IPv4 fallback" ) end [:inet6] else [] end db_url = get_var_from_path_or_env( config_dir, "DATABASE_URL", "postgres://postgres:postgres@plausible_db:5432/plausible_db" ) if db_socket_dir = get_var_from_path_or_env(config_dir, "DATABASE_SOCKET_DIR") do Logger.warning(""" DATABASE_SOCKET_DIR is deprecated, please use DATABASE_URL instead: DATABASE_URL=postgresql://postgres:postgres@#{URI.encode_www_form(db_socket_dir)}/plausible_db or DATABASE_URL=postgresql:///plausible_db?host=#{db_socket_dir}" """) end db_cacertfile = get_var_from_path_or_env(config_dir, "DATABASE_CACERTFILE") %URI{host: db_host} = db_uri = URI.parse(db_url) db_socket_dir? = String.starts_with?(db_host, "%2F") or db_host == "" if db_socket_dir? do [database] = String.split(db_uri.path, "/", trim: true) socket_dir = if db_host == "" do db_host = (db_uri.query || "") |> URI.decode_query() |> Map.get("host") db_host || raise ArgumentError, "DATABASE_URL=#{db_url} doesn't include host info" else URI.decode_www_form(db_host) end config :plausible, Plausible.Repo, socket_dir: socket_dir, database: database if userinfo = db_uri.userinfo do [username, password] = String.split(userinfo, ":") config :plausible, Plausible.Repo, username: username, password: password end else config :plausible, Plausible.Repo, url: db_url, socket_options: db_maybe_ipv6 if db_cacertfile do config :plausible, Plausible.Repo, ssl: [cacertfile: db_cacertfile] end end sentry_app_version = runtime_metadata[:version] || app_version config :sentry, dsn: sentry_dsn, environment_name: env, release: sentry_app_version, tags: %{ app_version: sentry_app_version, app_host: app_host }, client: Plausible.Sentry.Client, send_max_attempts: 1, before_send: {Plausible.SentryFilter, :before_send} config :plausible, :paddle, vendor_auth_code: paddle_auth_code, vendor_id: paddle_vendor_id config :plausible, :google, client_id: google_cid, client_secret: google_secret, api_url: "https://www.googleapis.com", reporting_api_url: "https://analyticsreporting.googleapis.com" config :plausible, Plausible.HelpScout, app_id: help_scout_app_id, app_secret: help_scout_app_secret, signature_key: help_scout_signature_key, vault_key: help_scout_vault_key config :plausible, :imported, max_buffer_size: get_int_from_path_or_env(config_dir, "IMPORTED_MAX_BUFFER_SIZE", 10_000) maybe_ch_ipv6 = get_bool_from_path_or_env(config_dir, "ECTO_CH_IPV6", false) if maybe_ch_ipv6 && config_env() in [:ce, :ce_dev, :ce_test] do Logger.warning( "ECTO_CH_IPV6 is no longer necessary as all TCP connections now try IPv6 automatically with IPv4 fallback" ) end ch_cacertfile = get_var_from_path_or_env(config_dir, "CLICKHOUSE_CACERTFILE") ch_transport_opts = [ keepalive: true, show_econnreset: true, inet6: maybe_ch_ipv6 ] ch_transport_opts = if ch_cacertfile do ch_transport_opts ++ [cacertfile: ch_cacertfile] else ch_transport_opts end config :plausible, Plausible.ClickhouseRepo, queue_target: 500, queue_interval: 2000, url: ch_db_url, transport_opts: ch_transport_opts, settings: [ readonly: 1, join_algorithm: "direct,parallel_hash,hash" ] config :plausible, Plausible.IngestRepo, queue_target: 500, queue_interval: 2000, url: ch_db_url, transport_opts: ch_transport_opts, flush_interval_ms: ch_flush_interval_ms, max_buffer_size: ch_max_buffer_size, pool_size: ingest_pool_size, settings: [ materialized_views_ignore_errors: 1 ] config :plausible, Plausible.AsyncInsertRepo, queue_target: 500, queue_interval: 2000, url: ch_db_url, transport_opts: ch_transport_opts, pool_size: 1, settings: [ async_insert: 1, wait_for_async_insert: 0, materialized_views_ignore_errors: 1 ] config :plausible, Plausible.ImportDeletionRepo, queue_target: 500, queue_interval: 2000, url: ch_db_url, transport_opts: ch_transport_opts, pool_size: 1 config :ex_money, open_exchange_rates_app_id: get_var_from_path_or_env(config_dir, "OPEN_EXCHANGE_RATES_APP_ID"), retrieve_every: :timer.hours(24) case mailer_adapter do "Bamboo.PostmarkAdapter" -> config :plausible, Plausible.Mailer, adapter: Bamboo.PostmarkAdapter, request_options: [recv_timeout: 10_000], api_key: get_var_from_path_or_env(config_dir, "POSTMARK_API_KEY") "Bamboo.MailgunAdapter" -> config :plausible, Plausible.Mailer, adapter: Bamboo.MailgunAdapter, hackney_opts: [recv_timeout: :timer.seconds(10)], api_key: get_var_from_path_or_env(config_dir, "MAILGUN_API_KEY"), domain: get_var_from_path_or_env(config_dir, "MAILGUN_DOMAIN") if mailgun_base_uri = get_var_from_path_or_env(config_dir, "MAILGUN_BASE_URI") do config :plausible, Plausible.Mailer, base_uri: mailgun_base_uri end "Bamboo.MandrillAdapter" -> config :plausible, Plausible.Mailer, adapter: Bamboo.MandrillAdapter, hackney_opts: [recv_timeout: :timer.seconds(10)], api_key: get_var_from_path_or_env(config_dir, "MANDRILL_API_KEY") "Bamboo.SendGridAdapter" -> config :plausible, Plausible.Mailer, adapter: Bamboo.SendGridAdapter, hackney_opts: [recv_timeout: :timer.seconds(10)], api_key: get_var_from_path_or_env(config_dir, "SENDGRID_API_KEY") "Bamboo.SMTPAdapter" -> config :plausible, Plausible.Mailer, adapter: Bamboo.SMTPAdapter, server: get_var_from_path_or_env(config_dir, "SMTP_HOST_ADDR", "mail"), hostname: base_url.host, port: get_var_from_path_or_env(config_dir, "SMTP_HOST_PORT", "25"), username: get_var_from_path_or_env(config_dir, "SMTP_USER_NAME"), password: get_var_from_path_or_env(config_dir, "SMTP_USER_PWD"), tls: :if_available, allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"], ssl: get_bool_from_path_or_env(config_dir, "SMTP_HOST_SSL_ENABLED", false), retries: get_var_from_path_or_env(config_dir, "SMTP_RETRIES") || 2, no_mx_lookups: get_bool_from_path_or_env(config_dir, "SMTP_MX_LOOKUPS_ENABLED", true) "Bamboo.Mua" -> config :plausible, Plausible.Mailer, adapter: Bamboo.Mua # prevents common problems with Erlang's TLS v1.3 middlebox_comp_mode = get_bool_from_path_or_env(config_dir, "SMTP_MIDDLEBOX_COMP_MODE", false) config :plausible, Plausible.Mailer, ssl: [middlebox_comp_mode: middlebox_comp_mode] if relay = get_var_from_path_or_env(config_dir, "SMTP_HOST_ADDR") do port = get_int_from_path_or_env(config_dir, "SMTP_HOST_PORT", 587) ssl_enabled = get_bool_from_path_or_env(config_dir, "SMTP_HOST_SSL_ENABLED") protocol = cond do ssl_enabled -> :ssl is_nil(ssl_enabled) and port == 465 -> :ssl true -> :tcp end config :plausible, Plausible.Mailer, protocol: protocol, relay: relay, port: port end username = get_var_from_path_or_env(config_dir, "SMTP_USER_NAME") password = get_var_from_path_or_env(config_dir, "SMTP_USER_PWD") cond do username && password -> config :plausible, Plausible.Mailer, auth: [username: username, password: password] username || password -> raise ArgumentError, """ Both SMTP_USER_NAME and SMTP_USER_PWD must be set for SMTP authentication. Please provide values for both environment variables. """ _both_nil = true -> nil end "Bamboo.LocalAdapter" -> config :plausible, Plausible.Mailer, adapter: Bamboo.LocalAdapter "Bamboo.TestAdapter" -> config :plausible, Plausible.Mailer, adapter: Bamboo.TestAdapter _ -> raise ArgumentError, """ Unknown mailer_adapter: #{inspect(mailer_adapter)} Please see https://hexdocs.pm/bamboo/readme.html#available-adapters for the list of available adapters that ship with Bamboo """ end base_cron = [ # Daily at midnight {"0 0 * * *", Plausible.Workers.RotateSalts}, # hourly {"0 * * * *", Plausible.Workers.ScheduleEmailReports}, # hourly {"0 * * * *", Plausible.Workers.SendSiteSetupEmails}, # Daily at midday {"0 12 * * *", Plausible.Workers.SendCheckStatsEmails}, # Every 15 minutes {"*/15 * * * *", Plausible.Workers.TrafficChangeNotifier}, # Every day at 1am {"0 1 * * *", Plausible.Workers.CleanInvitations}, # Every 2 hours {"30 */2 * * *", Plausible.Workers.CleanUserSessions}, # Every 2 hours {"0 */2 * * *", Plausible.Workers.ExpireDomainChangeTransitions}, # Daily at midnight {"0 0 * * *", Plausible.Workers.LocationsSync} ] cloud_cron = [ # Daily at midday {"0 12 * * *", Plausible.Workers.SendTrialNotifications}, # Daily at 14 {"0 14 * * *", Plausible.Workers.CheckUsage}, # Daily at 15 {"0 15 * * *", Plausible.Workers.NotifyAnnualRenewal}, # Every midnight {"0 0 * * *", Plausible.Workers.LockSites}, # Daily at 8 {"0 8 * * *", Plausible.Workers.AcceptTrafficUntil}, # First sunday of the month, 4:00 UTC {"0 4 1-7 * SUN", Plausible.Workers.ClickhouseCleanSites} ] crontab = if(is_selfhost, do: base_cron, else: base_cron ++ cloud_cron) base_queues = [ rotate_salts: 1, schedule_email_reports: 1, send_email_reports: 1, spike_notifications: 1, check_stats_emails: 1, site_setup_emails: 1, clean_invitations: 1, clean_user_sessions: 1, analytics_imports: 1, analytics_exports: 1, notify_exported_analytics: 1, domain_change_transition: 1, check_accept_traffic_until: 1, clickhouse_clean_sites: 1, locations_sync: 1 ] cloud_queues = [ trial_notification_emails: 1, check_usage: 1, notify_annual_renewal: 1, lock_sites: 1 ] queues = if(is_selfhost, do: base_queues, else: base_queues ++ cloud_queues) cron_enabled = !disable_cron thirty_days_in_seconds = 60 * 60 * 24 * 30 if config_env() in [:prod, :ce] do config :plausible, Oban, repo: Plausible.Repo, plugins: [ # Keep 30 days history {Oban.Plugins.Pruner, max_age: thirty_days_in_seconds}, {Oban.Plugins.Cron, crontab: if(cron_enabled, do: crontab, else: [])}, # Rescue orphaned jobs after 2 hours {Oban.Plugins.Lifeline, rescue_after: :timer.minutes(120)} ], queues: if(cron_enabled, do: queues, else: []), peer: if(cron_enabled, do: Oban.Peers.Postgres, else: false) else config :plausible, Oban, repo: Plausible.Repo, queues: queues, plugins: false end config :plausible, :hcaptcha, sitekey: hcaptcha_sitekey, secret: hcaptcha_secret nolt_sso_secret = get_var_from_path_or_env(config_dir, "NOLT_SSO_SECRET") config :joken, default_signer: nolt_sso_secret config :plausible, Plausible.Sentry.Client, finch_request_opts: [ pool_timeout: get_int_from_path_or_env(config_dir, "SENTRY_FINCH_POOL_TIMEOUT", 5000), receive_timeout: get_int_from_path_or_env(config_dir, "SENTRY_FINCH_RECEIVE_TIMEOUT", 15000) ] config :ref_inspector, init: {Plausible.Release, :configure_ref_inspector} config :ua_inspector, init: {Plausible.Release, :configure_ua_inspector} if config_env() in [:dev, :staging, :prod, :test] do config :kaffy, otp_app: :plausible, ecto_repo: Plausible.Repo, router: PlausibleWeb.Router, admin_title: "Plausible Admin", extensions: [Plausible.CrmExtensions], resources: [ auth: [ resources: [ user: [schema: Plausible.Auth.User, admin: Plausible.Auth.UserAdmin], api_key: [schema: Plausible.Auth.ApiKey, admin: Plausible.Auth.ApiKeyAdmin] ] ], sites: [ resources: [ site: [schema: Plausible.Site, admin: Plausible.SiteAdmin] ] ], billing: [ resources: [ enterprise_plan: [ schema: Plausible.Billing.EnterprisePlan, admin: Plausible.Billing.EnterprisePlanAdmin ] ] ] ] end geo_opts = cond do maxmind_license_key -> [ license_key: maxmind_license_key, edition: maxmind_edition, cache_dir: persistent_cache_dir, async: true ] ip_geolocation_db -> [path: ip_geolocation_db] true -> raise """ Missing geolocation database configuration. Please set the IP_GEOLOCATION_DB environment value to the location of your IP geolocation .mmdb file: IP_GEOLOCATION_DB=/etc/plausible/dbip-city.mmdb Or authenticate with MaxMind by configuring MAXMIND_LICENSE_KEY and (optionally) MAXMIND_EDITION environment variables: MAXMIND_LICENSE_KEY=LNpsJCCKPis6XvBP MAXMIND_EDITION=GeoLite2-City # this is the default edition """ end config :plausible, Plausible.Geo, geo_opts if geonames_source_file do config :location, :geonames_source_file, geonames_source_file end if honeycomb_api_key && honeycomb_dataset do config :opentelemetry, resource: Plausible.OpenTelemetry.resource_attributes(runtime_metadata), sampler: {Plausible.OpenTelemetry.Sampler, %{ratio: otel_sampler_ratio}}, span_processor: :batch, traces_exporter: :otlp config :opentelemetry_exporter, otlp_protocol: :grpc, otlp_endpoint: "https://api.honeycomb.io:443", otlp_headers: [ {"x-honeycomb-team", honeycomb_api_key}, {"x-honeycomb-dataset", honeycomb_dataset} ] else config :opentelemetry, sampler: :always_off, traces_exporter: :none end config :tzdata, :data_dir, Path.join(persistent_cache_dir || System.tmp_dir!(), "tzdata_data") promex_disabled? = get_bool_from_path_or_env(config_dir, "PROMEX_DISABLED", true) config :plausible, Plausible.PromEx, disabled: promex_disabled?, manual_metrics_start_delay: :no_delay, drop_metrics_groups: [], grafana: :disabled, metrics_server: :disabled config :plausible, Plausible.Verification.Checks.Installation, token: get_var_from_path_or_env(config_dir, "BROWSERLESS_TOKEN", "dummy_token"), endpoint: get_var_from_path_or_env(config_dir, "BROWSERLESS_ENDPOINT", "http://0.0.0.0:3000") if not is_selfhost do site_default_ingest_threshold = case System.get_env("SITE_DEFAULT_INGEST_THRESHOLD") do threshold when byte_size(threshold) > 0 -> {value, ""} = Integer.parse(threshold) value _ -> nil end config :plausible, Plausible.Site, default_ingest_threshold: site_default_ingest_threshold end s3_disabled? = get_bool_from_path_or_env(config_dir, "S3_DISABLED", true) unless s3_disabled? do s3_env = [ %{ name: "S3_ACCESS_KEY_ID", example: "AKIAIOSFODNN7EXAMPLE" }, %{ name: "S3_SECRET_ACCESS_KEY", example: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" }, %{ name: "S3_REGION", example: "us-east-1" }, %{ name: "S3_ENDPOINT", example: "https://.r2.cloudflarestorage.com" }, %{ name: "S3_EXPORTS_BUCKET", example: "my-csv-exports-bucket" }, %{ name: "S3_IMPORTS_BUCKET", example: "my-csv-imports-bucket" } ] s3_env = Enum.map(s3_env, fn var -> Map.put(var, :value, get_var_from_path_or_env(config_dir, var.name)) end) s3_missing_env = Enum.filter(s3_env, &is_nil(&1.value)) unless s3_missing_env == [] do raise ArgumentError, """ Missing S3 configuration. Please set #{s3_missing_env |> Enum.map(& &1.name) |> Enum.join(", ")} environment variable(s): #{s3_missing_env |> Enum.map(fn %{name: name, example: example} -> "\t#{name}=#{example}" end) |> Enum.join("\n")} """ end s3_env_value = fn name -> s3_env |> Enum.find(&(&1.name == name)) |> Map.fetch!(:value) end config :ex_aws, http_client: Plausible.S3.Client, access_key_id: s3_env_value.("S3_ACCESS_KEY_ID"), secret_access_key: s3_env_value.("S3_SECRET_ACCESS_KEY"), region: s3_env_value.("S3_REGION") %URI{scheme: s3_scheme, host: s3_host, port: s3_port} = URI.parse(s3_env_value.("S3_ENDPOINT")) config :ex_aws, :s3, scheme: s3_scheme <> "://", host: s3_host, port: s3_port config :plausible, Plausible.S3, exports_bucket: s3_env_value.("S3_EXPORTS_BUCKET"), imports_bucket: s3_env_value.("S3_IMPORTS_BUCKET") end