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.