Deploy Elixir Phoenix Application

Phoenix created a config/prod.secret.exs file with your project. This is production connectivity information and thus ignored by git for security. We need to create this file on the target server. We're also going to store our applications in ~/apps/ format which is the equivalent of /home/deploy/apps/.

You can generate a new secret key by using mix phx.gen.secret.

use Mix.Config

config :new_application_name, NewApplicationNameWeb.Endpoint,
  secret_key_base: "notreal4eG460CU9/2x68+hGBjJ5DIJ8YSxNeb6/S/uY8bm0x5VlpN4VsYtSwR3sqmdXt"

config :new_application_name, NewApplicationName.Repo,
  username: "phx",
  password: "the_password_for_the_phx_database_user",
  database: "new_application_database_prod",
  pool_size: 15

Distillery & edeliver

Distillery compiles our Phoenix application into releases, and edeliver uses ssh and scp to build and deploy the releases to our production server. These are needed in our local project that we plan to deploy.

On the local machine in mix.exs:

{:edeliver, ">= 1.6.0"},
{:distillery, "~> 2.0", warn_missing: false},

Add :edeliver to extra_applications in the application block

def application do
   [
     mod: {NewApplicationName.Application, []},
     extra_applications: [:logger, :runtime_tools, :edeliver]
   ]
end
mix deps.get

Initialize Distillery

Distillery requires a build configuration file that is not generated by default.

mix distillery.init

This generates configuration files for Distillery in the rel directory.

On the local machine, create the .deliver directory with the config file inside:

#!/bin/bash

APP="new_application_name"

BUILD_HOST="new_application_name.slrs.io"
BUILD_USER="deploy"
BUILD_AT="/tmp/edeliver/$APP/builds"

START_DEPLOY=true
CLEAN_DEPLOY=true

# prevent re-installing node modules; this defaults to "."
GIT_CLEAN_PATHS="_build rel priv/static"

PRODUCTION_HOSTS="new_application_name.slrs.io"
PRODUCTION_USER="deploy"
DELIVER_TO="/home/deploy/apps"
AUTO_VERSION=revision

# For Phoenix projects, symlink prod.secret.exs to our tmp source
pre_erlang_get_and_update_deps() {
  local _prod_secret_path="/home/deploy/apps/$APP/secret/prod.secret.exs"
  if [ "$TARGET_MIX_ENV" = "prod" ]; then
    status "Linking '$_prod_secret_path'"
    __sync_remote "
      [ -f ~/.profile ] && source ~/.profile
      mkdir -p '$BUILD_AT'
      ln -sfn '$_prod_secret_path' '$BUILD_AT/config/prod.secret.exs'
    "
  fi
}

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

  status "Compiling assets"
    __sync_remote "
      [ -f ~/.profile ] && source ~/.profile
      set -e
      cd '$BUILD_AT'/assets
      node_modules/.bin/webpack --mode production --silent
    "

  status "Running phoenix.digest" # log output prepended with "----->"
  __sync_remote " # runs the commands on the build host
    [ -f ~/.profile ] && source ~/.profile # load profile (optional)
    set -e # fail if any command fails (recommended)
    cd '$BUILD_AT' # enter the build directory on the build host (required)
    # prepare something
    mkdir -p priv/static # required by the phoenix.digest task
    # run your custom task
    APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD phx.digest $SILENCE
    APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD phx.digest.clean $SILENCE
  "
}

Note on host name, we tend to deploy projects on a subdomain of our slrs.io domain, but this may differ depending on your particular project. Confirm the target url, configure it on CloudFlare ( https://www.cloudflare.com/ ), and put the correct url in this config file, or use the Server IP instead.

SystemD service

sudo touch /etc/systemd/system/my_app.service
[Unit]
Description=example_phoenix
After=network.target

[Service]
User=deploy
Group=deploy
Restart=on-failure

Type=forking
Environment=HOME=/home/deploy/example_phoenix
EnvironmentFile=/home/deploy/example_phoenix.env # For environment variables like REPLACE_OS_VARS=true
ExecStart= /home/deploy/example_phoenix/bin/example_phoenix start
ExecStop= /home/deploy/example_phoenix/bin/example_phoenix stop

[Install]
WantedBy=multi-user.target
sudo systemctl enable my_app.service

Config Prod

use Mix.Config

# For production, don't forget to configure the url host
# to something meaningful, Phoenix uses this information
# when generating URLs.
#
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
config :new_application_name, NewApplicationNameWeb.Endpoint,
  http: [port: {:system, "PORT"}],
  url: [host: "new_application_name.slrs.io", port: 80],
  cache_static_manifest: "priv/static/cache_manifest.json",
  server: true,
  code_reloader: false

# Do not print debug messages in production
config :logger, level: :info

config :phoenix, :serve_endpoints, true
import_config "prod.secret.exs"

Target server

export MIX_ENV=prod
export PORT=4000

Deployment

On your local machine, we need to build the production release.

mix edeliver build release production

This builds the release on the target server, and stores the archive in your local .edeliver/releases directory. If everything went well with the build, lets deploy:

mix edeliver deploy release to production

This uploads the release archive to the specified directory and extracts it, and starts the production server.

Next, lets migrate our database schema with the command:

mix edeliver migrate production
mix edeliver ping production # shows which nodes are up and running
mix edeliver version production # shows the release version running on the nodes
mix edeliver show migrations on production # shows pending database migrations
mix edeliver migrate production # run database migrations
mix edeliver restart production # or start or stop

Nginx

upstream phoenix {
    server 127.0.0.1:4000;
}

server {
  listen 80 default_server;
  listen [::]:80 default_server;
  server_name _;
  location / {
    allow all;

    # Proxy Headers
    proxy_pass http://phoenix;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Cluster-Client-Ip $remote_addr;

    # WebSockets
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";

  }
}