Deploy phoenix with kamal

Summary

Kamal deploys web apps anywhere from bare metal to cloud VMs using Docker with no downtime. We will deploying a phoenix app to digital ocean.

Prerequisite

You need to have elixir, ruby and docker installed:

  • I recommand using asdf to manage ruby / elixir
  • Install docker

You need to have ssh keys ready:

We will use the following services for hosting and managing the domain:

Setup asdf

Follow the installation guide

We need 3 plugins ruby erlang and elixir

asdf plugin-add ruby https://github.com/asdf-vm/asdf-ruby.git asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git

You will also need some extra packages for building erlang dependings of your OS check asdf-erlang

Create a ~/.tool-versions with the following content

ruby 3.2.1 elixir 1.14.3-otp-25 erlang 25.1.2

run asdf install after it finish you should have ruby, elixir and erlang installed

ruby -v ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-linux]
elixir -v Erlang/OTP 25 [erts-13.1.2] [source] [64-bit] [smp:24:24] [ds:24:24:10] [async-threads:1] [jit:ns] Elixir 1.14.3 (compiled with Erlang/OTP 25)

Docker

Follow the installation guide

Digital ocean

Create droplets

  • Choose region
  • Choose ubuntu 22.04 LTS
  • Basic
  • Regular 1 GB
  • SSH Key
  • Quantity 2

Create database

  • postgres 15
  • first plan

Create load balancer

  • Choose region
  • Connect droplets
  • edit health_check to /up

Creating the phoenix project

mix archive.install hex phx_new 1.7.7 mix phx.new blogex cd blogex

Creating a local dev database with docker compose

Create a file called docker-compose.dev.yml

version: '3.8' services: postgres: image: postgres:15.4 environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres ports: - '5432:5432' restart: 'no'

Run the compose container postgres

docker compose -f docker-compose.dev.yml up

Create the database

mix ecto.create

Generating the article crud

We will use phx.gen

mix phx.gen.live Blog Article articles title:text body:text

Add the created routes to lib/blogex_web/router.ex

... live "/articles", ArticleLive.Index, :index live "/articles/new", ArticleLive.Index, :new live "/articles/:id/edit", ArticleLive.Index, :edit live "/articles/:id", ArticleLive.Show, :show live "/articles/:id/show/edit", ArticleLive.Show, :edit ...

Run the migration

mix phx.migrate

Preparing blogex for deployment

Digital ocean and kamal expect a healthcheck route on /up that answer a 200 HTTP status.

Writing HealthCheckPlug

Create lib/blogex_web/health_check.ex

defmodule BlogexWeb.HealthCheckPlug do @moduledoc """ A Plug to return a health check on `/up` """ import Plug.Conn @behaviour Plug def init(opts), do: opts def call(%{path_info: ["up"]} = conn, _opts) do conn |> send_resp(200, "ok") |> halt() end def call(conn, _opts), do: conn end

We will add the plugin directly in the endpoint before the logs

Edit lib/blogex_web/endpoint.ex

... plug BlogexWeb.HealthCheckPlug ...

Testing /up

curl -v localhost:4000/up

Preparing the release

mix release.init

Edit rel/env.sh.eex

ip=$(hostname -i) export RELEASE_DISTRIBUTION=name export RELEASE_NODE=<%= @release.name %>@$ip

Preparing the image

Let's generate the base docker image with phx cmd

mix phx.gen.release --docker

We need to add curl to our image it's used by kamal to do healthcheck

On the Dockerfile add curl to the list of dependencies

RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales curl \ && apt-get clean && rm -f /var/lib/apt/lists/*_*

We also need to expose port 3000

EXPOSE 3000

Github token

Let's get a github token to create docker images

Go to https://github.com/settings/tokens/new:

  • Note: blogex
  • Select scopes write:packages
  • Generate token

Kamal part

Installation

Let's instal the kamal gem

gem install kamal kamal version

Kamal config

kamal init

It will create a /config/deploy.yml and an .env file

Edit the .env

We need to generate some secrets for our deployment

Generate SECRET_KEY_BASE

mix phx.gen.secret

Edit the .env file with our generated secrets

KAMAL_REGISTRY_PASSWORD=<github-token> SECRET_KEY_BASE=<secret-key-base> DATABASE_URL=[dogenerated] PHX_HOST=[yourdomain]

Edit config/runtime.exs

config :blogex, Blogex.Repo, ssl: true, ssl_opts: [ verify: :verify_none ], ...

Edit deploy.yml

Edit the config/deploy.yml

# Name of your application. Used to uniquely configure containers. service: blogex # Name of the container image. image: mrdotb/blogex # Deploy to these servers. servers: - [server-ip-1] - [server-ip-2] # Credentials for your image host. registry: server: ghcr.io # Specify the registry server, if you're not using Docker Hub # server: registry.digitalocean.com / ghcr.io / ... username: <github-username> # Always use an access token rather than real password when possible. password: - KAMAL_REGISTRY_PASSWORD # Inject ENV variables into containers (secrets come from .env). env: clear: PORT: 3000 secret: - DATABASE_URL - SECRET_KEY_BASE - PHX_HOST # Use a different ssh user than root # ssh: # user: app # Configure builder setup. builder: # If you’re developing on the same architecture as the one you’re deploying on # which is AMD64 multiarch: false # If you’re developing on ARM64 (like Apple Silicon) # local: # arch: arm64 # host: unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock # Remote builder can be used to speedup the process # remote: # arch: amd64 # host: ssh://root@192.168.0.1

Kamal deploy

kamal server bootstrap

It will install docker and both server.

make the project a git repo

git init
kamal deploy

It will:

  • build the blogex image locally
  • push the image to the registry
  • connect on the server through ssh
  • pull the image from the registry
  • create a container with

Cloudflare

Now we will serve the app on a domain using cloudflare

Pick the domain

In the SSL/TSL/Overview select the Flexible option.

In the DNS Records Create a new record, Type A, Name blogex.mrdotb.com, IPv4 [load-balancer-ip], Proxy status proxied

Checking logs and running migration

You will notice an error if you hit the yourdomain/articles that's because we need to run the migration

Let's check the logs

kamal app logs

Let's run the migration

kamal app exec -i --reuse '/app/bin/blogex remote' Blogex.Release.migrate

Edit & re-deploy

Let's edit the body input to be a textarea

kamal deploy

Rolling back

Listing containers

kamal app containers

Rolling back

kamal rollback [git hash]

Stay up to date

Sign up for the mailing list and get notified via email when new blog posts come out.