If you're trying to run your shiny new Elixir app in a docker container, you'll find a few new problems over running Ruby. But you'll run into a few gotchas with environment variables.

The first problem

Elixir (being based on Erlang) is pretty cool, in that it gets compiled. The downside is that the environmental variables you use get hardcoded at compile time. That prevents you from using the same binary image (in a docker image) in your staging environment and production environment.

In our case, we're using Phoenix, so we've got this in our config/prod.exs (I moved this from the prod.secret.exs because with ENV it isn't secret!):

# config/prod.exs
config :FooApp, FooApp.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: System.get_env("DB_USER"),
  password: System.get_env("DB_PASS"),
  database: "foo_app",
  hostname: System.get_env("DB_HOST"),
  pool_size: 20

In your Dockerfile you'll want to release with exrm:

RUN MIX_ENV=prod mix do deps.get, compile, release

But once you do that, your config has the DB_USER, etc, that was available when your built your docker image! That's not good. You'll see this when you try to run your image:

$ docker build -t foo .
$ docker run -e "PORT=4000" \
> -e "DB_USER=foo" \
> -e "DB_PASS=secret" \
> -e "DB_HOST=my.mysql.server" \
> foo ./rel/foo/bin/foo foreground
Exec: /rel/foo/erts-8.0/bin/erlexec -noshell -noinput +Bd -boot /rel/foo/releases/0.0.1/foo -mode embedded -config /rel/foo/running-config/sys.config -boot_var ERTS_LIB_DIR /rel/foo/erts-8.0/../lib -env ERL_LIBS /rel/foo/lib -pa /rel/foo/lib/foo-0.0.1/consolidated -args_file /rel/foo/running-config/vm.args -- foreground
Root: /rel/foo
17:27:20.429 [info] Running Foo.Endpoint with Cowboy using http://localhost:4000
17:27:20.435 [error] Mariaex.Protocol (#PID<0.1049.0>) failed to connect: ** (Mariaex.Error) tcp connect: nxdomain
17:27:20.435 [error] Mariaex.Protocol (#PID<0.1048.0>) failed to connect: ** (Mariaex.Error) tcp connect: nxdomain
...

You'll notice I was able to specify the http port to use, that's a Phoenix specific configuration option, not available for other mix configs.

The first solution

Luckily, Exrm has a solution for this:

  1. Specify your environmental variables with "${ENV_VAR}"
  2. Run your release with RELX_REPLACE_OS_VARS=true

So we'll replace our Ecto config block with:

# config/prod.exs
config :FooApp, FooApp.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: "${DB_USER}",
  password: "${DB_PASS}",
  database: "foo_app",
  hostname: "${DB_HOST}",
  pool_size: 20

And run our release:

$ docker build -t foo .
$ docker run -e "PORT=4000" \
> -e "DB_USER=foo" \
> -e "DB_PASS=secret" \
> -e "DB_HOST=my.mysql.server" \
> -e "RELX_REPLACE_OS_VARS=true" \
> foo ./rel/foo/bin/foo foreground

And we get no errors!

The second problem

But now you want to create and migrate your database with mix do ecto.create --quiet, ecto.migrate.

$ docker run --link mysql \
> -e "PORT=4000" \
> -e "DB_HOST=mysql" \
> -e "DB_USER=foo" \
> -e "DB_PASS=secret" \
> -e "DB_HOST=mysql" \
> -e "MIX_ENV=prod" \
> foo mix do ecto.create --quiet, ecto.migrate
** (Mix) The database for Foo.Repo couldn't be created: ERROR 2005 (HY000): Unknown MySQL server host '${DB_HOST}' (110)

What's happening is that mix doesn't know to replace the "${ENV_VAR}" strings with our environmental variables. It isn't being run through the Exrm release (the mix tasks aren't even compiled into the release).

The second solution

This is easy to fix, we just add what we'd started with System.get_env:

# config/prod.exs
config :FooApp, FooApp.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: System.get_env("DB_USER") || "${DB_USER}",
  password: System.get_env("DB_PASS") || "${DB_PASS}",
  database: "foo_app",
  hostname: System.get_env("DB_HOST") || "${DB_HOST}",
  pool_size: 20

This gives us both options; either we can set them at "compile" time in the mix script, or don't specify them, so when the release runs it uses the run time environmental variables.

TLDR; Summary

With Elixir, Phoenix, Exrm, and Docker, you can build a release and run the same binary in staging and production. To specify different runtime environments and be able to run mix tasks like migrate, you need to combile two solutions.

Run your release with RELX_REPLACE_OS_VARS=true and define your config variables with System.get_env("ENV_VAR") || "${ENV_VAR}".

🎉