Create an Elixir umbrella project containing a phoenix app and build a release with Distillery

Bruce Pomeroy
7 min readSep 23, 2016

--

Related to: https://medium.com/@brucepomeroy/deploying-an-elixir-umbrella-project-using-distillery-and-edeliver-b0e8528569e3#.qwiwgdag7

Create our umbrella project and umbrella children

There’s a great explaination on Umbrella Apps here http://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-apps.html

Let’s create one on our local machine:

# Create the umbrella project:
mix new chat_umbrella --umbrella

This created an umbrella project, it’s a container for mix apps but doesn’t do much by itself. Let’s add a couple of apps. We add them in the apps/ directory. The first app uses the Phoenix web framework so we’ll create it using the phoenix.new generator.

cd chat_umbrella/apps
mix phoenix.new chat_web --no-ecto
...
Fetch and install dependencies? [Yn] y
...

I love Ecto but it’;s not important to this demo and omitting it means we don’t need to worry about database connections and can focus on the deployment story.

Still in the apps directory let’s create another Elixir app, it’s a sibling app of chat_web, this app doesn’t use Phoenix so we’ll use mix.new instead of phoenix.new. Make sure you’re in chat_umbrella/apps and run this.

mix new chat_backend --module ChatBackend --sup

We just created a second app under the umbrella project. We now have something like this:

Note that the umbrella project has a mix.exs file for configuration but it doesn’t have a lib folder, there’s nowhere to put application code, the umbrella is a container for apps, it’s not an app in itself. Also note that both our apps have test directories, we can run mix.test from the umbrella route and while the umbrella doesn’t have it’s own tests it will run the tests for all it’s child apps.

Let’s start the chat_web Phoenix app. We can do this from the umbrella root or from the chat_web root. I’m going to start it from the umbrella root.

mix phoenix.server

We can now visit http://localhost:4000 and see that the web app is running.

All the backend is going to do is provide a single hardcoded message for now. Let’s write a test for that in

# apps/chat_backend/test/chat_backend_test.exs
defmodule ChatBackendTest do
use ExUnit.Case
test "getting the message" do
assert ChatBackend.get_message() == "Hello, from ChatBackend"
end
end

Now in the umbrella root run:

mix.test
==> chat_backend
1) test getting the message (ChatBackendTest)
test/chat_backend_test.exs:4
** (UndefinedFunctionError) function ChatBackend.get_message/0 is undefined or private
Finished in 0.01 seconds
1 test, 1 failure
==> chat_web
....
Finished in 0.04 seconds
4 tests, 0 failures

Our test failed as expected but note that it also ran the tests that ship with phoenix. It ran the tests for both apps.

Now implement ChatBackend.get_message() to get the test to pass.

# apps/chat_backend/lib/chat_backend.ex
defmodule ChatBackend do
def get_message do
"Hello, from ChatBackend"
end
end

Run your tests again and we should be good. I’ll omit testing going forward, the point is that you can run your tests for one app from that app’s root, or you can run the tests for all projects by running your test command from the umbrella root.

We haven’t done any configuration of the umbrella and we haven’t told either of our apps about the other but already we can call code in one app from the other. In the chat_web app edit the pages controller which serves the default Phoenix welcome page.

# apps/chat_web/lib/web/controllers/page_controller.exs
# Or apps/chat_web/web/controllers/page_controller.exs, depending on your directory structure
defmodule ChatWeb.PageController do
use ChatWeb.Web, :controller
def index(conn, _params) do
message = ChatBackend.get_message()
render conn, "index.html", message: message
end
end

Above we call a function in the chat_backend app from the chat_web app. To see the message the we retrieved from chat_backend edit apps/chat_web/web/templates/page/index.html.eex and add this anywhere to print the message:

<%= @message %>

What if we need to do something more interesting in our chat backend, maybe we want to loop through a list of messages providing the next one in the list each time get_message is called. Let’s use a gen server.

Modify apps/chat_backend/mix.exs

def application do
[applications: [:logger]],
mod: {ChatBackend, []}]
end

Create a new file in our chat_backend. It’s a simple genserver that cycles through a list of messages returning the next message each time.

# apps/chat_backend/lib/chat_backend/message_provider.ex
defmodule ChatBackend.MessageProvider do
use GenServer
@messages ["Message 1", "Message 2", "Message 3"] def start_link(name) do
{:ok, pid} = GenServer.start_link(__MODULE__, :ok, [])
Process.register(pid, name)
{:ok, pid}
end
def init(:ok), do: {:ok, @messages}
def handle_call(:next_message, _from, [h | t]), do: {:reply, h, t ++ [h]}
# Public api:
def get_message(server), do: GenServer.call(server, :next_message)
end

Now start our MessageProvider:

defmodule ChatBackend do
alias ChatBackend.MessageProvider
def start(_type, _args) do
import Supervisor.Spec, warn: false

children = [
worker(MessageProvider, [:message_provider]),
]
opts = [strategy: :one_for_one, name: Test.Supervisor]
Supervisor.start_link(children, opts)
end
def get_message() do
MessageProvider.get_message(:message_provider)
end
end

Now repeatedly reload http://localhost:4000 and you should see a different message each time.

Note that we still haven’t referenced our chat_backend from the umbrella or the phoenix app. Also note that although we can still run mix phoenix.server from the app directory the app won’t run properly as this won’t start the chat_backend. And that running mix phoenix.server from the umbrella root starts the phoenix app but also starts chat_backend, even though chat_backend doesn’t use Phoenix.

Now let’s look at how we might deploy this. One way is with Elixir “Releases”. Let’s use Distillery to create a release. Edit mix.exs in the umbrella root.

defp deps do
[{:distillery, "~> 0.9"}]
end

Then run mix deps.get to bring in Distillery and then mix release.init to create a distillery config file. You can checkout the distillery config file at rel/config.exs. For now we’ll leave it at the defaults. Let’s create a production release by running the following mix command which is provided by Distillery.

mix release --env=prod
==> Assembling release..
==> Building release chat_umbrella:0.1.0 using environment prod
==> Including ERTS 8.0.2 from /usr/local/Cellar/erlang/19.0.2/lib/erlang/erts-8.0.2
==> Packaging release..
==> Release successfully built!
You can run it in one of the following ways:
Interactive: rel/chat_umbrella/bin/chat_umbrella console
Foreground: rel/chat_umbrella/bin/chat_umbrella foreground
Daemon: rel/chat_umbrella/bin/chat_umbrella start

Let’s try running it. Make sure you don’t have your app already running in dev by visiting http://localhost:4000 and ensuring you don’t see your app. Now let’s run the Interactive command that Distillery gave us.

rel/chat_umbrella/bin/chat_umbrella console
[info] Application chat_web exited: ChatWeb.start(:normal, []) returned an error: shutdown: failed to start child: ChatWeb.Endpoint
** (EXIT) shutdown: failed to start child: Phoenix.CodeReloader.Server
** (UndefinedFunctionError) function Mix.Project.config/0 is undefined (module Mix.Project is not available)

Doesn’t look good. We can see here the errors are related to Phoenix’s code-reloader. Phoenix.CodeReloader is for use in dev, it shouldn’t be running in a production release. It turns out the env=prod flag wasn’t enough we need to build the relase like this

MIX_ENV=prod mix release --env=prod

Now run rel/chat_umbrella/bin/chat_umbrella console again. This time we don’t get any errors but the server isn’t running.

To run the Phoenix server in production edit apps/chat_web/config/prod.exs

config :chat_web, ChatWeb.Endpoint,
http: [port: 8080],
url: [host: "localhost", port: 8080],
cache_static_manifest: "priv/static/manifest.json",
server: true

Now we should be good to go.

MIX_ENV=prod mix release --env=prod
...
rel/chat_umbrella/bin/chat_umbrella console
...
[info] Running ChatWeb.Endpoint with Cowboy using http://localhost:8080
...

Now visit the url in your browser and see your app, this time running in production mode.

There’s a problem though, using your web browser inspector’s network tab, take a look at the js and css files that the page loaded. Notice that they’re not minified. We didn’t actually even build the assets at all, I think the reason we are getting assets at all is the build probably picked up the dev assets.

Let’s build again but this time we’ll build the assets first.

# Run brunch to transpile and compress our assets
cd apps/chat_web
./node_modules/brunch/bin/brunch b -p
- info: compiled 6 files into 2 files, copied 3 in 1.6 sec

Take a look at these files:
apps/chat_web/priv/static/js/app.js
apps/chat_web/priv/static/css.app.css
Notice that they’re now minified. That’s good. The next step is to “digest” them, this gives them unique names that are a hash of their content, changing the name when the content changes mean we don’t have to worry about users getting out of date assets that were cached by our CDN or their browser.

MIX_ENV=prod mix phoenix.digest

This task looks for files in apps/chat_web/priv/static/ (the place where brunch just put our minified files) and generates versions of them with unique names and and gzipped versions. You can run brunch and phoenix digest repeatedly and the names of the files stay the same. Make a change to app.css thought then run the brunch and phoenix digest commands again, this time the content is different so the filename changes. Now go back to the umbrella root and run.

MIX_ENV=prod mix release --env=prod

Now browse to http://localhost:8080/ and check the assets the page is using, this time they have digested filenames and they’re minified. We’re not getting the gzipped version of the assets unfortunately, they are there though our webserver doesn’t know to serve them. We’ll tackle that later.

We’ve built a release now, we could possibly copy this to our webserver and run it. Odds are though our webserver is Linux and our dev machine is OS X, our release is built to run on OS X, if we want it to run on a Linux web server we need to build it on Linux. We’ll use Edeliver to help us build it for Linux but that will be in another article. I think this has gotten quite long enough!

Related to: https://medium.com/@brucepomeroy/deploying-an-elixir-umbrella-project-using-distillery-and-edeliver-b0e8528569e3#.qwiwgdag7

--

--

Bruce Pomeroy

Full-stack developer, specializing in Elixir and React