A short summary how to deploy a Phoenix-Project to an AWS-Instance

Sure, there are deploy-utils out there which can automate deploying taking every single detail into consideration. Though, for a simple project, simple shell scripts can do the job as well.

Ingredients

  • Elixir / Phoenix to create your project
  • Docker and Docker-Compose to create your docker-images
  • An account at docker-hub and Amazon-AWS
  • Some simple shell-scripts

The Goal

I want to be able to compile and deploy a Phoenix-project with a single command from my workstation.

How to

  1. Create your phoenix project. See: Phoenix
  2. Write a Dockerfile. See below.
  3. Write a docker-compose.yml file. See below.
  4. A simple script is needed to set the proper permissions for
  5. postgres, thus the application container has access to the postgres-server in the postgres. See initialize_database.sh below.
  6. Create an EC2-instance using AWS-Linux on Amazon’s platform.
  7. Install Docker and Docker-compose
  8. Run the script build-and-deploy as shown below for the first time.
  9. Login to EC2 and run the following commands once:

    docker-compose down docker-compose up init # Will initialize the database docker-compose up -d web # Will start the app

From now on you can use the script build-and-deploy-script.

This blog post doesn’t cover how to create your EC2-instance and how to install “Docker” and “Docker-compose” but it shouldn’t be a big deal.

  • Create an instance using “AWS-Linux”
  • install docker and docker-compose
  • Log in to your docker-hub account

Just a hint. Google for ‘install docker-compose on aws-linux’ if it doesn’t work as simple as

sudo bash
yum install -y docker
sudo yum install -y docker-

Files

The Dockerfile

We’re using the latest postgres-image from docker-hub and build our application image from this Dockerfile. Notice the last line of the file is needed only if you use bcrypt within your project.

FROM elixir:1.5.1
ENV DEBIAN_FRONTEND=noninteractive

MAINTAINER Your Name

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get install -y -q nodejs gcc

# Suggested https://hexdocs.pm/phoenix/installation.html
RUN apt-get update && apt-get install -y \
      inotify-tools \
      && rm -rf /var/lib/apt/lists/*

# Create and set home directory
ENV HOME /opt/app
WORKDIR $HOME

# done by docker-compose
# EXPOSE 4000

# Configure required environment
ENV MIX_ENV prod

# Install hex (Elixir package manager)
RUN mix local.hex --force

# Install rebar (Erlang build tool)
RUN mix local.rebar --force

# Copy all dependencies files
COPY mix.* ./

# Install all production dependencies
RUN mix deps.get --only prod

# Compile all dependencies
RUN mix deps.compile

# Copy all application files
COPY . .

# Compile the entire project and build the digest for production
RUN mix do compile, phx.digest
RUN (cd deps/bcrypt_elixir && make clean && make)

docker-compose.yml

Replace your-docker-hub-accout/your-docker-image-name:latest with the names for your account and project. For exapmple nickname/project-one:latest.

init

This service will be executed one to create the database with docker-compose up init.

web

Starts the database and elixir app. docker-compose up -d web will do the job and -d will keep it running in background.

postgres

Uses the official postgres docker image.

version: '3'

services:

      init:
        image: "your-docker-hub-accout/your-docker-image-name:latest"
        environment:
          - MIX_ENV=prod
          - PORT=4000
          - POSTGRES_USER=postgres
          - POSTGRES_PASSWORD=postgres
          - POSTGRES_HOST=postgres
        volumes:
          - "./postgresql:/opt/app/postgresql"
        command: ./initialize_database.sh
        networks:
          - backend
        depends_on:
          - postgres

      web:
        image: "your-docker-hub-account/your-docker-image-name:latest"
        ports:
          - "80:4000"
        command: mix do ecto.migrate, phx.digest, phx.server
        environment:
          - MIX_ENV=prod
          - MIX_HOST=localhost
          - LMS_HOST=localhost
          - PORT=4000
          - POSTGRES_USER=postgres
          - POSTGRES_PASSWORD=postgres
          - POSTGRES_HOST=postgres
        networks:
          - backend
          - frontend
        depends_on:
          - postgres


      postgres:
        image: postgres:latest
        volumes:
          - "./postgresql/data:/var/lib/postgresql/data"
        ports:
          - "5432:5432"
        environment:
          - POSTGRES_USER=postgres
          - POSTGRES_PASSWORD=postgres
          - POSTGRES_HOST=postgres
        networks:
          - backend

    networks:
      frontend:
      backend:

initialize_database.sh

#!/bin/bash
echo "TRUST ALL LOCAL CONNECTIONS IN pg_hba.conf"
if test -d ./postgresql
then
  echo "host all all all trust" >> ./postgresql/data/pg_hba.conf
  echo "WAIT FOR POSTGRES RECOGNIZING NEW PERMISSIONS"
  sleep 10
  mix do ecto.create, ecto.migrate
  echo ""
  echo ""
  echo "IF ANY ERROR OCCURED RUN 'docker-deploy up init' AGAIN"
  echo "OTHERWISE YOU CAN START THE CONTAINER WITH:"
  echo ""
  echo "   docker-deploy up -d web      "
else
  echo "DATABASE NOT CREATED YET. PLEASE TRY AGAIN"
fi

Shell scripts to build and deploy

build-and-deploy is the one and only script you have to call and should be located in the root-path of your project. For all other scripts create a subfolder deploy.

build-and-deploy.sh

#!/bin/bash

docker build -t your-docker-hub-account/your-docker-image-name:latest .
docker push your-docker-hub-account/your-docker-image-name:latest

deploy/deploy-aws.sh
In subfolder deploy

deploy-aws.sh will run localy

You will have to replace XX-XX-XXX-XXX.eu-central-1 with the address of your EC2-server. and path/to/your/private-aws.pem with the path to your local private key (you will get when creating your EC2 instance)

#!/bin/bash
echo "********************************************************"
echo "COPY UPDATE SCRIPTS TO AWS-INSTANCE"
echo "********************************************************"
scp -i ~/path/to/your/private-aws.pem  deploy/update-and-restart.sh \
deploy/restart-docker.sh \
ec2-user@ec2-XX-XX-XXX-XXX.eu-central-1.compute.amazonaws.com:./

echo "********************************************************"
echo "UPDATE AWS-INSTANCE"
echo "********************************************************"
ssh -i ~/path/to/your/private-aws.pem \
 ec2-user@ec2-XX-XX-XXX-XXX.eu-central-1.compute.amazonaws.com \
 sudo ./update-and-restart.sh

echo "********************************************************"
echo "DEPLOYED `date`"
echo "********************************************************"

restart-docker.sh will run on EC2-instance

#!/bin/bash

echo "********************************************************"
echo "DOWNLOAD CURRENT IMAGE "
echo "********************************************************"

sudo docker pull your-docker-hub-account/your-docker-image-name:latest > /dev/null

echo "********************************************************"
echo "RESTART DB AND SERVICE WEB "
echo "********************************************************"

cd /home/ec2-user/your-app-dir && \
  /usr/local/bin/docker-compose down

cd /home/ec2-user/your-app-dir && \
  /usr/local/bin/docker-compose up -d web

sleep 5
echo "********************************************************"
echo "DOCKER IMAGE UPDATED AND SERVICE RESTARTED "
echo "********************************************************"

update-and-restart.sh will run on EC2-instance

#!/bin/bash
echo "********************************************************"
echo "UPDATING AND RESTARTING "
echo "********************************************************"
hostname
pwd
echo "--------------------------------------------------------"

sudo docker pull your-docker-hub-account/your-docker-image-name:latest
sudo /home/ec2-user/restart-docker.sh
sudo docker system prune -f

echo "---------- UPDATE DONE --------------------------------"