Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save brienw/85db445a0c3976d323b859b1cdccef9a to your computer and use it in GitHub Desktop.
Save brienw/85db445a0c3976d323b859b1cdccef9a to your computer and use it in GitHub Desktop.

Deploying Elixir and Phoenix applications using Docker and Exrm

Goal

By the end of this quick guide, you will know how to compile a Phoenix app release using Exrm and run it inside a Docker container. I've found only a couple of articles that discuss getting an Elixir app up and running inside a Docker container, and even those only touched on some parts of the process. The idea is that this guide will give you a full end-to-end example of how to get all the pieces and parts working together so that you are able to deploy your Phoenix application inside a Docker container.

Assumptions

  1. You already have a working Elixir environment with the Phoenix Framework installed
  2. You have at least basic working knowledge of Docker, and have installed the Docker tools onto your local environment

The journey at a glance

There are only a few things you have to do in order to get things all set up. At a high level, our task list looks like this:

  1. Configure your Phoenix app for Exrm
  2. Configure Relx, the Erlang release manager that underlies Exrm [OPTIONAL]
  3. Setup your Docker image

At this point you will be set up for compiling releases using Exrm. Building a Dockerized release is just a matter of compiling a new release, adjusting the Dockerfile to grab the new release, and then rebuilding the Docker container.

Configure your Phoenix app for Exrm

Before you get started on this part, create a new hello_phoenix app right now that you can use as your test dummy:

mix phoenix.new hello_phoenix

First, add the exrm dependency to your mix.exs file. At the time of this writing, the latest Exrm version is 1.0.3.

  defp deps do
    [{:phoenix, "~> 1.1.4"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_ecto, "~> 2.0"},
            ...
     {:exrm, "~> 1.0.3"}]
  end

Next make sure all of your production dependencies are listed in the :applications list inside application/0 of mix.exs. In the hello_phoenix application, this step should already be done for you. If you forget to do this in another project, Exrm may continually fail citing unmet dependencies.

NOTE: While you're poking around inside mix.exs, take note of the version number inside our project definition at the top of the file. This is the version number that Exrm will use when it packages up a release. When you want to bump the release version, this is where you make that change.

In production you want your production release to act as a server. Open up config/prod.exs and add the server: true option to the main config:

config :hello_phoenix, HelloPhoenix.Endpoint,
	http: [port: 8888], # put your desired port here
	url: [host: "example.com"],
	cache_static_manifest: "priv/static/manifest.json",
	server: true

Build your first release

You are now ready to go ahead and build your first release. The basic flow is four steps:

  1. Install dependencies
  2. Digest and compress assets
  3. Compile the Phoenix project
  4. Package release

Of course, you'll want to make sure you have all of our dependencies in order, so start off by making sure all of your required dependencies are installed.

$ mix do deps.get, compile

Next, compress assets for your production environment.

$ MIX_ENV=prod mix phoenix.digest

In older versions of Exrm, this next step was not required, but as of this writing, you will need to run it manually.

$ MIX_ENV=prod mix compile

And finally, the first big payoff; it's time to build the actual release with Exrm:

$ MIX_ENV=prod mix release

NOTE: if you run into errors while building a release, running with the --verbosity=verbose flag should give you more clues as to what is going on.

As the build output suggests, you can now try running your release as a sanity check that it all worked:

$ rel/hello_phoenix/bin/hello_phoenix console

With any luck, you should now be in an iex console, and if you visit http://localhost:8888, you should see your application. If your app doesn't appear to be responding to your browser request, make sure that you have set server: true in config/prod.exs

Going forward, whenever you want to build a new release, you'll need to run those same 4 commands. Here they are again all in one place:

mix do deps.get, compile
MIX_ENV=prod mix phoenix.digest
MIX_ENV=prod mix compile
MIX_ENV=prod mix release

To bundle ERTS or not to bundle ERTS, that is the question

If you are like me, you are doing all this fancy release building locally on your OSX development environment. This causes some extra issues, since the default behavior of relx (the tool that Exrm uses to actually package the release) is to bundle up the local Erlang Run-Time System (ERTS) binaries. Since our production environment is a Linux Docker image, our OSX binaries will not be compatible.

There are two solutions to this cross-platform issue, one is you have binaries for each environment stored locally, and then tell relx to include those in the release, the far simpler option is to just tell relx not to bundle up ERTS, and instead to just us whatever version is available on the host system. There are some caveats to this, but for most scenarios, this option should suffice. For those with sufficient time and interest, read this comment for slightly more details as to what this config change means.

To disable this ERTS bundling, create a new file rel/relx.config (you may need to create the rel directory first) and add the following:

{include_erts, false}.
{system_libs, false}.

NOTE: This file contains Erlang tuples, not Elixir tuples.

ANOTHER NOTE: Be warned that if you ever run a full implode to clean builds ( mix release.clean --implode as opposed to the usual mix release.clean) this file will definitely be blown away. Be sure to recreate it if you ever --implode your builds

After making this change, feel free to sanity check that things are still working as expected:

$ MIX_ENV=prod mix release
$ rel/hello_phoenix/bin/hello_phoenix console

Alright, you now have a packaged release, it's time to get this thing running inside a Docker container.

Configure your Dockerfile

The Dockerfile setup is just a matter of setting some environment variables about our release, creating a home where releases will live, unpacking the release, and then telling Docker how to start up the server.

Since you may already have your Docker image of choice, I will only be covering the additional configuration lines in addition to whatever already exists to set up your base image. If you don't have an image selected, I recommend using @trenpixster's elixir-dockerfile, which is the same one used for this guide. The key is to make sure that whatever base Docker image you create contains both Erlang and Elixir support.

Once you have your Dockerfile starting point, add the following commands at the end of the config. This tell Docker to pull in the application release, unpack it, and configure it. Note that you set a VERSION environment variable here. This should match the project's version setting inside mix.esx.

... 
ENV VERSION 0.0.1
ENV MIX_HOST 8888
EXPOSE $MIX_HOST

RUN mkdir /app
WORKDIR /app
COPY ./rel/hello_phoenix/releases/$VERSION/hello_phoenix.tar.gz /app/hello_phoenix.tar.gz
RUN tar -zxvf hello_phoenix.tar.gz


WORKDIR /app/releases/$VERSION
ENTRYPOINT ["./hello_phoenix.sh"]
CMD ["foreground"]
# Start up in 'foreground' mode by default so the container stays running

NOTE: See that version number in the path of the COPY command? If you change the project's version number in your mix.exs file, you'll need to make the same change here

Now that you have your Dockerfile configured, build your new/updated Docker image.

$ docker build -t hello_phoenix .

Now run that baby, and give it a name, 'hello_phoenix' to make our life a tad easier, and bind it to a local port so you can hit it with our browser after you start your Phoenix application up.

$ docker run -p 8888:8888 --name hello_phoenix -d hello_phoenix

With any luck, you should see Cowboy start listening on port 8888. The easiest way to actually view your application is to open up Docker's Kitematic app, select your newly lanched hello_app container, and you should see your Phoenix app inside the "Web Preview" sidebar. You now have your Phoenix application running inside a Docker container. For now I'll leave it as an excercise to the reader to continue on to deloying to a cloud service. Leave a comment below if you would like to see another guide on the cloud deploy process.

NOTE: to stop your running Docker container, you can either use the controls inside Kitematic, or from the command line run docker stop hello_phoenix and then clean up the container, with docker rm hello_phoenix

Putting it all together

That's it, you did it! Going forward, after you've made changes to your application, to build your new Dockerized release, you'll only need to repeat the last few steps you went over:

$ MIX_ENV=prod mix do phoenix.digest, compile, release
$ docker build -t hello_phoenix .

Comment below and share your thoughts on how you currently handle your own Phoenix deployments (whether using containers or not). This guide just touched the surface of a containerized build process, so I'd love to hear how you are integrating this or similar workflows into a continuous integration solution like Jenkins or Travis.

Brien Wankel is a Principal Engineer at the product development group Lab Zero. Do you need help with your Phoenix project, or are you thinking of migrating from Ruby to Elixir? If so, give us a shout; we'd love to see how we can help.

@note89
Copy link

note89 commented Jul 12, 2016

i will do the build in the dockerfile, that way i will get the same environment as on my server.
also i can build the image on docker hub. Any caveats this ?

@jeroenhouben
Copy link

I'm new to Docker and one blind spot I have is what it means to deploy a new release of your software. Does that build a new docker container (OS and all) and replace the current container? And if it does, doesn't this make for very slow deployments?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment