Coming from a Rails background, deploying an Elixir app is significantly different compared to deploying a Rails app. The most important difference is that we don’t have to install Elixir on our VPS, thanks to the fact that Elixir app can be compiled into executable Erlang/OTP release packages, with Erlang already embedded. For the purpose of this guide, I’m assuming the VPS is Ubuntu 14.04.2 LTS.

Setting up PostgreSQL

If you’re using Ecto and PostgreSQL in your project (and you probably are), you should make sure the PostgreSQL version is at least 9.5 to avoid possible problems. While Ecto supports older versions of PostgreSQL, some of its features will fail on runtime - for example if you use Ecto’s type map, which is a jsonb type in PostgreSQL, which was added in 9.4.

So let’s connect to our server through ssh and install PostgreSQL 9.6:

$ sudo apt-get update
$ sudo apt-get install postgresql-9.6 postgresql-contrib libpq-dev

This will fail for some older versions of Ubuntu, like 14.04 LTS, but it’s still possible to install PostgreSQL 9.6 on it, just follow the instructions here.

If there’s already an older PostgreSQL version installed on your VPS, you can either install the new version to run alongside it (although on a different port), or transfer all the data from the old postgres database to the new one by dumping the data from the old one and reading it on the new one - follow the instructions here.

Once we finally have a proper PostgreSQL set up, let’s create the user and the database our Elixir app will be using. First, login to the PostgreSQL console

$ sudo -u postgres psql

Then create a database

postgres=# CREATE DATABASE elixir_app_production;

And a user with a password

postgres=# CREATE USER elixir_app_user WITH PASSWORD 'some_password';

And finally grant all privileges to the user for that database

postgres=# GRANT ALL PRIVILEGES ON DATABASE elixir_app_production to elixir_app_user;

Exit the console by entering

postgres=# \q

Now the database is all set up and ready for our application. Let’s go back to our local machine now and prepare the package.

Building a release package using Distillery

Once upon a time, the go-to tool for generating releases of Elixir projects was Exrm, it was even recommended in Phoenix’s release guides. Exrm is no longer being maintained, and instead the author urges us to use its replacement - Distillery, which we’re going to use here. Of course there are alternatives, like for example Relx.

Let’s begin by adding Distillery as a dependency in our mix.exs file:

{:distillery, "~> 1.4"}

Or whatever is the most recent version. Next, get the new dependencies:

$ mix deps.get

and create initial configuration files for our release:

$ mix release.init

Let’s take a look at the configuration file that was just added in rel/config.exs. Our production environment should be by default configured to include Erlang binaries, and not include our project source code:

environment :prod do
  set include_erts: true
  set include_src: false
  set cookie: :"A*CesyJa[$IYwrq*FZCno8Nnv,mqiyA$MhGH/:EK$)es//~*@EcUDVWCp}0607A;"
end

Now, we’ll probably need to change our release configuration at the very bottom of the file. A default one should work for basic, non-umbrella applications:

release :myapp do
  set version: current_version(:myapp)
  set applications: [
    :runtime_tools
  ]
end

Here we specify which applications are to be packed together with our project. In this example, Distillery only added runtime_tools by default, but there will probably be more needed.

I don’t know what applications your project will need here. Do you? Probably not. Let’s just ask Elixir by trying to build a release (in development environment for now):

$ mix release --env=dev

While the release was probably built successfully, a warning might have been shown, for example:

==> One or more direct or transitive dependencies are missing from
    :applications or :included_applications, they will not be included
    in the release:

    :ex_aws
    :uuid

    This can cause your application to fail at runtime. If you are sure
    that this is not an issue, you may ignore this warning.

These are the applications your project is missing now. Just add them to the list in rel/config.exs:

release :myapp do
  set version: current_version(:myapp)
  set applications: [
    :runtime_tools,
    :ex_aws,
    :uuid
  ]
end

And try to build the release again, which should be fine now:

$ mix release --env=dev

If you got the same warning again, but with different applications, add these to rel/config.exs too. Repeat until no more warnings are shown.

Now, as the success messages suggest, we can run our build to check if it really works. For example, try to open the interactive console, an equivalent of the local iex -S mix command:

$ _build/dev/rel/myapp/bin/myapp console

This will fail if you’re using Phoenix Code Reloader plug in your development environment. Simply temporarily change code_reloader: true to code_reloader: false in config/dev.exs and rebuild the release again to get rid of the error.

After confirming an application can be built in development environment, let’s configure our basic production environment in config/prod.exs.

First our application’s endpoint, which will listen for http requests:

config :myapp, Myapp.Endpoint,
  http: [port: 8888],
  url: [host: "127.0.0.1", port: 8888],
  cache_static_manifest: "priv/static/manifest.json",
  secret_key_base: "rkb5NLnoB1jXI5hDYnpG9Q",
  server: true,
  root: "."

And then the access to the database we configured earlier, using whatever username, password and database name was used:

config :myapp, Myapp.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "elixir_app_user",
  password: "some_password",
  database: "elixir_app_production",
  hostname: "localhost",
  pool_size: 10

You might also need to add a non-default port number if you have multiple instances of PostgreSQL running on your server:

port: 5434

Now we may finally build our production release:

$ MIX_ENV=prod mix release --env=prod

Let’s test it a bit more

For more extensive local testing we can temporarily copy our local database credentials from config/dev.exs to config/prod.exs, then rebuild the release again:

$ MIX_ENV=prod mix release --env=prod

Start the server using the executable we just built:

$ _build/prod/rel/myapp/bin/myapp start

Check if it responds:

$ _build/prod/rel/myapp/bin/myapp ping

And finally using a browser, visit the address configured in our config/prod.exs Endpoint, which for this guide is 127.0.0.1:8888.

But what about database migrations on our production server?

We can no longer run mix tasks, as mix is not included (and should not be included) in our release package. We’ll have to add a custom command for that, which we’ll execute in a similar way to commands we already used on our executable, like for example _build/prod/rel/myapp/bin/myapp console.

Let’s follow Distillery’s recommended method of adding an executable migration module to our project, and create a module that will run migrations for us anywhere in our project, for example in lib/myapp/release_tasks.ex:

defmodule MyApp.ReleaseTasks do

  @start_apps [
    :postgrex,
    :ecto
  ]

  @myapps [
    :myapp
  ]

  @repos [
    MyApp.Repo
  ]

  def seed do
    IO.puts "Loading myapp.."
    # Load the code for myapp, but don't start it
    :ok = Application.load(:myapp)

    IO.puts "Starting dependencies.."
    # Start apps necessary for executing migrations
    Enum.each(@start_apps, &Application.ensure_all_started/1)

    # Start the Repo(s) for myapp
    IO.puts "Starting repos.."
    Enum.each(@repos, &(&1.start_link(pool_size: 1)))

    # Run migrations
    Enum.each(@myapps, &run_migrations_for/1)

    # Run the seed script if it exists
    seed_script = Path.join([priv_dir(:myapp), "repo", "seeds.exs"])
    if File.exists?(seed_script) do
      IO.puts "Running seed script.."
      Code.eval_file(seed_script)
    end

    # Signal shutdown
    IO.puts "Success!"
    :init.stop()
  end

  def priv_dir(app), do: "#{:code.priv_dir(app)}"

  defp run_migrations_for(app) do
    IO.puts "Running migrations for #{app}"
    Ecto.Migrator.run(MyApp.Repo, migrations_path(app), :up, all: true)
  end

  defp migrations_path(app), do: Path.join([priv_dir(app), "repo", "migrations"])
  defp seed_path(app), do: Path.join([priv_dir(app), "repo", "seeds.exs"])

end

Notice that this module will also run the seed script every migration, which can be potentially destructive, or just needlessly add too many records to our database. If that is the case in your project, you should create separate functions for running migrations and executing the seed script in the above module, or simply delete the code that runs the seed script.

Now after we rebuild our release package, we can run migrations with the following command:

$ _build/prod/rel/myapp/bin/myapp command Elixir.MyApp.ReleaseTasks seed

We can shorten it howered by adding a custom command in rel/config.exs

release :myapp do
  set version: current_version(:myapp)
  set applications: [
    :runtime_tools,
    :ex_aws,
    :uuid
  ]
  set commands: [
    "migrate": "rel/commands/migrate.sh"
  ]
end

And creating a script file in rel/commands/migrate.sh:

#!/bin/sh

bin/myapp command Elixir.MyApp.ReleaseTasks seed

Similarly other, more complex commands can be added to our package for convenience. Let’s rebuild our package one more time, and we might be now ready for deployment:

$ MIX_ENV=prod mix release --env=prod

Cross-platform releases

Our package currently includes compiled Erlang binaries from our local system. This means our package won’t run on a different platform.

If you’re on the same distro as your target server, you’re already done, you can skip this chapter.

Otherwise there are a few ways to correct this. Here are just 3 common and simple ones among many more:

1) Install Erlang on your VPS, and don’t include Erlang binaries in your release package

On our example Ubuntu VPS it’s just a one liner:

$ sudo apt-get install erlang

Back on our local machine, edit rel/config.exs so include_erts is false

environment :prod do
  set include_erts: false
  set include_src: false
  set cookie: :"A*CesyJa[$IYwrq*FZCno8Nnv,mqiyA$MhGH/:EK$)es//~*@EcUDVWCp}0607A;"
end

And rebuild the release:

$ MIX_ENV=prod mix release --env=prod

2) Copy Erlang binaries from your VPS to your local machine, and link them when building a release.

Again, first install Erlang on your VPS:

$ sudo apt-get install erlang

Navigate to the directory that contains Erlang libraries, which should be /usr/lib/erlang, and copy everything back to your local machine, for example using scp:

$ scp -r root@some.address:/usr/lib/erlang path/to/compiled

Despite Distillery claiming it only needs erts-*/bin and erts-*/lib, copying erts-9.0 directory alone doesn’t work.

Now edit rel/config.exs to point to the directory we just copied:

environment :prod do
  set include_erts: "path/to/compiled/erlang"
  set include_src: false
  set cookie: :"A*CesyJa[$IYwrq*FZCno8Nnv,mqiyA$MhGH/:EK$)es//~*@EcUDVWCp}0607A;"
end

Rebuild the release, and it’s ready for deployment.

$ MIX_ENV=prod mix release --env=prod

And finally, you can remove Erlang from your VPS if you want to:

$ sudo apt-get purge erlang

3) Use Docker to build the package inside it.

While this is probably the most elegant, flexible and expandable solution, it requires some initial research to familiarize yourself with Docker and to set up a proper Docker container. While Docker is generally a commercial service, it also offers a free Community Edition, which is enough for our needs. The point here is to create a container, configured as closely to our VPS as possible, and to build our releases in it. Check out our screencast on Docker. More info on Docker and Docker with Elixir.

Deploying the package to a VPS

This is pretty straight forward. Besides a few one-time tasks, what we do in a normal deployment process is just copying a .tar.gz archive to our server, and then unpacking it there. Despite its simplicity, the process can be error-prone and it lacks scalability in case our project grows into multiple separate applications or environments. Let’s take a few minutes to set up an automatic deployment system by using edeliver.

As usual, add the dependency in our mix.exs file:

{:edeliver, "~> 1.4.3"}

Get the new dependency:

$ mix deps.get

And add :edeliver to our Distillery config file in rel/config.exs, to the end of the applications list:

release :myapp do
  set version: current_version(:myapp)
  set applications: [
    # ....
    :edeliver
  ]
end

Now let’s configure edelivery itself. While it offers many more features, we’ll just instruct it to build our application locally (it automatically detects Distillery being set up) and deploy it to production. Create a file .deliver/config:

APP="myapp"

BUILD_HOST="localhost"
BUILD_USER="$USER"
BUILD_AT="~/my-build-dir"

PRODUCTION_HOSTS="my.vps.address"
PRODUCTION_USER="user"
DELIVER_TO="/home/web"

PRODUCTION_USER and PRODUCTION_HOSTS will be the same as the user and host names in the ssh command you used to connect to your VPS before.

While it’s very redundant, edeliver will also use ssh to build the release on your own local machine, so you might need to install openssh-server to handle that:

sudo apt-get install openssh-server

To keep our repository clean, let’s add edelivery’s release directory to .gitignore:

echo ".deliver/releases/" >> .gitignore

Now all there’s left to do is build, deploy, extract and start the application. With edeliver it’s just one command:

mix edeliver update production

And then run migrations:

mix edeliver migrate production

If this wasn’t our first deployment, that would be all we would have to do thanks to Elixir’s hot-code updates (in some uncommon case that fails, you just stop/start the app again). We’re deploying for the first time though, so there are a few more things to check.

Connect to the VPS via ssh and navigate to the application’s directory. We have instructed edeliver to DELIVER_TO/home/web directory, so we’ll have to navigate to /home/web/myapp

Let’s check if the app responds:

$ bin/myapp ping

And you can also check if it really serves some data with a simple curl:

$ curl 127.0.0.1:8888

In case of any problems, you should check out logs in var/log/ directory (from your package’s root directory).

The last thing to do here is to make sure the application will be started again in case our VPS resets or temporarily shuts down. Since our example VPS is Ubuntu, we can just add an upstart init script:

$ sudo nano /etc/init/myapp.conf

description "myapp"

start on startup
stop on shutdown

respawn

env MIX_ENV=prod
env PORT=8888
export MIX_ENV
export PORT

exec /bin/sh /some/path/to/myapp/bin/myapp start

That’s it. The only thing left to do is to make our app accessible from the outside.

Setting up nginx

First let’s install the package:

$ sudo apt-get install nginx

Then edit the default site configuration file using any editor you like:

$ sudo nano /etc/nginx/sites-available/default

Now we’ll set an upstream to our running application, and a basic server block with your website’s DNS address that uses it:

upstream my_app {
  server localhost:8888;
}

server {
        listen 80;
        server_name your_website_address.com;

        location / {
          try_files $uri @proxy;
        }

        location @proxy {
          include proxy_params;
          proxy_redirect off;
          proxy_pass http://my_app;
        }
}

It’s a good practice to test the configuration before restarting nginx:

$ sudo nginx -t

If it’s all good, let’s restart it for the changes to take effect:

$ sudo service nginx restart

Congratulations, the app is now accessible from the Internet.

Post by Konrad Piekutowski

Konrad has been working with AmberBit since beginning of 2016, but his experience and expertise involves Ruby, Elixir and JavaScript.