15.5 KiB · text 5af55d9
import Config
# config/runtime.exs is executed for all environments, including
# during releases. It is executed after compilation and before the
# system starts, so it is typically used to load production configuration
# and secrets from environment variables or elsewhere. Do not define
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.
# ## Using releases
#
# If you use `mix release`, you need to explicitly enable the server
# by passing the PHX_SERVER=true when you start it:
#
# PHX_SERVER=true bin/git_gud start
#
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
# script that automatically sets the env var above.
if System.get_env("PHX_SERVER") do
config :git_gud, GitGudWeb.Endpoint, server: true
end
config :git_gud, GitGudWeb.Endpoint,
http: [port: String.to_integer(System.get_env("PORT", "4000"))]
if config_env() == :prod do
# Two ways to point at Postgres:
#
# 1. `DATABASE_URL` — single connection-string env, classic Phoenix.
# 2. Split `POSTGRES_HOST` / `POSTGRES_PORT` / `POSTGRES_USER` /
# `POSTGRES_PASSWORD` / `POSTGRES_DB` — friendlier under k8s when
# passwords come from a Secret and contain URL-hostile chars.
#
# `DATABASE_URL` wins when both are set.
database_url =
System.get_env("DATABASE_URL") ||
(case {System.get_env("POSTGRES_HOST"), System.get_env("POSTGRES_USER"),
System.get_env("POSTGRES_PASSWORD"), System.get_env("POSTGRES_DB")} do
{host, user, pass, db} when is_binary(host) and is_binary(user) and
is_binary(pass) and is_binary(db) ->
port = System.get_env("POSTGRES_PORT") || "5432"
"ecto://#{URI.encode_www_form(user)}:#{URI.encode_www_form(pass)}@#{host}:#{port}/#{db}"
_ ->
nil
end) ||
raise """
No database configured. Set DATABASE_URL, or supply the
POSTGRES_HOST / POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB
bundle (POSTGRES_PORT defaults to 5432).
"""
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
config :git_gud, GitGud.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
# For machines with several cores, consider starting multiple pools of `pool_size`
# pool_count: 4,
socket_options: maybe_ipv6
# Persistent paths — point these at mounted PVCs in k8s, or anywhere
# writable on a single-node host. Leave unset to keep the in-release
# `priv/` defaults (fine for dev, not durable in a release).
if path = System.get_env("GIT_REPOS_PATH") do
config :git_gud, GitGud.Repositories.Storage, base_path: path
end
if root = System.get_env("STORAGE_DISK_ROOT") do
config :git_gud, GitGud.Storage.Disk, root: root
end
# Embedded SSH daemon. Off by default in prod (you may front it with
# an external sshd / dedicated TCP service); flip the env to opt in.
# `SSH_HOST_KEY_DIR` should point at *shared* storage when running
# multi-replica so every pod presents the same host fingerprint —
# otherwise SSH clients get a "remote host identification changed"
# warning when their load-balancer connection lands on a new pod.
# `SSH_PORT` is what the embedded daemon listens on inside the pod.
# `SSH_EXTERNAL_PORT` is what clients see — i.e., whatever port your
# external LB / Traefik / ingress maps inbound to the daemon. The
# UI renders the *external* port in clone URLs. Same idea for
# `SSH_EXTERNAL_HOST` vs the Endpoint URL host.
parse_port = fn key ->
case System.get_env(key) do
nil -> nil
str -> String.to_integer(str)
end
end
ssh_overrides =
[enabled: System.get_env("SSH_SERVER_ENABLED") in ~w(true 1)]
|> Keyword.merge(
case System.get_env("SSH_HOST_KEY_DIR") do
nil -> []
dir -> [host_key_dir: dir]
end
)
|> Keyword.merge(
case parse_port.("SSH_PORT") do
nil -> []
p -> [port: p]
end
)
|> Keyword.merge(
case parse_port.("SSH_EXTERNAL_PORT") do
nil -> []
p -> [external_port: p]
end
)
|> Keyword.merge(
case System.get_env("SSH_EXTERNAL_HOST") do
nil -> []
h -> [external_host: h]
end
)
config :git_gud, GitGud.Ssh.Server, ssh_overrides
# Registration mode — :invite_only in code defaults; commonly flipped
# at deploy time. Accepts the literal strings "open", "invite_only",
# or "closed"; anything else falls back to :invite_only.
if mode = System.get_env("REGISTRATION_MODE") do
config :git_gud, registration_mode: mode
end
# OCI registry token signing key. Generated by
# `mix gitgud.registry.keygen`; the matching public PEM lives in the
# `zot` Secret (mounted at /etc/zot/registry-pub.pem inside the zot
# pod). Unset → `Packages.Token.issue/2` raises on first `/v2/token`
# request, but the rest of the app boots fine, so we don't fail
# boot here either.
if pem = System.get_env("GITGUD_REGISTRY_PRIVATE_KEY") do
config :git_gud, GitGud.Packages.Token, private_key_pem: pem
end
# Registry host injected into CI jobs as `vars.CI_REGISTRY` (e.g.
# "ggr.neiam.co") so workflows reference ${{ vars.CI_REGISTRY }}
# without hardcoding or per-repo config.
if host = System.get_env("GITGUD_REGISTRY_HOST") do
config :git_gud, :registry_host, host
end
# zot → Packages mirror webhook shared secret. zot sends it as
# `Authorization: Bearer <secret>`; `PackagesWebhookController`
# secure-compares against this value. Unset = no auth (open
# endpoint); fine on a private cluster network, otherwise set it.
if zot_secret = System.get_env("ZOT_WEBHOOK_SECRET") do
config :git_gud, GitGudWeb.PackagesWebhookController, shared_secret: zot_secret
end
# Where forgejo-runner clones `uses: <owner>/<repo>@<ref>` action
# references from. Default is code.forgejo.org (Forgejo's mirror of
# common actions). Set to `https://github.com` to use GitHub
# canonical, or to your own instance once you mirror them locally.
if actions_url = System.get_env("GITGUD_DEFAULT_ACTIONS_URL") do
config :git_gud, :default_actions_url, actions_url
end
# Sentry error tracking. Unset -> Sentry no-ops.
config :sentry, dsn: System.get_env("SENTRY_DSN")
# The secret key base is used to sign/encrypt cookies and other secrets.
# A default value is used in config/dev.exs and config/test.exs but you
# want to use a different value for prod and you most likely don't want
# to check this value into version control, so we use an environment
# variable instead.
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
"""
host = System.get_env("PHX_HOST") || "example.com"
config :git_gud, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
# libcluster — Kubernetes endpoints lookup against the headless
# service deployed alongside the app. RBAC (pods + endpoints) is
# granted to the `git-gud` ServiceAccount in app.yml.
#
# Gated on `LIBCLUSTER_KUBERNETES=1`. Off → `GitGud.Application` skips
# Cluster.Supervisor entirely and `GitGud.Cache.L2` runs as a single-
# member `pg` group, which is exactly right on a single node.
if System.get_env("LIBCLUSTER_KUBERNETES") in ~w(true 1) do
namespace = System.get_env("LIBCLUSTER_NAMESPACE") || "git-gud"
service = System.get_env("LIBCLUSTER_SERVICE") || "git-gud-headless"
selector = System.get_env("LIBCLUSTER_SELECTOR") || "name=git-gud"
basename = System.get_env("LIBCLUSTER_BASENAME") || "git_gud"
config :libcluster,
topologies: [
git_gud: [
strategy: Cluster.Strategy.Kubernetes,
config: [
mode: :ip,
kubernetes_selector: selector,
kubernetes_service_name: service,
kubernetes_node_basename: basename,
kubernetes_namespace: namespace,
polling_interval: 10_000
]
]
]
end
config :git_gud, GitGudWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"],
http: [
# Enable IPv6 and bind on all interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
# See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
ip: {0, 0, 0, 0, 0, 0, 0, 0}
],
secret_key_base: secret_key_base
# Mailer adapter selection:
#
# * SMTP_RELAY set → Swoosh.Adapters.SMTP (via gen_smtp)
# * otherwise → Swoosh.Adapters.Logger
#
# The Logger adapter doesn't actually send mail — it logs the
# rendered email at :info — but it gets us off Swoosh.Adapters.Local
# (which is what the dev mailbox uses), so the magic-link flow at
# least leaves a trail in the pod log.
# Literal branches (not `String.to_existing_atom/1`) so the atom
# table is populated by the compiler. `bin/git_gud eval` evaluates
# runtime.exs *before* applications are loaded — at that point the
# atom table only contains atoms baked into the release, so any
# dynamic-to-atom conversion of a not-yet-seen value blows up at
# boot. `start` worked because Swoosh.Adapters.SMTP gets loaded
# earlier in that codepath, priming the atoms incidentally.
smtp_tri = fn var, default ->
case System.get_env(var, default) do
"always" -> :always
"if_available" -> :if_available
"never" -> :never
other -> raise "#{var} must be one of always|if_available|never, got: #{inspect(other)}"
end
end
# Eager-load the OS CA bundle into OTP's `public_key` cache. Without
# this, `:public_key.cacerts_get/0` returns `:undefined`, and
# `gen_smtp`'s internal default `tls_options` injects
# `cacerts: :undefined` — which `:ssl` then rejects as incompatible
# with `verify: :verify_peer`, clobbering our explicit `cacertfile`.
case :public_key.cacerts_load() do
:ok ->
:ok
{:error, reason} ->
require Logger
Logger.warning("public_key:cacerts_load failed: #{inspect(reason)} — TLS verify may fail")
end
# CA bundle for TLS verification. We pass both `cacertfile` (path)
# and rely on the cacerts_load above for `cacerts_get/0` to work,
# belt-and-braces. Override `SMTP_CACERTFILE` if your image puts the
# bundle elsewhere; set `SMTP_TLS_VERIFY=verify_none` for self-signed
# / private relays where you'd rather skip cert validation.
smtp_cacertfile =
System.get_env("SMTP_CACERTFILE", "/etc/ssl/certs/ca-certificates.crt")
smtp_verify =
case System.get_env("SMTP_TLS_VERIFY", "verify_peer") do
v when v in ~w(verify_peer verify_none) -> String.to_existing_atom(v)
other -> raise "SMTP_TLS_VERIFY must be verify_peer or verify_none, got: #{inspect(other)}"
end
# Resolve the CA store NOW (after `cacerts_load`) rather than letting
# the downstream stack call `cacerts_get/0` lazily. gen_smtp / ssl
# has a habit of evaluating it in a context where it still returns
# `:undefined`, which then collides with our `cacertfile`. Passing
# `cacerts: <list>` explicitly is unambiguous — `:ssl` honors it
# and never re-derives.
smtp_cacerts =
case :public_key.cacerts_get() do
certs when is_list(certs) and certs != [] -> certs
_ -> nil
end
mailer_opts =
if relay = System.get_env("SMTP_RELAY") do
tls_options =
[
verify: smtp_verify,
server_name_indication: String.to_charlist(relay),
depth: 99
] ++
cond do
smtp_verify != :verify_peer ->
[]
is_list(smtp_cacerts) ->
[cacerts: smtp_cacerts]
true ->
[cacertfile: smtp_cacertfile]
end
# Pin modern TLS at both the top level (read by gen_smtp before
# it calls :ssl.connect) and inside tls_options (read by :ssl
# itself). Matches Mobilizon's working pattern.
tls_versions = [:"tlsv1.2", :"tlsv1.3"]
# gen_smtp 1.2 has two distinct connection paths with different
# option keys:
#
# * `ssl: true` (port 465 implicit TLS) reads its TLS opts
# from `sockopts` — see gen_smtp_client.erl:848. `tls_options`
# is ignored on this path.
# * `tls: :always|:if_available` (STARTTLS upgrade) reads from
# `tls_options` — only used during do_STARTTLS.
#
# Without sockopts on the implicit-TLS path, OTP 28's `:ssl`
# falls through to its own defaults (verify_peer +
# `public_key.cacerts_get()` which returns `:undefined`) and
# bails with `{options, incompatible, [verify: verify_peer,
# cacerts: undefined]}`. Putting the same opts on both keys
# covers both relay setups.
ssl_opts = [{:versions, tls_versions} | tls_options]
[
adapter: Swoosh.Adapters.SMTP,
relay: relay,
port: String.to_integer(System.get_env("SMTP_PORT", "587")),
username: System.get_env("SMTP_USERNAME"),
password: System.get_env("SMTP_PASSWORD"),
tls: smtp_tri.("SMTP_TLS", "if_available"),
ssl: System.get_env("SMTP_SSL", "false") == "true",
auth: smtp_tri.("SMTP_AUTH", "if_available"),
allowed_tls_versions: tls_versions,
sockopts: ssl_opts,
tls_options: ssl_opts,
retries: 1,
no_mx_lookups: false
]
else
[adapter: Swoosh.Adapters.Logger, level: :info]
end
config :git_gud, GitGud.Mailer, mailer_opts
config :git_gud, GitGud.Mailer,
from_name: System.get_env("MAIL_FROM_NAME", "GitGud"),
from_address: System.get_env("MAIL_FROM_ADDRESS") || "noreply@#{host}"
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to your endpoint configuration:
#
# config :git_gud, GitGudWeb.Endpoint,
# https: [
# ...,
# port: 443,
# cipher_suite: :strong,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
# ]
#
# The `cipher_suite` is set to `:strong` to support only the
# latest and more secure SSL ciphers. This means old browsers
# and clients may not be supported. You can set it to
# `:compatible` for wider support.
#
# `:keyfile` and `:certfile` expect an absolute path to the key
# and cert in disk or a relative path inside priv, for example
# "priv/ssl/server.key". For all supported SSL configuration
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
#
# We also recommend setting `force_ssl` in your config/prod.exs,
# ensuring no data is ever sent via http, always redirecting to https:
#
# config :git_gud, GitGudWeb.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
# ## Configuring the mailer
#
# In production you need to configure the mailer to use a different adapter.
# Here is an example configuration for Mailgun:
#
# config :git_gud, GitGud.Mailer,
# adapter: Swoosh.Adapters.Mailgun,
# api_key: System.get_env("MAILGUN_API_KEY"),
# domain: System.get_env("MAILGUN_DOMAIN")
#
# Most non-SMTP adapters require an API client. Swoosh supports Req, Hackney,
# and Finch out-of-the-box. This configuration is typically done at
# compile-time in your config/prod.exs:
#
# config :swoosh, :api_client, Swoosh.ApiClient.Req
#
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
end