Build and Deploy Elixir App with Releases and CircleCI

Build and Deploy Elixir App with Releases and CircleCI


Introduction

One of the common ways to deploy an elixir-phoenix application is through releases. A release is a self-contained directory with all of your application code, all dependencies, and the whole of the Erlang Virtual Machine runtime environment. Once you package your app this way, it can be deployed to the target server environment, as long as the target server runs on the same OS distribution and version as the build machine.

In this article, I'm going to take you through how to set up a build, test and deployment pipeline for your Elixir (specifically Phoenix app) app using CircleCI and Git.

What we intend to achieve by the end of this article:

  1. Set up the production environment

  2. Create a CI/CD pipeline

  3. Create a CircleCI account

  4. Connect your GitHub or Bitbucket repo to your CircleCI project

  5. Configure your CircleCI project

  6. Deploy your app from your local environment to GitHub

  7. CircleCI builds your release and deploys it to your target production server and starts the app.

So let's jump right into the actual task. We won't follow the steps one after another, but I will make sure you can track every step throughout the article.

Requirements:

You'll need to have a Phoenix application in your development environment or clone one here.

Setting up the production server

We need to create a non-sudo user for deployment. Please avoid the use of root or any user with sudo privileges.

For this step, you can access your server through ssh. I'll name the user circleci as we will use this user to perform our tasks with CircleCI.

Create a passwordless user and assign them a home directory using -m to create and -d to assign the home directory.

$ sudo useradd -m -d /home/circleci -s /bin/bash circleci

Do not add the user to sudo group as we will only use this user for ssh connection to the production server from CircleCI.

Head back to your local machine and create an ssh key pair which our new user will be using to connect to the server.

Make sure not to add a passphrase when prompted, or else CircleCI will not decrypt it.

$ cd
$ ssh-keygen -m PEM -t rsa -f .ssh/circleci

We cd to the home directory on the local machine. The -f will ensure that we create a key pair named circleci and circleci.pub

We are specifying the name circleci to avoid overwriting your other keys that might be existing.

Next, we copy the public key (circleci.pub).

$ cat ~/.ssh/circleci.pub

Select and copy the entire content, so we can register it on our production server.
So, ssh to your server again if you had closed the previous session and create a .ssh directory for the circleci user

$ sudo mkdir /home/circleci/.ssh

Then paste the public key into a file named authorized_key inside the .ssh directory

$ sudo nano /home/circleci/.ssh/authorized_keys

Change directory ownership and permissions for /home/circleci so that our user doesn't run into permission issues during the deployment

$ sudo chown -R circleci:circleci /home/circleci

also change the directory permission to allow all other users and groups read and execute access. This is important for when we are serving our static assets after deployment, but we will explain more about this later, for now just execute this command.

$ su circleci
$ chmod +R 755 /home/circleci

We can now test whether we can ssh to the server from our local machine. Remember our private key in our local machine? that's what we will use here.

ssh circleci@your_server_ip -i ~/.ssh/circleci

If you are able to log-in, it means that the ssh connection was successful and we can proceed.

Setting up the CircleCI account

Now you need to create a CircleCI here if you haven't already. You can sign up with your GitHub or Bitbucket account or your email.

If you sign up with GitHub or Bitbucket, all your account repos will be auto-imported. Otherwise, you'll have to do it manually (not a big deal).

Now head on to the CircleCI projects dashboard and click Setup Project button on the project you want to deploy with CircleCI.

Go to your local machine terminal again and copy the content of the circleci private key we generated earlier by running:

$ cat ~/.ssh/circleci

Back to the CircleCI project setup page, go to SSH KEY from the sidebar menu. Scroll down to the section Additional SSH keys and paste your key there. The hostname can be left blank. Remember to save it - this will allow CircleCI to ssh to the remote server.

Next, we set up some environmental variables required by CircelCI to perform its task. Some of the env variables are Host IP (host server IP), SSH User and App/release Name.

On the CircleCI project set-up page, locate Environmental Variables from the sidebar menu and add your .env variables there.

HOST_IP:********
APP_NAME: my_app
SSH_USER: circleci

Adding a CircleCI config.yml file

Let's write the test, build and deploy pipelines for our project. To do this, go to your Phoenix project on your local machine. Create .circleci folder in the root directory of your project. Then create a file named config.yml .

In our config.yml file, we start by telling CircleCI which images to use for our build machine.

# Elixir CircleCI 2.0 configuration file
# See: https://circleci.com/docs/language-elixir/
version: 2

# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/configuration-reference/#jobs
jobs:
  build:
    # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub.
    # See: https://circleci.com/docs/configuration-reference/#docker-machine-macos-windows-executor
    docker:
      # specify the version here 
      - image: cimg/elixir:1.14.4
      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # specify postgres image and postgres user for test purposes
      # documented at https://circleci.com/docs/circleci-images/ circleci/postgres:9.6
      - image: cimg/postgres:14.8
        environment:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: password

Here, I'm using CircleCI 2.0 configuration file. Next, I set up the jobs for our workflow.
For the build environment, we need to have Elixir and Postgres. CircleCI achieves this by using docker images. In my case, I'm using CircleCI convenience images (cimg). CircelCI will start a container with Elixir v1.14.4 and another one with Postgres v14.8.

You can use your preferred versions, but make sure to check whether those docker images exist.

I have also set up the default Postgres user and password, these will be used for the test pipeline. Make sure not to expose sensitive data in this file as it will be pushed to your git.

Next, we need to specify our app version and our working directory.

# set up version - optional but must match the app version configured nextpay-
environment: 
  - APP_VERSION: "0.1.0"

working_directory: ~/repo
    # Add steps to the job
    # See: https://circleci.com/docs/configuration-reference/#steps

Next, we add steps to our workflow. These steps will be executed in order one after another. In my case, I'll set up the build and deploy steps. You'll need to add more steps to perform your test. The test steps should run before the build to make sure that pushed code meet pass your test before deploying to the production server.

steps:
   - checkout

   # specify any bash command here prefixed with `run: `
   # Setup
   - run: mix local.hex --force
   - run: mix local.rebar

   # Testing Steps goes here

First, we check out our code and then we install hex locally and rebar.

- run:
   name: Install some necessary deps like rsync, nodejs, npm on the server for this case rsync will be installed
   command: sudo apt-get update && sudo apt-get install rsync

Here, I'm instructing CircleCI to install the necessary packages needed for our build and deploy task. These dependencies may include Nodejs, npm, and rsync - file copying tool. But I just want to install rsync as the rest is taken care of by the CircleCI convenience images.

Next, we fetch all dependencies, compile the app for production and deploy our static assets. This is done in the three steps below:

 # get dependencies and compile
 - run:
    name: Fetch mix dependencies
    command: MIX_ENV=prod mix deps.get --only prod
 - run: 
    name: Compile App
    command: MIX_ENV=prod mix compile
 # deploy static assets - css, js  
 - run:
    name: Deploy Static Files
    command: MIX_ENV=prod mix assets.deploy

Finally, we build our release and copy the release files to production server via ssh.
The variables used are the ones we did set up earlier under CircleCI environment variables.

# Build the Release
- run: 
    name: Build APP Release
    command: MIX_ENV=prod mix release

# Deploy to production server
- deploy: 
    name: Deploy to Production
    command: |
      ssh-keyscan -H $HOST_IP >> ~/.ssh/known_hosts;
      rsync -ravhz --stats --progress ./_build/prod/rel/$SSH_USER $SSH_USER@$HOST_IP:apps;
# post deploy step - stop and start the server
- deploy: 
    name: Post Deploy and App restart on production
    command: |
      chmod +x ./.circleci/post-deploy.sh
      ./.circleci/post-deploy.sh

You'll notice that the last step involves running a post-deploy.sh file. This file is the one that contains script to start and stop our app in the production environment. So create a file in your project under the .circleci folder we created earlier. Name it post-deploy.sh . The file looks like this:

#!/bin/bash

ssh $APP_USER@$APP_IP <<ENDSSH
    echo "switching directory to ./apps/$APP_NAME";
    cd ./apps/$APP_NAME;
    date >> deploy-log.txt;

    echo "Stopping application...";
    ./bin/$APP_NAME stop >> deploy-log.txt || true;
    echo "Exiting";
ENDSSH

ssh $APP_USER@$APP_IP <<ENDSSH
    echo "Changing directory into ./apps..";
    cd ./apps;

    echo "Getting environment variables ..";
    source .env;

    echo "$APP_NAME";
    date >> deploy-log.txt;

    echo "Starting application...";
    ./bin/$APP_NAME daemon >> deploy-log.txt

    echo "Finished";
ENDSSH

Here we are stopping the previously running application daemon and starting it again.

Our final CircleCI config.yml should look like this:

# Elixir CircleCI 2.0 configuration file
# See: https://circleci.com/docs/language-elixir/
version: 2

# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/configuration-reference/#jobs
jobs:
  build:
    # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub.
    # See: https://circleci.com/docs/configuration-reference/#docker-machine-macos-windows-executor
    docker:
      # specify the version here 
      - image: cimg/elixir:1.14.4
      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # specify postgres image and postgres user for test purposes
      # documented at https://circleci.com/docs/circleci-images/ circleci/postgres:9.6
      - image: cimg/postgres:14.8
        environment:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: password

    # set up version - optional but must match the app version configured nextpay-
    environment: 
        - APP_VERSION: "0.1.0"

    working_directory: ~/repo
    # Add steps to the job
    # See: https://circleci.com/docs/configuration-reference/#steps
    steps:
      - checkout

      # specify any bash command here prefixed with `run: `
      - run: MIX_ENV=prod mix local.hex --force
      - run: MIX_ENV=prod mix local.rebar

      # Testing Steps goes here

      # install necessary build dependencies
      - run:
          name: Install some necessary deps like rsync, nodejs, npm on the server for this case rsync will be installed
          command: sudo apt-get update && sudo apt-get install rsync

      # get mix dependencies and compile
      - run:
          name: Fetch mix dependencies
          command: MIX_ENV=prod mix deps.get --only prod
      - run: 
          name: Compile App
          command: MIX_ENV=prod mix compile
      # Deploy static files - css, js
      - run:
          name: Deploy Static Files
          command: MIX_ENV=prod mix assets.deploy

      # Build the Release
      - run: 
          name: Build APP Release
          command: MIX_ENV=prod mix release

      # Deploy to production server
      - deploy: 
          name: Deploy to Production
          command: |
            ssh-keyscan -H $APP_IP >> ~/.ssh/known_hosts;
            rsync -ravhz --stats --progress ./_build/prod/rel/$APP_NAME $APP_USER@$APP_IP:apps;
      # post deploy step - stop and start the server
      - deploy: 
          name: Post Deploy and App restart on production
          command: |
            chmod +x ./.circleci/post-deploy.sh
            ./.circleci/post-deploy.sh

It's now time for you to commit and push your code to your GitHub or Bitbucket repo.
But there's one more thing we need to do.

Our circleci user needs to authenticate with GitHub or Bitbucket to run tasks such as git pull.

We will create an ssh key pair - yes another ssh key pair - for our user to authenticate against Github. For Bitbucket users you don't need to do this.

So, ssh to your production server as circleci .

$ ssh circleci@your_server_ip -i ~/.ssh/circleci

Create the key pair with no passphrase

$ ssh-keygen -t rsa

Then output the public key and copy

$ cat ~/.ssh/id_rsa.pub

Copy the output and then head on to your CircleCI dashboard. Click the Project Settings button next to your repo and go to SSH Keys from the sidebar. Under Checkout SSH Keys click on Add Deploy Key to add the copied key. The key name should be your desired name, then paste the copied key under Key field then clicks Add Key to save. For Bitbucket users, this is done automatically thus no action is needed.

Now that your circleci user has access to your GitHub or Bitbucket, it's time to commit and push your code to your repo.

CircleCI will now pull your code to the build machine, build and deploy the release to your production server, and start the app on production.

Voila! Thank you for taking the time to read this kinda long post. Let me know if you have a question or need help in the comments.