View Source Dotenvy behaviour (Dotenvy v0.8.0)
Dotenvy
is an Elixir port of the original dotenv Ruby gem.
It is designed to help applications follow the principles of the 12-factor app and its recommendation to store configuration in the environment.
Unlike other configuration helpers, Dotenvy
enforces no convention for the naming
of your files: .env
is a common choice, you may name your configuration files whatever
you wish.
See the strategies for examples of various use cases.
Link to this section Summary
Types
An input source may be either a path to an env file or a map with string keys,
e.g. "envs/.env"
or %{"FOO" => "bar"}
. This allows users to specify a list
of env files interspersed with other values from other sources, e.g. System.get_env()
or values fetched from a secure parameter store.
Callbacks
A parser implementation should receive the contents
read from a file,
a map of vars
(with string keys, as would come from System.get_env/0
),
and a keyword list of opts
.
Link to this section Types
An input source may be either a path to an env file or a map with string keys,
e.g. "envs/.env"
or %{"FOO" => "bar"}
. This allows users to specify a list
of env files interspersed with other values from other sources, e.g. System.get_env()
or values fetched from a secure parameter store.
Link to this section Callbacks
@callback parse(contents :: binary(), vars :: map(), opts :: keyword()) :: {:ok, map()} | {:error, any()}
A parser implementation should receive the contents
read from a file,
a map of vars
(with string keys, as would come from System.get_env/0
),
and a keyword list of opts
.
This callback is provided to help facilitate testing. See Dotenvy.Parser
for the default implementation.
Link to this section Functions
@spec env!(variable :: binary(), type :: Dotenvy.Transformer.conversion_type()) :: any() | no_return()
Reads the given env variable
and converts its value to the given type
.
This function attempts to read a value from a local data store of sourced values;
it will fall back to System.fetch_env/1
when no locally stored variable is available.
This function may raise an error because type conversion is delegated to
Dotenvy.Transformer.to!/2
-- see its documentation for a list of supported types.
examples
Examples
iex> env!("PORT", :integer)
5432
iex> env!("ENABLED", :boolean)
true
@spec env!( variable :: binary(), type :: Dotenvy.Transformer.conversion_type(), default :: any() ) :: any() | no_return()
Reads an env variable and converts its output or returns a default value.
Use
env!/2
when possibleUse of
env!/2
is recommended overenv!/3
because it creates a stronger contract with the environment: your app literally will not start when required env variables are missing.
If the given variable
is set, its value is converted to
the given type
. The provided default
value is only used when the
variable is not set; the default
value is returned as-is, without conversion.
This allows greater control of the output.
Conversion is delegated to Dotenvy.Transformer.to!/2
, which may raise an error.
See its documentation for a list of supported types.
This function attempts to read a value from a local data store of sourced values;
it will fall back to System.fetch_env/1
when no locally stored variable is available.
examples
Examples
iex> env!("PORT", :integer, 5432)
5433
iex> env!("NOT_SET", :boolean, %{not: "converted"})
%{not: "converted"}
iex> System.put_env("HOST", "")
iex> env!("HOST", :string!, "localhost")
** (RuntimeError) Error converting HOST to string!: non-empty value required
@spec source(inputs :: input_source() | [input_source()], opts :: keyword()) :: {:ok, %{optional(String.t()) => String.t()}} | {:error, any()}
Like its Bash namesake command, source/2
accumulates values from the given input(s).
The accumulated values are stored via a side effect function to make them available
to the env!/2
and env!/3
functions.
Think of source/2
as a merging operation which can accept maps (like Map.merge/2
)
or paths to env files.
Inputs are processed from left to right so that values can be overridden by each
subsequent input. As with Map.merge/2
, the right-most input takes precedence.
options
Options
:parser
module that implementsDotenvy.parse/3
callback. Default:Dotenvy.Parser
:require_files
specifies which of the givenfiles
(if any) must be present. Whentrue
, all the listed files must exist. Whenfalse
, none of the listed files must exist. When some of the files are required and some are optional, provide a list specifying which files are required. If a file listed here is not included in the function'sfiles
argument, it is ignored. Default:false
:side_effect
an arity 1 function called after the successful parsing inputs. The default is an internal function that stores the values inside a process dictionary so the values are available to theenv!/2
andenv!/3
functions. This option is overridable to facilitate testing. Changing it is not recommended.
examples
Examples
The simplest implementation is to parse a single file by including its path:
iex> Dotenvy.source(".env")
{:ok, %{
"TIMEOUT" => "5000",
"DATABASE_URL" => "postgres://postgres:postgres@localhost/myapp",
# ...etc...
}
}
More commonly, you will source multiple files (often based on the config_env()
)
and you will defer to pre-existing system variables. The most common pattern looks like this:
iex> Dotenvy.source([
"#{config_env()}.env",
"#{config_env()}.override.env",
System.get_env()
])
In the above example, the prod.env
, dev.env
, and test.env
files would be version-controlled,
but the *.override.env
variants would be ignored, giving developers the ability to override
values without needing to modify versioned files.
Give Precedence to System Envs!
Don't forget to include
System.get_env()
as the final input tosource/2
so that system environment variables take precedence over values sourced from.env
files.When you run a shell command like
❯ LOG_LEVEL=debug mix run
, your expectation is probably that theLOG_LEVEL
variable would be set todebug
, overriding whatever may have been defined in your sourced.env
files. Similarly, you may export env vars in your Bash profile. System env vars are not granted precedence automatically: you must explicitly includeSystem.get_env()
as the final input tosource/2
.
If your env files are making use of variable substitution based on system env vars,
e.g. ${PWD}
(see the Dotenv File Format), then you
would need to specify System.get_env()
as the first argument to source/2
.
For example, if your .env
references the system HOME
variable:
# .env
CACHE_DIR=${HOME}/cache
then your source/2
command would need to make the system env vars available
to the parser by including them as one of the inputs, e.g.
iex> Dotenvy.source([System.get_env(), ".env"])
Including the System.get_env()
before your files means that your files have final
say over the values, potentially overriding any pre-existing system env vars. In
some cases, you may wish to reference the system vars both before and after your own
.env files, e.g.
iex> Dotenvy.source([System.get_env(), ".env", System.get_env()])
or you may wish to cherry-pick which variables you need to make available for variable substitution:
iex> Dotenvy.source([
%{"HOME" => System.get_env("HOME")},
".env",
System.get_env()
])
This syntax favors explicitness so there is no confusion over what might have been "automagically" accumulated.
@spec source!(files :: binary() | [binary()], opts :: keyword()) :: %{optional(String.t()) => String.t()} | no_return()
As source/2
, but returns a map on success or raises on error.