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.
Series
- Part 1: Server Preparation
- Part 2: App Configuration & Deployment
- Part 3: Finishing Deployment
- Repo: https://gitlab.com/phxroad/phoenix-deploy-example
Create a Phoenix App
Let’s start by creating a Generic Phoenix Application:
$ mix phx.new deploy_example
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 deploy.do)
APP="deploy_example"
BUILD_HOST="<ip-address>"
BUILD_USER="deploy"
BUILD_AT="/home/deploy/deploy_example/builds"
PRODUCTION_HOSTS="<ip-address>"
PRODUCTION_USER="deploy"
DELIVER_TO="/home/deploy/deploy_example"
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}
else
{:ok, config}
end
end
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 =
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}
else
{:ok, config}
end
end
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
uDXmCEgDHDyxIOIa+T13/OoSUL7m/FLqJY4RUa5YsI8/wnJpecbTEendjVUGFVkp
Log back into the DO server as deploy:
$ ssh deploy.do
And add the variables to ~/.profile:
$ vim ~/.profile
export DATABASE_URL=ecto://<user>:<pass>@localhost:5432/myapp_prod
export APP_HOST=deploy-example.phxroad.com
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'
APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD phx.digest $SILENCE
"
}
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 start_erl.data file"
__sync_remote "
[ -f ~/.profile ] && source ~/.profile
set -e
if [ -f /home/deploy/deploy_example/deploy_example/var/start_erl.data ]; then
rm /home/deploy/deploy_example/deploy_example/var/start_erl.data
fi
"
}
There is a known issue where the wrong version of the application is started after deploying updates.
See the PR for more details:
https://github.com/bitwalker/distillery/pull/718
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
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
deploy_example_0.1.0.release.tar.gz
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
Success! The release has been deployed, but we also need to start the application:
$ mix edeliver start production
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:
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.