Application Deployment
The deployment of Elixir applications. You may have experience in deploying your Ruby or .NET applications, but when it comes to Elixir, do you have what it takes to deploy your app? Learn the essentials for getting your application from development to production.
Requirements
Deployment is great and all, but be sure you have these things before continuing:
- Application
- Build environment
- Hosting environment
Psst! Your hosting environment and your build environment can be one in the same if you're doing a test run.
Be sure that the architectures for both your build and hosting environments are the same, e.g. 64-bit Linux -> 64-bit Linux. Without doing this, you run the risk of your application not running. Using a virtual machine for your build environment that mirrors your hosting environment will be an easy way to ensure you don't have any such problems when deploying your application.
Goals
Our main goal for this guide is to generate a release, using Elixir Release Manager (exrm) and deploy it to our hosting environment. Once we have our application running, we will discuss steps needed to expose it to the world.
Overview
Let's separate our goals into a few tasks of which we can keep track:
- Add exrm as a dependency
- Generate our first release
- Deploy our release to our hosting environment
- Expose our application
The new dependency in town
To get started, we'll need to add exrm into our list of dependencies. With later versions of Elixir, we just need to add { :exrm, "~> 0.14.7" }
to have hex download everything correctly. Here's an example:
def deps do
[ { :plug, "~> 0.7.0" },
{ :cowboy, "~> 1.0.0" },
{ :exrm, "~> 0.14.7" } ]
end
With that taken care of, a simple mix do deps.get, deps.compile
will pull down exrm and its dependencies, along with the rest of your application's dependencies, and ensures that everything compiles so exrm's mix tasks are available as well. Speaking of...
$ mix help
mix # Run the default task (current: mix run)
...
mix release # Build a release for the current mix application.
mix release.clean # Clean up any release-related files.
mix release.plugins # View information about active release plugins
mix run # Run the given file or expression
mix test # Run a project's tests
iex -S mix # Start IEx and run the default task
Bam! Now we're cooking with fire!
Our first release
Setup
Update our mix.exs
file to have all dependencies listed under applications
in the application function:
def application do
[ mod: { MyApp, [] },
applications: [ :plug, :cowboy ] ]
end
Doing so helps us overcome one of exrm's common issues by helping exrm know of all our dependencies so that it can properly bundle them into our release. When not completed, our application will probably alert us of missing modules or of a failure to start a child application when we go to run our release.
Even if you list all of your dependencies, your application may still fail, typically because one of your dependencies does not properly list its own dependencies. A quick fix for this is to include the missing dependency in your list of applications. You should create an issue or a pull request to that project's repo to help the community, but it isn't necessary.
Add our application's router as a child to our application's supervisor:
def init([]) do
children = [
worker(MyApp.Router, [], function: :start)
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
supervise(children, strategy: :one_for_one)
end
Warning! The
phoenix.start
mix task will stop working once that worker is in your application's supervisor tree becauseMyApp.Router.start
would be started twice, resulting in a failure from the second operation. To easily bring up a development server, runiex -S mix
, which will give you a running application and aniex
console as well.
Generating the release
Running mix release
will kick off the build process for our release.
Note: In the following sections, you'll see our application's version (
0.0.1
) pop up in bunch of places. This value is pulled from the application'smix.exs
file, under the project's version.
$ mix release
==> Generating relx configuration...
==> Generating sys.config...
==> Generating boot script...
==> Performing protocol consolidation...
==> Conform: Loading schema...
==> Conform: No schema found, conform will not be packaged in this release!
==> Generating release...
===> Application metadata file exists but is malformed: /Users/shane/elixir/lib/mix/test/fixtures/deps_status/_build/dev/lib/invalidapp/ebin/invalidapp.app
===> Application metadata file exists but is malformed: /Users/shane/elixir/lib/mix/test/fixtures/deps_status/_build/dev/lib/invalidvsn/ebin/invalidvsn.app
===> Application metadata file exists but is malformed: /Users/shane/elixir/lib/mix/test/fixtures/deps_status/_build/dev/lib/ok/ebin/ok.app
==> Generating nodetool...
==> Packaging release...
==> The release for my_app-0.0.1 is ready!
After a bit, you will start to see exrm's output to the console. Exrm uses a set of default configuration options when building your application that will work for most applications (they say 99%). If you end up needing advanced configuration abilities, checkout exrm's configuration section for some awesome details about this process.
Once we see ==> The release for my_app-0.0.1 is ready!
pop up in our console, our release is complete! You may be asking yourself, “Where's the release? What's this rel
directory?” Let's take a look!
Contents of a release
$ ls -la rel/my_app
total 21488
drwxr-xr-x 7 shane staff 238 Aug 22 10:03 .
drwxr-xr-x 3 shane staff 102 Aug 22 10:03 ..
drwxr-xr-x 6 shane staff 204 Aug 22 10:03 bin
drwxr-xr-x 8 shane staff 272 Aug 22 10:03 erts-6.1
drwxr-xr-x 21 shane staff 714 Aug 22 10:03 lib
drwxr-xr-x 5 shane staff 170 Aug 22 10:03 releases
-rw-r--r-- 1 shane staff 10999433 Aug 22 10:03 my_app-0.0.1.tar.gz
bin
contains our generated executables for running our application. The bin/my_app
executable is what we will eventually use to issue commands to our application.
erts-6.1
contains all necessary files for the Erlang run-time system, pulled from our build environment.
lib
contains the compiled BEAM files for our applicaiton and all of our dependencies. This is where all of your hard work goes.
releases
is the home for our releases, being used to house any release-dependent configurations and scripts that exrm finds necessary for running our application.
The tarball is our release in archive form, ready to be shipped off to our hosting environment.
Testing our release
Before deploying our release, we should make sure that it runs on our build environment. To do that, we will issue the console
command to our executable, essentially running our application via iex
.
$ rel/my_app/bin/my_app console
Exec: /Users/shane/code/elixir/my_app/rel/my_app/erts-6.1/bin/erlexec -boot /Users/shane/code/elixir/my_app/rel/my_app/releases/0.0.1/my_app -env ERL_LIBS /Users/shane/code/elixir/my_app/rel/my_app/lib -config /Users/shane/code/elixir/my_app/rel/my_app/releases/0.0.1/sys.config -pa /Users/shane/code/elixir/my_app/rel/my_app/lib/consolidated -args_file /Users/shane/code/elixir/my_app/rel/my_app/releases/0.0.1/vm.args -user Elixir.IEx.CLI -extra --no-halt +iex -- console
Root: /Users/shane/code/elixir/my_app/rel/my_app
/Users/shane/code/elixir/my_app/rel/my_app
Erlang/OTP 17 [erts-6.1] [source-d2a4c20] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (0.15.2-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(my_app@127.0.0.1)1>
This is the point where your application will crash if it fails to start a child application. However, if all goes well, you should be dropped into an iex
prompt. Congratulations! We're ready to deploy our application!
Deploy! Deploy!
Now comes the easy part! There are many ways for us to get our tarballed release to our hosting environment, so you have a bit of free reign in this step.
In our example, we'll use SCP to upload to a remote server.
$ scp -i ~/.ssh/id_rsa.pub rel/my_app/my_app-0.0.1.tar.gz ubuntu@hostname.com:/home/ubuntu
my_app-0.0.1.tar.gz 100% 18MB 80.0KB/s 03:48
Hooray! Let's SSH into that environment to set our application up.
$ ssh -i ~/.ssh/id_rsa.pub ubuntu@hostname.com
$ sudo mkdir -p /app
$ sudo chown ubuntu:ubuntu /app
$ cd /app
$ tar xfz /home/ubuntu/my_app-0.0.1.tar.gz
See? I told you it would be easy.
Exposè
We're getting close. Can you feel it?
Set up our init system
First step in exposing our application to the world is ensuring that our application is running in case of a system restart, expected or unexpected. To do this, we will need to create an init script for our hosting environment's init system, be it systemd
, upstart
, or whatever.
In this case, we'll be using upstart
as our OS is Ubuntu, and upstart
has been bundled with Ubuntu since 6.10. Let's edit our init script with sudo vi /etc/init/my_app.conf
description "my_app"
## Uncomment the following two lines to run the
## application as www-data:www-data
#setuid www-data
#setgid www-data
start on startup
stop on shutdown
respawn
env MIX_ENV=prod
env PORT=8888
export MIX_ENV
export PORT
exec /bin/sh /app/bin/my_app start
Here, we've told upstart
a few basic things about how we want it to handle our application. If you need to know how to do somthing in particular, take a look at the upstart
cookbook for loads of information on it. We'll kick off the first start of our application with sudo start my_app
.
One key point to notice is that we're instructing upstart
to run our release's bin/my_app start
command, which boostraps our application and runs it as a daemon.
exrm commands
Along with the start
command, exrm bundles a few others with our application that are equally useful. Check out the exrm docs for details on what's possible.
ping
The ping
command is a great sanity check when you need to ensure your application is running:
$ bin/my_app ping
pong
Or to see if it isn't:
$ bin/my_app ping
Node 'my_app@127.0.0.1' not responding to pings.
remote_console
remote_console
will be your friend when debugging is in order. It allows you to attach an IEx console to your running application. When closing the console, your application continues to run.
$ bin/my_app remote_console
Erlang/OTP 17 [erts-6.1] [source-d2a4c20] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (0.15.2-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(my_app@127.0.0.1)1>
upgrade
Although at the time of writing (25 Aug 2014) there is an open issue with upgrading an app with exrm, the upgrade
command allows you to upgrade your application to a newer codebase without downtime. Awesome, right?
stop
You may run into situations where your application needs to stop. Look no further than the stop
command.
$ bin/my_app stop
ok
Set up our web server
In a lot of cases, you're going to have more than one application running in your hosting environment, all of which might need to be accessible on port 80. Since only one application can listen on a single port at a time, we need to use something to proxy our application. You will typically see Apache (with mod_proxy
enabled) or nginx used for this, and we'll be setting up nginx in this case.
Let's create our config file for our application. By default, everything in /etc/nginx/sites-enabled
is included into the main /etc/nginx/nginx.conf
file that is used to configure nginx's runtime environment. Standard practice is to create our file in /etc/nginx/sites-available
and make a symbolic link to it in /etc/nginx/sites-enabled
.
Note: These points hold true for Apache as well, but the steps to accomplish them are slightly different.
$ sudo touch /etc/nginx/sites-available/my_app
$ sudo ln -s /etc/nginx/sites-available /etc/nginx/sites-enabled
$ sudo vi /etc/nginx/sites-available/my_app
Contents of our /etc/nginx/sites-available/my_app
file:
upstream my_app {
server 127.0.0.1:8888;
}
server{
listen 80;
server_name .hostname.com;
location / {
try_files $uri @proxy;
}
location @proxy {
include proxy_params;
proxy_redirect off;
proxy_pass http://my_app;
}
}
Like our upstart
script, this nginx config is basic. Look to the nginx wiki for steps to configure any more involved features. Restart nginx with sudo service nginx restart
to load our new config.
At this point, we should be able to see our application if we visit http://hostname.com/
if everything has been successful up to this point.