Skip to content
docuconf
docuconf on GitHub

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

Every docuconf SDK is a v0.1 alpha and none is on a package registry yet, so step 1 installs from the 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
mix.exs
# 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.

lib/orders/env.ex
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
end

3.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.

config/runtime.exs
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_level

4.Run it, and see an error

Run the example with a valid environment:

DATABASE_URL=postgres://orders:pw@localhost:5432/orders mix run --no-halt

With 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.

Terminal
$ 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 1

5.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.

test/orders_env_test.exs
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
end
mix test --no-start

6.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
contract.cue
// 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

Every snippet on this page is compiled or run against docuconf-elixir's main branch in CI. Facts checked 2026-10-08.