Get started / Elixir
docuconf for Elixir
You keep config/runtime.exs. docuconf gives it one declaration, in the NimbleOptions style, of every variable and file the app reads, checks them all at boot, and exports the contract with mix docuconf.export.
- Builds on
- config/runtime.exs
- Package
- docuconf (Hex)
- Requires
- Elixir 1.18, OTP 25
- Status
- v0.1 alpha, not yet released · conformance 134 of 134
- Source
- docuconf-elixir · the orders example
Not published yet
main branch. After the first release it will be {:docuconf, "~> 0.1"}.The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.
1.Install
The package is not on Hex yet. Take it from the main branch (Elixir 1.18+, OTP 25+); it has no runtime dependencies.
mix deps.get# Until the first release, from the main branch:
{:docuconf, github: "Docuconf/docuconf-elixir", branch: "main"}2.Declare your configuration
A module with use Docuconf and one env or secret per variable: a type, a description and options such as min, max, schemes and default. The name is the field upcased. A @doc above an env works instead of description:: its first paragraph is the description and the rest the details.
defmodule Orders.Env do
@moduledoc "Every environment variable the orders service reads."
use Docuconf, name: "orders"
env :port, :integer, description: "HTTP listen port", default: 8080, min: 1, max: 65535
# Atom values come back as atoms, ready for Logger.
env :log_level, {:in, [:debug, :info, :warning, :error]},
description: "Minimum log level",
default: :info
# A secret: docuconf never prints its value (not in errors, and not when
# Orders.Env is inspected or logged), and the contract tells the platform
# to supply it from a Kubernetes Secret.
secret :database_url, :url,
description: "Postgres connection string",
required: true,
schemes: ["postgres"],
max_length: 2048
env :allowed_origins, {:list, :string},
description: "Origins allowed to call the API (CORS)",
min_items: 1,
default: ["http://localhost:3000"]
@doc """
Time limit for one request.
Raise it when clients upload large order batches. Keep it below the load
balancer's idle timeout, or the client sees a reset rather than a `504`.
"""
env :request_timeout, :duration,
min: "1s",
max: "5m",
default: "30s"
env :worker_count, :integer,
description: "Order processing workers",
min: 1,
max: 64,
default: 4
end3.Load it at boot
Call load!/0 in config/runtime.exs and put the values into your app's config, as you would with System.fetch_env!/1.
import Config
# Every variable is validated here, at boot. All problems are reported
# together, the process exits with status 1, and the report is written to
# /dev/termination-log in Kubernetes.
env = Orders.Env.load!()
config :orders, env: env
config :logger, level: env.log_level4.Run it, and see an error
Run the example with a valid environment:
DATABASE_URL=postgres://orders:pw@localhost:5432/orders mix run --no-haltWith PORT=0 and no DATABASE_URL, load! prints every problem with its error code, without a stack trace, and the process exits 1. The report also goes to /dev/termination-log.
$ PORT=0 mix run --no-halt
...
docuconf: 2 configuration problems:
- DATABASE_URL [missing_required]: required, but not set
- PORT [out_of_range]: "0" is below min 15.Test your configuration
load/1 takes an env: map instead of the process environment, and returns {:error, %Docuconf.ValidationError{}} with every violation. Note that config/runtime.exs runs for mix test too: load there only if config_env() != :test, or give the test run a valid environment.
defmodule Orders.EnvTest do
use ExUnit.Case, async: true
# load/1 with an explicit env: map never reads or changes the process environment.
test "defaults" do
{:ok, env} = Orders.Env.load(env: %{"DATABASE_URL" => "postgres://orders@db/orders"})
assert env.port == 8080
assert env.worker_count == 4
end
test "rejects bad values" do
{:error, error} = Orders.Env.load(env: %{"PORT" => "70000"})
found = Enum.map(error.violations, &{&1.input, &1.code})
assert {"PORT", :out_of_range} in found
assert {"DATABASE_URL", :missing_required} in found
end
endmix test --no-start6.Export the contract
mix docuconf.export writes contract.cue for the module named in mix.exs (docuconf: [module: Orders.Env]). In CI, mix docuconf.export --check fails when the committed file is out of date.
mix docuconf.export// Code generated by docuconf. DO NOT EDIT.
package orders
import "docuconf.dev/contract"
contract.#Contract & {
apiVersion: "docuconf.dev/v1alpha1"
kind: "ConfigContract"
metadata: {
name: "orders"
appVersion: "1.0.0"
generator: {
language: "elixir"
sdk: "docuconf"
version: "0.1.0"
}
}
vars: {
ALLOWED_ORIGINS: {
type: "list"
description: "Origins allowed to call the API (CORS)"
items: "string"
encoding: "csv"
separator: ","
minItems: 1
default: ["http://localhost:3000"]
}
DATABASE_URL: {
type: "url"
description: "Postgres connection string"
required: true
secret: true
schemes: ["postgres"]
maxLength: 2048
}
LOG_LEVEL: {
type: "enum"
description: "Minimum log level"
values: ["debug", "info", "warning", "error"]
default: "info"
}
PORT: {
type: "int"
description: "HTTP listen port"
min: 1
max: 65535
default: 8080
}
REQUEST_TIMEOUT: {
type: "duration"
description: "Time limit for one request"
details: "Raise it when clients upload large order batches. Keep it below the load\nbalancer's idle timeout, or the client sees a reset rather than a `504`."
encoding: "go"
min: "1s"
max: "5m"
default: "30s"
}
WORKER_COUNT: {
type: "int"
description: "Order processing workers"
min: 1
max: 64
default: 4
}
}
}The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.
7.Phoenix and releases
Phoenix reads its runtime configuration in config/runtime.exs already: call MyApp.Env.load!() at the top and pass the values to MyAppWeb.Endpoint, MyApp.Repo and the rest. It runs the same way in a mix release, at boot, after secrets are injected. A TLS file input gives the certfile and keyfile paths Cowboy and Bandit expect. See the README.
Next
- The docuconf-elixir README: every type, file input and option.
- The orders example, with its smoke test.
- The same service in every language, side by side.
- What every SDK supports, and this one's known gaps: Watched files reload only while the app supervises Docuconf.Watcher.
Every snippet on this page is compiled or run against docuconf-elixir's main branch in CI. Facts checked 2026-10-08.