# In this file, we load production configuration and secrets # from environment variables. You can also hardcode secrets, # although such is generally not recommended and you have to # remember to add this file to your .gitignore. import Config # Allow any secret to be provided via a file (`_FILE`, e.g. Docker secrets / systemd # LoadCredential) instead of a plain env var. See bonfire-app#1663. alias Bonfire.Common.EnvSecrets IO.puts("Preparing runtime config...") System.get_env("MIX_QUIET") || IO.puts("🔥 Welcome to Bonfire!") yes? = ~w(true yes 1) no? = ~w(false no none 0) # flavour = System.get_env("FLAVOUR", "classic") host = System.get_env("HOSTNAME", "localhost") server_port = String.to_integer(System.get_env("SERVER_PORT", "4000")) public_port = String.to_integer(System.get_env("PUBLIC_PORT", "4000")) test_instance? = System.get_env("TEST_INSTANCE") in yes? federate? = test_instance? or System.get_env("FEDERATE") in yes? hosts = "#{host},#{System.get_env("EXTRA_DOMAINS")}" |> String.replace(["`", " "], "") |> String.split(",") |> Enum.map(&"//#{&1}") EnvSecrets.env_or_file("DATABASE_URL") || EnvSecrets.env_or_file("CLOUDRON_POSTGRESQL_URL") || EnvSecrets.env_or_file("POSTGRES_PASSWORD") || EnvSecrets.env_or_file("CLOUDRON_POSTGRESQL_PASSWORD") || System.get_env("MIX_QUIET") || System.get_env("CI") || raise """ Environment variables for database are missing. For example: DATABASE_URL=ecto://USER:PASS@HOST/DATABASE You can also set POSTGRES_PASSWORD (required), and POSTGRES_USER (default: postgres) and POSTGRES_HOST (default: localhost) """ ## load extensions' runtime configs (and behaviours) directly via extension-provided modules Bonfire.Common.Config.LoadExtensionsConfig.load_configs([Bonfire.RuntimeConfig]) ## secret_key_base = EnvSecrets.env_or_file("SECRET_KEY_BASE") || System.get_env("MIX_QUIET") || System.get_env("CI") || raise """ environment variable SECRET_KEY_BASE is missing. You can generate one by calling: mix phx.gen.secret """ signing_salt = EnvSecrets.env_or_file("SIGNING_SALT") || System.get_env("MIX_QUIET") || System.get_env("CI") || raise """ environment variable SIGNING_SALT is missing. """ encryption_salt = EnvSecrets.env_or_file("ENCRYPTION_SALT") || System.get_env("MIX_QUIET") || System.get_env("CI") || raise """ environment variable ENCRYPTION_SALT is missing. """ cute_gifs_dir = System.get_env("CUTE_GIFS_DIR", "data/uploads/cute-gifs/") config :bonfire, # how many nested replies to show thread_default_max_depth: String.to_integer(System.get_env("THREAD_DEPTH", "3")), feed_live_update_many_preload_mode: :async_actions, host: host, default_cache_hours: String.to_integer(System.get_env("BONFIRE_CACHE_HOURS", "3")), app_name: System.get_env("APP_NAME", "Bonfire"), ap_base_path: System.get_env("AP_BASE_PATH", "/pub"), # github_app_client_id: System.get_env("GITHUB_APP_CLIENT_ID", "Iv1.8d612e6e5a2149c9"), github_token: EnvSecrets.env_or_file("GITHUB_TOKEN"), show_debug_errors_in_dev: System.get_env("SHOW_DEBUG_IN_DEV"), encryption_salt: encryption_salt, signing_salt: signing_salt, root_path: File.cwd!(), cute_gifs: [ num: length(Path.wildcard(cute_gifs_dir <> "*.gif")), dir: cute_gifs_dir ] if System.get_env("ENABLE_STATIC_CACHING") not in yes? do config :bonfire_ui_common, Bonfire.UI.Common.StaticGenerator, modularity: :disabled end phx_server = System.get_env("PHX_SERVER") plug_server = System.get_env("PLUG_SERVER") use_cowboy? = plug_server == "cowboy" use_socket_file? = plug_server == "cowboy_socket" socket_file_path = System.get_env("SOCKET_FILE_PATH", "/tmp/bonfire_socket") phx_compress? = System.get_env("PHX_COMPRESS_HTTP") not in no? if System.get_env("DISABLE_LOG") in yes? do # to suppress non-captured logs in tests (eg. in setup_all) config :logger, backends: [] end http_options = cond do use_socket_file? -> [ ip: {:local, socket_file_path}, port: 0, compress: phx_compress?, transport_options: [ post_listen_callback: fn _ -> File.touch!(socket_file_path) File.chmod!(socket_file_path, 0o660) end, socket_opts: [] ] ] use_cowboy? -> [ # only bind the app to localhost when serving behind a proxy # ip: (if public_port != server_port, do: {127, 0, 0, 1}), port: server_port, compress: phx_compress?, transport_options: [ max_connections: 16_384, socket_opts: [:inet6] ] ] true -> # for bandit [ port: server_port, http_options: [ compress: phx_compress? ] ] end config :bonfire, Bonfire.Web.Endpoint, server: phx_server not in no? and (config_env() != :test or test_instance? or phx_server in yes?), url: [ host: host, port: public_port ], check_origin: hosts, # check_origin: false, adapter: if(use_cowboy?, do: Phoenix.Endpoint.Cowboy2Adapter, else: Bandit.PhoenixAdapter ), http: http_options, thousand_island: [transport_ports: [hibernate_after: to_timeout(second: 15)]], secret_key_base: secret_key_base, live_view: [signing_salt: signing_salt] if test_instance? do test_instance_hostname = System.get_env("TEST_INSTANCE_HOSTNAME", "localhost") test_instance_server_port = String.to_integer(System.get_env("TEST_INSTANCE_SERVER_PORT", "4002")) config :bonfire, Bonfire.Web.FakeRemoteEndpoint, url: [ host: test_instance_hostname, port: if(test_instance_hostname != "localhost", do: public_port, else: test_instance_server_port ) ], http: [ port: test_instance_server_port ], secret_key_base: secret_key_base end # HTTP client(s) configuration finch_conn_opts = [case_sensitive_headers: true] finch_conn_opts = finch_conn_opts ++ case System.get_env("HTTP_PROXY_URL") do nil -> [] uri -> uri = uri |> URI.parse() [proxy: {String.to_existing_atom(uri.scheme), uri.host, uri.port, []}] end finch_pools = %{ :default => [ # Number of connections to maintain in each pool (HTTP1) size: String.to_integer(System.get_env("FINCH_POOL_SIZE", "50")), # Number of pools to start (increasing is only useful for HTTP2, which we don't use by default) count: String.to_integer(System.get_env("FINCH_POOL_COUNT", "1")), # 10 min by default pool_max_idle_time: String.to_integer(System.get_env("FINCH_POOL_MAX_IDLE_TIME", "600000")), conn_opts: finch_conn_opts ], "https://icons.duckduckgo.com" => [ conn_opts: [ transport_opts: [size: 8, timeout: to_timeout(second: 3), conn_opts: finch_conn_opts] ] ], "https://www.google.com/s2/favicons" => [ conn_opts: [ transport_opts: [size: 8, timeout: to_timeout(second: 3), conn_opts: finch_conn_opts] ] ] } # config :tesla, adapter: Tesla.Adapter.Hackney config :bonfire, :finch_pools, finch_pools if config_env() != :test or federate? do config :tesla, :adapter, {Tesla.Adapter.Finch, name: Bonfire.Finch, pools: finch_pools} end oban_notifier = case System.get_env("OBAN_NOTIFIER", "postgres") do "pg" -> Oban.Notifiers.PG "phoenix" -> Oban.Notifiers.Phoenix _postgres -> Oban.Notifiers.Postgres end # disable insert notifications? oban_insert_trigger = System.get_env("OBAN_INSERT_TRIGGER", "true") in ["true", "1", "yes"] # with insert_trigger enabled, polling is just a safety net for future-scheduled jobs and missed # notifications — 30s is sufficient and avoids constant background DB queries oban_stage_interval = System.get_env("OBAN_STAGE_INTERVAL_MS", "30000") |> String.to_integer() # average ms a producer waits before fetching more jobs after a trigger; jitter applies (0-2x actual) oban_dispatch_cooldown = System.get_env("OBAN_DISPATCH_COOLDOWN_MS", "5") |> String.to_integer() # Set OBAN_TESTING=inline to run jobs synchronously on insert (useful for e2e tests — makes federation between servers effectively instant so tests don't need to poll and wait). # Set to "manual" to disable automatic job execution entirely (useful for unit tests). oban_testing = case System.get_env("OBAN_TESTING") do "inline" -> [testing: :inline] "manual" -> [testing: :manual] _ -> [] end config :bonfire, Oban, notifier: oban_notifier, repo: Bonfire.Common.Repo, insert_trigger: oban_insert_trigger, stage_interval: oban_stage_interval, dispatch_cooldown: oban_dispatch_cooldown, queues: [ federator_incoming_mentions: String.to_integer(System.get_env("QUEUE_SIZE_AP_IN_MENTIONS", "3")), federator_incoming_follows: String.to_integer(System.get_env("QUEUE_SIZE_AP_IN_FOLLOWS", "2")), federator_incoming: String.to_integer(System.get_env("QUEUE_SIZE_AP_IN", "3")), federator_incoming_unverified: String.to_integer(System.get_env("QUEUE_SIZE_AP_IN_UNVERIFIED", "1")), federator_outgoing: String.to_integer(System.get_env("QUEUE_SIZE_AP_OUT", "3")), remote_fetcher: String.to_integer(System.get_env("QUEUE_SIZE_AP_FETCH", "1")), import: String.to_integer(System.get_env("QUEUE_SIZE_IMPORT", "1")), deletion: String.to_integer(System.get_env("QUEUE_SIZE_DELETION", "1")), database_prune: String.to_integer(System.get_env("QUEUE_SIZE_DB_PRUNE", "1")), static_generator: String.to_integer(System.get_env("QUEUE_SIZE_STATIC_GEN", "1")), # video_transcode: 1, # boost_activities: 1, fetch_open_science: String.to_integer(System.get_env("QUEUE_SIZE_OPEN_SCIENCE_FETCH", "1")), ghost_webhooks: String.to_integer(System.get_env("QUEUE_SIZE_GHOST_WEBHOOKS", "2")), # batched Meilisearch indexing flush (Bonfire.Search.Workers.IndexWorker); search_index: String.to_integer(System.get_env("QUEUE_SIZE_SEARCH_INDEX", "1")) ], plugins: [ # delete job history after 6 hours {Oban.Plugins.Pruner, max_age: 60 * 60 * 6}, # rescue orphaned jobs {Oban.Plugins.Lifeline, rescue_after: :timer.minutes(60)}, {Oban.Plugins.Cron, crontab: [ {"@daily", ActivityPub.Pruner.PruneDatabaseWorker, max_attempts: 1} ] ++ if Bonfire.Common.Extend.module_enabled?(Bonfire.UI.Common.StaticGenerator) do #  generate static pages for guests every X min interval = if config_env() == :prod, do: 10, else: 60 IO.puts("Static pages will be generated and cached every #{interval} minutes.") [{"*/#{interval} * * * *", Bonfire.UI.Common.StaticGenerator, max_attempts: 3}] else IO.puts("Static pages will not be cached") [] end ++ if Bonfire.Common.Extend.extension_enabled?(:bonfire_open_science) do IO.puts("Open science publications will be fetched for all users once an hour.") [{"@hourly", Bonfire.OpenScience.APIs}] else IO.puts("Open science extension is not enabled") [] end} # ++ # if Bonfire.Common.Extend.module_enabled?(Bonfire.Social.TrendingLinksCacheWorker) do # [{"@hourly", Bonfire.Social.TrendingLinksCacheWorker, max_attempts: 1}] # else # [] # end ] config :activity_pub, :oban_queues, retries: [federator_incoming: 2, federator_outgoing: 3, remote_fetcher: 1] config :activity_pub, Oban, notifier: Oban.Notifiers.PG, # to avoid running it twice queues: false, repo: Bonfire.Common.Repo if oban_testing != [] do config :bonfire, Oban, oban_testing config :activity_pub, Oban, oban_testing end config :activity_pub, :http, user_agent: System.get_env("AP_USER_AGENT", "#{System.get_env("APP_NAME") || Bonfire.Application.name() || "Bonfire"} federation") config :activity_pub, ActivityPub.Federator.HTTP.RateLimit, scale_ms: String.to_integer(System.get_env("AP_RATELIMIT_PER_MS", "10000")), limit: String.to_integer(System.get_env("AP_RATELIMIT_NUM", "20")) case System.get_env("GRAPH_DB_URL") do nil -> nil url -> pool_size = case System.get_env("POOL_SIZE") do pool when is_binary(pool) and pool not in ["", "0"] -> String.to_integer(pool) # default to twice the number of CPU cores _ -> System.schedulers_online() * 2 end # config :bolt_sips, Bolt, # url: url, # basic_auth: [username: "memgraph", password: "memgraph"], # pool_size: pool_size config :boltx, Bolt, uri: url, auth: [username: "memgraph", password: "memgraph"], pool_size: pool_size end if (config_env() == :prod or System.get_env("OTEL_ENABLED") in yes?) and (System.get_env("OTEL_EXPORTER_OTLP_ENDPOINT") || EnvSecrets.env_or_file("OTEL_LIGHTSTEP_API_KEY") || EnvSecrets.env_or_file("OTEL_HONEYCOMB_API_KEY")) do # Enable tracing only when we have a configured endpoint config :opentelemetry, span_processor: :batch, traces_exporter: {:opentelemetry_exporter, %{}} config :opentelemetry_exporter, otlp_protocol: :http_protobuf otel_endpoint = System.get_env("OTEL_EXPORTER_OTLP_ENDPOINT") if otel_endpoint do IO.puts("NOTE: OTLP (open telemetry) data will be sent to #{otel_endpoint}") config :opentelemetry_exporter, otlp_endpoint: otel_endpoint end config :opentelemetry_exporter, # You can configure the compression type for exporting traces. otlp_compression: :gzip, otlp_traces_compression: :gzip if EnvSecrets.env_or_file("OTEL_LIGHTSTEP_API_KEY") do IO.puts("NOTE: OTLP (open telemetry) data will be sent to lightstep / servicenow.com") # Example configuration, for more refer to: https://github.com/open-telemetry/opentelemetry-erlang/tree/main/apps/opentelemetry_exporter#application-environment config :opentelemetry_exporter, otlp_traces_endpoint: "https://ingest.lightstep.com:443/traces/otlp/v0.9", otlp_headers: [ {"lightstep-access-token", EnvSecrets.env_or_file("OTEL_LIGHTSTEP_API_KEY")} ] end if EnvSecrets.env_or_file("OTEL_HONEYCOMB_API_KEY") do IO.puts("NOTE: OTLP (open telemetry) data will be sent to honeycomb.io") config :opentelemetry, :processors, otel_batch_processor: %{ exporter: { :opentelemetry_exporter, %{ endpoints: [ {:https, ~c"api.honeycomb.io", 443, [ verify: :verify_peer, cacertfile: :certifi.cacertfile(), depth: 3, customize_hostname_check: [ match_fun: :public_key.pkix_verify_hostname_match_fun(:https) ] ]} ], headers: [ {"x-honeycomb-team", EnvSecrets.env_or_file!("OTEL_HONEYCOMB_API_KEY")}, {"x-honeycomb-dataset", System.get_env("OTEL_SERVICE_NAME", "bonfire")} ], protocol: :grpc } } } end else # Ensure OpenTelemetry is properly disabled when no endpoints are configured config :opentelemetry, modularity: :disabled, span_processor: :simple, traces_exporter: :none end # Error reporting case System.get_env("SENTRY_DSN", "") do "" -> config :sentry, modularity: :disabled dsn -> IO.puts("NOTE: errors will be reported to Sentry.") config :sentry, dsn: dsn, integrations: [ oban: [ capture_errors: true, cron: [enabled: true] ] ] if System.get_env("SENTRY_NAME") do config :sentry, server_name: System.get_env("SENTRY_NAME") end end config :sentry, source_code_exclude_patterns: [ ~r/\/flavours\//, ~r/\/extensions\//, ~r/\/deps\//, ~r/\/_build\//, ~r/\/priv\//, ~r/\/test\// ] case EnvSecrets.env_or_file("APPSIGNAL_PUSH_API_KEY", "") do "" -> config :appsignal, :config, active: false api_key -> IO.puts("NOTE: errors and performance metrics will be reported to AppSignal.") config :appsignal, :config, otp_app: :bonfire, name: System.get_env("APPSIGNAL_APP_NAME", "bonfire"), push_api_key: api_key, env: config_env(), active: true end config :untangle, env: config_env(), # level: :error, to_io: System.get_env("UNTANGLE_TO_IO") in yes? # start prod-only config if config_env() == :prod do config :bonfire, Bonfire.Common.Repo, # ssl: true, # database: System.get_env("POSTGRES_DB", "bonfire"), # Note: keep this disabled if using ecto_dev_logger or EctoSparkles.Log instead # log: String.to_existing_atom(System.get_env("DB_QUERIES_LOG_LEVEL", "false")) end # end prod only config # start prod and dev only config if config_env() != :test do config :bonfire, Bonfire.Common.Repo, # The timeout for establishing new connections (default: 5000) connect_timeout: String.to_integer(System.get_env("DB_CONNECT_TIMEOUT", "10000")), # The time in milliseconds (as an integer) to wait for the query call to finish (default: 15_000) timeout: String.to_integer(System.get_env("DB_QUERY_TIMEOUT", "20000")), # DBConnection pool tuning - CRITICAL for federation workers under load # queue_target: Time a connection checkout request can wait in queue before DBConnection # starts to reject new requests. Default is 50ms which is too aggressive for high-concurrency # workloads. Setting to 5000ms matches test config and allows patient waiting under load. queue_target: String.to_integer(System.get_env("DB_QUEUE_TARGET", "5000")), # queue_interval: How often to check if queue_target is being exceeded. Default is 1000ms. queue_interval: String.to_integer(System.get_env("DB_QUEUE_INTERVAL", "2000")), # pool_timeout: Overall timeout for getting a connection from the pool. Default is 5000ms. # Increasing to 30000ms allows workers to wait patiently during load spikes. pool_timeout: String.to_integer(System.get_env("DB_POOL_TIMEOUT", "30000")), parameters: [ # Abort any statement that takes more than the specified amount of time. The timeout is measured from the time a command arrives at the server until it is completed by the server. statement_timeout: System.get_env("DB_STATEMENT_TIMEOUT", "20000"), # idle-in-transaction timeout: terminates any session with an open transaction that has been idle for longer than the specified amount of time. This allows any locks held by that session to be released and the connection slot to be reused. WARNING: this seems to also apply to migrations when running in a release, so needs to be high enough for DB migrations and fixtures to run. idle_in_transaction_session_timeout: System.get_env("DB_IDLE_TRANSACTION_TIMEOUT", "120000") ] end ## bonfire_livebook if Code.ensure_loaded?(Livebook) do Livebook.config_runtime() end if api_key = EnvSecrets.env_or_file("PIRATE_WEATHER_API_KEY") do config :forecastr, backend: Forecastr.PirateWeather, appid: api_key, # minutes to cache ttl: to_timeout(minute: 14) end if api_key = EnvSecrets.env_or_file("OPEN_WEATHER_MAP_API_KEY") do config :forecastr, backend: Forecastr.OWM, appid: api_key, # minutes to cache ttl: to_timeout(minute: 14) end db_ssl? = System.get_env("DB_SSL") in ~w(true yes 1) if db_ssl? do ssl_opts = case System.get_env("DB_CA_CERT_FILE") do nil -> true "" -> true path -> [cacertfile: path] end config :bonfire, Bonfire.Common.Repo, ssl: ssl_opts end IO.puts("Runtime config ready")