Configuration
Need environment-specific configuration? Elixir provides compile-time and runtime config with environment variable support, enabling flexible configuration management across development, staging, and production environments.
Prerequisites
- Understanding of Mix projects
- Basic knowledge of environment variables
- Familiarity with deployment concepts
Problem
Applications need different configurations for development, testing, staging, and production. Managing secrets, environment-specific settings, and runtime configuration is challenging.
Challenges:
- Separating compile-time and runtime configuration
- Managing secrets securely
- Supporting multiple environments
- Handling environment variables
- Avoiding hard-coded configuration
Solution
Use Config module with compile-time config files and runtime.exs for environment-specific and runtime configuration.
How It Works
1. Compile-Time Configuration
import Config
config :my_app,
ecto_repos: [MyApp.Repo],
generators: [timestamp_type: :utc_datetime]
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
import_config "#{config_env()}.exs"Development config:
import Config
config :my_app, MyApp.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "my_app_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
config :my_app, MyAppWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [
esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
]
config :logger, :console, format: "[$level] $message\n"
config :phoenix, :stacktrace_depth, 20
config :phoenix, :plug_init_mode, :runtimeTest config:
import Config
config :my_app, MyApp.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "my_app_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
config :my_app, MyAppWeb.Endpoint,
http: [port: 4002],
server: false
config :logger, level: :warning
config :phoenix, :plug_init_mode, :runtimeProduction config:
import Config
config :my_app, MyAppWeb.Endpoint,
cache_static_manifest: "priv/static/cache_manifest.json"
config :logger, level: :info2. Runtime Configuration
import Config
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
config :my_app, MyApp.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6
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"
port = String.to_integer(System.get_env("PORT") || "4000")
config :my_app, MyAppWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"],
http: [
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: port
],
secret_key_base: secret_key_base
# External service API keys
config :my_app, :stripe,
api_key: System.get_env("STRIPE_API_KEY"),
webhook_secret: System.get_env("STRIPE_WEBHOOK_SECRET")
config :my_app, :aws,
access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
region: System.get_env("AWS_REGION") || "us-east-1"
end3. Environment Variables
database_url = System.get_env("DATABASE_URL")
port = System.get_env("PORT", "4000")
secret = System.fetch_env!("SECRET_KEY_BASE")
pool_size = System.get_env("POOL_SIZE", "10") |> String.to_integer()
enable_feature = System.get_env("ENABLE_FEATURE") in ["true", "1"]
api_key =
System.get_env("API_KEY") ||
System.get_env("LEGACY_API_KEY") ||
raise "API key not configured"Using with ExUnit:
System.put_env("DATABASE_URL", "ecto://postgres:postgres@localhost/my_app_test")
System.put_env("SECRET_KEY_BASE", "test_secret_key_base_at_least_64_bytes_long_test")
ExUnit.start()Advanced Patterns
1. Application Environment
config :my_app, :settings,
timeout: 5000,
retries: 3,
api_url: "https://api.example.com"
defmodule MyApp.Client do
@timeout Application.compile_env(:my_app, [:settings, :timeout])
@retries Application.compile_env(:my_app, [:settings, :retries])
def fetch_data do
api_url = Application.fetch_env!(:my_app, [:settings, :api_url])
with {:ok, response} <- HTTPoison.get(api_url, [], timeout: @timeout) do
{:ok, response.body}
end
end
end
def get_setting(key) do
Application.get_env(:my_app, :settings)
|> Keyword.get(key)
end2. Feature Flags
config :my_app, :features,
new_dashboard: false,
experimental_api: false,
beta_features: false
if config_env() == :prod do
config :my_app, :features,
new_dashboard: System.get_env("FEATURE_NEW_DASHBOARD") == "true",
experimental_api: System.get_env("FEATURE_EXPERIMENTAL_API") == "true",
beta_features: System.get_env("FEATURE_BETA") == "true"
end
defmodule MyAppWeb.DashboardLive do
def mount(_params, _session, socket) do
if Application.get_env(:my_app, :features)[:new_dashboard] do
{:ok, assign(socket, :view, :new_dashboard)}
else
{:ok, assign(socket, :view, :legacy_dashboard)}
end
end
end3. Multi-Tenant Configuration
config :my_app, :tenants, [
%{
id: "tenant_a",
database: System.get_env("TENANT_A_DATABASE"),
api_key: System.get_env("TENANT_A_API_KEY")
},
%{
id: "tenant_b",
database: System.get_env("TENANT_B_DATABASE"),
api_key: System.get_env("TENANT_B_API_KEY")
}
]
defmodule MyApp.Tenants do
def get_config(tenant_id) do
Application.get_env(:my_app, :tenants)
|> Enum.find(&(&1.id == tenant_id))
end
def get_repo(tenant_id) do
config = get_config(tenant_id)
# Return appropriate repo based on config
end
end4. Configuration Providers
defmodule MyApp.ConfigProvider do
@behaviour Config.Provider
def init(path) when is_binary(path) do
path
end
def load(config, path) do
# Load configuration from external source
# (e.g., AWS Secrets Manager, Vault, etc.)
external_config = load_from_source(path)
Config.Reader.merge(config, external_config)
end
defp load_from_source(path) do
# Implementation for loading config
[
my_app: [
secret_key_base: fetch_secret("secret_key_base"),
database_url: fetch_secret("database_url")
]
]
end
defp fetch_secret(key) do
# Fetch from external secret manager
end
end
def project do
[
releases: [
my_app: [
config_providers: [
{MyApp.ConfigProvider, "/etc/my_app/secrets.json"}
]
]
]
]
endReal-World Examples
1. Database Configuration
if config_env() == :prod do
# Parse DATABASE_URL
database_url = System.get_env("DATABASE_URL") || raise "DATABASE_URL not set"
config :my_app, MyApp.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
ssl: System.get_env("DATABASE_SSL") in ["true", "1"],
queue_target: 500,
queue_interval: 1000,
timeout: 15_000,
connect_timeout: 5_000,
handshake_timeout: 5_000,
pool_timeout: 5_000
# Read replica configuration
if replica_url = System.get_env("DATABASE_REPLICA_URL") do
config :my_app, MyApp.ReadRepo,
url: replica_url,
pool_size: String.to_integer(System.get_env("READ_POOL_SIZE") || "5"),
ssl: true
end
end2. External Services Configuration
config :my_app, :services,
stripe: %{
api_key: System.get_env("STRIPE_API_KEY"),
webhook_secret: System.get_env("STRIPE_WEBHOOK_SECRET"),
enabled: System.get_env("STRIPE_ENABLED") in ["true", "1"]
},
sendgrid: %{
api_key: System.get_env("SENDGRID_API_KEY"),
from_email: System.get_env("FROM_EMAIL") || "noreply@example.com"
},
s3: %{
bucket: System.get_env("S3_BUCKET"),
region: System.get_env("AWS_REGION") || "us-east-1",
access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY")
},
redis: %{
url: System.get_env("REDIS_URL") || "redis://localhost:6379",
pool_size: String.to_integer(System.get_env("REDIS_POOL_SIZE") || "10")
}
defmodule MyApp.Services do
def stripe_config do
Application.get_env(:my_app, :services)[:stripe]
end
def stripe_enabled? do
stripe_config()[:enabled]
end
def sendgrid_config do
Application.get_env(:my_app, :services)[:sendgrid]
end
end3. Logging Configuration
log_level =
case System.get_env("LOG_LEVEL") do
"debug" -> :debug
"info" -> :info
"warn" -> :warning
"error" -> :error
_ -> if config_env() == :prod, do: :info, else: :debug
end
config :logger, level: log_level
config :logger, :console,
format: {MyApp.LogFormatter, :format},
metadata: [:request_id, :user_id, :tenant_id]
if config_env() == :prod do
if sentry_dsn = System.get_env("SENTRY_DSN") do
config :sentry,
dsn: sentry_dsn,
environment_name: config_env(),
enable_source_code_context: true,
root_source_code_path: File.cwd!(),
tags: %{
env: to_string(config_env())
}
end
if logflare_api_key = System.get_env("LOGFLARE_API_KEY") do
config :logflare,
api_key: logflare_api_key,
source_id: System.get_env("LOGFLARE_SOURCE_ID")
end
end4. Phoenix Endpoint Configuration
if config_env() == :prod do
host = System.get_env("PHX_HOST") || raise "PHX_HOST not set"
port = String.to_integer(System.get_env("PORT") || "4000")
# Force SSL configuration
force_ssl = System.get_env("FORCE_SSL") in ["true", "1"]
config :my_app, MyAppWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"],
http: [
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: port,
protocol_options: [max_header_value_length: 8192]
],
secret_key_base: System.fetch_env!("SECRET_KEY_BASE"),
server: true,
force_ssl: if(force_ssl, do: [rewrite_on: [:x_forwarded_proto]], else: false)
# Session configuration
config :my_app, MyAppWeb.Endpoint,
live_view: [signing_salt: System.fetch_env!("LIVE_VIEW_SALT")],
session_options: [
key: "_my_app_key",
signing_salt: System.fetch_env!("SESSION_SALT"),
same_site: "Lax",
secure: force_ssl,
max_age: 86400 * 30 # 30 days
]
# CORS configuration
if allowed_origins = System.get_env("ALLOWED_ORIGINS") do
origins = String.split(allowed_origins, ",")
config :cors_plug, origin: origins
end
endConfiguration Validation
defmodule MyApp.Config do
@required_env_vars [
"DATABASE_URL",
"SECRET_KEY_BASE",
"PHX_HOST"
]
@optional_env_vars [
"PORT",
"POOL_SIZE",
"LOG_LEVEL"
]
def validate! do
missing = Enum.reject(@required_env_vars, &System.get_env/1)
unless Enum.empty?(missing) do
raise """
Missing required environment variables:
#{Enum.join(missing, "\n")}
"""
end
:ok
end
def required_vars, do: @required_env_vars
def optional_vars, do: @optional_env_vars
end
def start(_type, _args) do
MyApp.Config.validate!()
# ... rest of start function
endCommon Pitfalls
1. Hard-Coding Secrets
Problem:
config :my_app, :stripe,
api_key: "sk_live_abc123..." # Never do this!Solution:
config :my_app, :stripe,
api_key: System.get_env("STRIPE_API_KEY")2. Compile-Time Environment Variables
Problem:
config :my_app, :api_url, System.get_env("API_URL")Solution:
if config_env() == :prod do
config :my_app, :api_url, System.get_env("API_URL")
end3. Missing Environment Variables
Problem:
database_url = System.get_env("DATABASE_URL")Solution:
database_url =
System.get_env("DATABASE_URL") ||
raise "DATABASE_URL environment variable is required"
database_url = System.fetch_env!("DATABASE_URL")Configuration Best Practices
- Use runtime.exs for production - Read environment variables at runtime
- Never commit secrets - Use environment variables for sensitive data
- Validate required config - Fail fast if required configuration is missing
- Provide defaults - Use sensible defaults for optional configuration
- Document environment variables - List all required and optional variables in README
- Use different configs per environment - Separate dev/test/prod configuration
- Keep config DRY - Extract common configuration patterns
- Version configuration files - Track config changes in version control (except secrets)
Environment Variable Documentation
Create a .env.example file:
DATABASE_URL=ecto://postgres:postgres@localhost/my_app_dev
POOL_SIZE=10
SECRET_KEY_BASE=
PHX_HOST=localhost
PORT=4000
STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=
SENDGRID_API_KEY=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=us-east-1
S3_BUCKET=
FEATURE_NEW_DASHBOARD=false
FEATURE_EXPERIMENTAL_API=false
LOG_LEVEL=info
SENTRY_DSN=Testing Configuration
defmodule MyApp.ConfigTest do
use ExUnit.Case
test "validates required environment variables" do
# Save current env
original_db_url = System.get_env("DATABASE_URL")
# Remove required var
System.delete_env("DATABASE_URL")
assert_raise RuntimeError, ~r/DATABASE_URL/, fn ->
MyApp.Config.validate!()
end
# Restore
if original_db_url do
System.put_env("DATABASE_URL", original_db_url)
end
end
test "loads stripe configuration" do
config = MyApp.Services.stripe_config()
assert is_binary(config[:api_key])
assert is_boolean(config[:enabled])
end
endRelated Resources
Last updated