Deploy Phoenix to Ubuntu VPS - App Config and Deploy

In the first part of the series we prepared a build/deploy host using an Ubuntu flavored Digital Ocean droplet. In this section we're going to install some packages, plan our configuration and deploy our app for the first time.
Deploy Phoenix to Ubuntu VPS - App Config and Deploy

In the first part of the series we prepared a build/deploy host using an Ubuntu flavored Digital Ocean droplet.

In this section we’re going to install some packages, plan our configuration and deploy our app for the first time.


Create a Phoenix App

Let’s start by creating a Generic Phoenix Application:

$ mix deploy_example

alt text

Follow the instructions to make sure our app works locally:

$ cd deploy_example && mix ecto.create && mix phx.server

Install Distillery & Edeliver

Open mix.exs and add the dependencies:

{:edeliver, ">= 1.6.0"},
{:distillery, "~> 2.1"}

Install the deps:

$ mix deps.get

Initialize distillery:

$ mix distillery.init

Configure Edeliver

First let’s ignore the release store from being stored in the git repo.

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

Create a .deliver/config file with the following content:

(NOTE: replace <ip-address> with your DO address or use




Some info about these options:

  • APP - our application name
  • BUILD_HOST - target build host
  • BUILD_USER - user to login and build on server
  • BUILD_AT - server directory where builds will occur
  • PRODUCTION_HOSTS - our DO server, can have multiple production hosts
  • PRODUCTION_USER - server user that will run the app
  • DELIVER_TO - server directory where production app will run

There are more configuration options such as how to configure a staging server, but let’s stay focused on the simplest deployment possible.

Phoenix Application Config

Elixir application configuration is a complex subject and if you’d like to learn more about the nuances and some common patterns, we have an article that goes into more detail: Handling Environment Variables in Phoenix

Again, we’re going to focus on production configuration.

Open config/prod.exs and replace the contents with:

import Config

config :deploy_example, DeployExample.Repo,
  load_from_system_env: true,
  pool_size: 10

config :deploy_example, DeployExampleWeb.Endpoint,
  load_from_system_env: true,
  cache_static_manifest: "priv/static/cache_manifest.json",
  server: true,
  code_reloader: false

config :logger, level: :info

(If you’re using a different application name, remember to update the deploy_example and DeployExample portions)

The part of the configuration I’d like to focus on is:

load_from_system_env: true

We’re going to use this as a flag to instruct Repo and Endpoint to load environment variables at runtime.

If we reference environment variables here, they’ll be evaluated at compile-time which could lead to unexpected results in production.

Using init Callback in Repo

Open LIB_PATH/repo.ex and add the init callback:

def init(_type, config) do
  if config[:load_from_system_env] do
    db_url =
      System.get_env("DATABASE_URL") ||
      raise("expected the DATABASE_URL environment variable to be set")

    config = Keyword.put(config, :url, db_url)

    {:ok, config}
    {:ok, config}

This will attempt to load the environment variable at runtime, and will raise an error if it’s not found.

Using init Callback in Endpoint

We’re going to use the same pattern for configuring Endpoint.

Open WEB_PATH/endpoint.ex and add the init callback:

def init(_key, config) do
  if config[:load_from_system_env] do
    port = System.fetch_env!("PORT")
    secret_key_base = System.fetch_env!("SECRET_KEY_BASE")
    app_host = System.fetch_env!("APP_HOST")
    config =
      |> Keyword.put(:http, [:inet6, port: port])
      |> Keyword.put(:secret_key_base, secret_key_base)
      |> Keyword.put(:url, host: app_host, scheme: "https", port: 443)

    {:ok, config}
    {:ok, config}

This may look like there’s a lot going on but it’s just checking for three environment variables and loading them into the application configuration.

We’re setting the PORT the app will listen on, the SECRET_KEY_BASE and the APP_HOST.

NOTE: Because we’re using Nginx as a proxy, we’re setting the url scheme to https and port to 443 so the app generates the right URLs.

Set Environment Variables

Locally generate a new secret key base:

$ mix phx.gen.secret

Log back into the DO server as deploy:

$ ssh

And add the variables to ~/.profile:

$ vim ~/.profile
export DATABASE_URL=ecto://<user>:<pass>@localhost:5432/myapp_prod
export PORT=4005
export SECRET_KEY_BASE=<secret-key-base>

We’re using .profile as edeliver loads this file by convention with most commands.

Edeliver Build Tasks

As a final step before we actually build our app, we’ll need to add some build tasks to .edeliver/config to run some commands for static assets.

Open .deliver/config and add these hooks to the bottom:

pre_erlang_clean_compile() {
  status "Installing NPM dependencies"
  __sync_remote "
    [ -f ~/.profile ] && source ~/.profile
    set -e
    cd '$BUILD_AT'
    npm install --prefix ./assets $SILENCE

  status "Building static files"
  __sync_remote "
    [ -f ~/.profile ] && source ~/.profile
    set -e
    cd '$BUILD_AT'
    mkdir -p priv/static
    npm run deploy --prefix ./assets $SILENCE

  status "Running phx.digest.clean"
  __sync_remote "
    [ -f ~/.profile ] && source ~/.profile
    set -e
    cd '$BUILD_AT'
    APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD phx.digest.clean $SILENCE

  status "Running phx.digest"
  __sync_remote "
    [ -f ~/.profile ] && source ~/.profile
    set -e
    cd '$BUILD_AT'

This looks like a lot is happening but the steps are outlined by the status call for each step.

  • compile npm assets
  • build static files
  • phx.digest.clean
  • phx.digest

Add custom Task

There’s another custom hook we need to add to handle updates to the application:

(Make sure you update the path if yours is different)

post_extract_release_archive() {
  status "Removing release version file"
  __sync_remote "
    [ -f ~/.profile ] && source ~/.profile
    set -e
    if [ -f /home/deploy/deploy_example/deploy_example/var/ ]; then
      rm /home/deploy/deploy_example/deploy_example/var/

There is a known issue where the wrong version of the application is started after deploying updates.

See the PR for more details:

And you can read more about running additional build tasks in the Edeliver Docs .

Initial Application Build

Let’s build this puppy on the production server:

$ mix edeliver build release

alt text

Elixir uses semantic versioning and by default our app starts at v0.1.0.

It’s pretty clear by the output of the steps Edeliver has taken to build our application on the DO Server.

As a final step in the process, a tar build of our app is downloaded to our local store:

$ ls .deliver/releases

If you encounter any issues at this stage, you can run the command with --verbose or --debug flags to get a closer look at what’s happening.

Deploy the Release

Now that we have our release built and stored in the local release store, we can deploy the application with a simple command:

$ mix edeliver deploy release to production

alt text

Success! The release has been deployed, but we also need to start the application:

$ mix edeliver start production

alt text

We can verify that the application is running in production by logging into the DO server as deploy and running:

$ ps fax

And you should see your freshly deployed application running:

alt text

So far so good.

Viewing Application Logs

At this point, our application may have some issues actually serving requests. Let’s check the log output of the app on the DO server.

The application is located in the DELIVER_TO directory we configured, in our case:

$ ls ~/deploy_example/deploy_example/var/log
erlang.log.1  run_erl.log

deploy_example happens to be our deliver to location, and our application name.

Inside the var/log directory we have erlang.log.1

When I initially deployed the DB password set in .profile was wrong and by tailing this log:

$ tail -f ~/deploy_example/deploy_example/var/log/erlang.log.1

I was quickly able to fix the environment variables so that the app could connect to the DB.

Testing in the Browser

If you load your domain in the browser you’ll notice that it’s still just showing the Nginx default page.

This is expected because we haven’t configured Nginx to route to the application yet.

We’ll cover this and some other things in the next part of this Series.

Wrapping Up

We have successfully deployed our app! Here are some highlights of things we covered:

  • Installation and configuration for Distillery and Edeliver.
  • Configure runtime variables for Phoenix using Environment variables on the host.
  • A few basic commands for working with edeliver.
  • Check production logs for issues.

In the next part we’ll configure nginx, set up SSL with Let’s Encrypt and use Systemd to monitor our application as a service.


Troy Martin
Senior Engineer working with Elixir and JS.
Keep in the Loop

Subscribe for Updates