Skip to content
Dimitri Frederick
Go back

Deploying a Phoenix App with Ansible and Kamal

While building an app with the Elixir Phoenix framework i was in need of a deployment pipeline that just works. Here’s how I set one up using Ansible, Docker, and Kamal.

Steps

  1. Setting Up the VM with Ansible
  2. Configuring Docker on the VM
  3. Installing and Setting Up Kamal on the Host
  4. Configuring the Phoenix App
  5. Deploying with Kamal

Setting Up the VM with Ansible

First, we’ll need to provision and configure the server for deployment. These include things like updates, package installs, security, etc…

Ansible simplifies this process using scripts called playbooks. I’m using this playbook as a starting point: kamal-ansible-manager.

My VM is a fresh install of Ubuntu 24.04 LTS on my local network, but this approach works for cloud VMs as well.

Modify the hosts.ini file to reflect your server details and make any necessary configurations.

Ensure your remote server is set up for SSH, then run the playbook with Ansible:

ansible-playbook -i hosts.ini playbook.yml

Once your server is properly set up, you’re good to go to the next step.

Configuring Docker on the VM

Kamal uses Docker, and for security purposes, we want Docker to run as a non-root user. Here’s how you can achieve this on the VM:

sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
docker run hello-world

You may need to reboot your system afterward.

Installing and Setting Up Kamal on the Host

Back on the host machine, there are two ways to install Kamal: via Ruby or using Docker. If you have Docker installed, you can set up a simple alias script that runs Kamal:

alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock" -e SSH_AUTH_SOCK="/run/host-services/ssh-auth.sock" -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/basecamp/kamal:latest'

Verify the installation:

kamal version

Set up Kamal within your project:

kamal init

This creates a few files in your repository:

Environment variables can be set in the config file or read from .kamal/secrets like so:

KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
SECRET_KEY_BASE=$SECRET_KEY_BASE

The deploy.yml file is Kamal’s main configuration file and will need to be configured based on your app. Here’s a sample of mydeploy.yml configuration in which i am pulling in env variables from an .env.staging

Notable modifications include specifying the SSH user, adding arm64 as a build target for my M-series Mac, and setting the app port to 4000 (the default for Phoenix).

<% require "dotenv"; Dotenv.load(".env.staging") %>
---

service: myapp
image: myapp_staging
servers:
  web:
    hosts:
      - <%= ENV["MAIN_SERVER_IP"] %>

env:
  clear:
    PORT: 4000
    MIX_ENV: prod
    PHX_HOST: <%= ENV["PHX_HOST"] %>
  secret:
    - SECRET_KEY_BASE
    - DATABASE_URL

ssh:
  user: <%= ENV["SSH_USER"] %>

proxy:
  ssl: false
  app_port: 4000
  healthcheck:
    interval: 10
    path: /up

registry:
  username: registry-user-name
  password:
    - KAMAL_REGISTRY_PASSWORD

builder:
  arch:
    - arm64
    - amd64

Refer to the documentation for other configurations tailored to your use case, such as adding databases and utilizing accessory services.

Configuring the Phoenix App

When deploying, Kamal performs a readiness check by pinging a health check endpoint to ensure the application is running. By default, this checks the /up endpoint.

To set up this endpoint in Phoenix, create a new route at /up that returns a 200 OK response.

In router.ex:

get "/up", PageController, :health_check

In page_controller.ex:

def health_check(conn, _params) do
  send_resp(conn, 200, "OK")
end

Phoenix Release

Phoenix makes it easy to create a release. This packages your app into a self-contained directory with the Erlang VM, Elixir, all code, and dependencies, ready for deployment.

If you haven’t created one yet:

mix phx.gen.release --docker

Assuming you have a Dockerfile, ensure it exposes the application port to match the Kamal configuration. Add this line before the CMD:

EXPOSE 4000
CMD ["sh", "-c", "/app/bin/server"]

Deploying with Kamal

When ready to deploy, run:

kamal setup
  1. Connect to servers over SSH.
  2. Install Docker on any missing servers.
  3. Log into the registry locally and remotely.
  4. Build the Docker image.
  5. Push the image to the registry.
  6. Pull the image onto the servers.
  7. Ensure kamal-proxy is running.
  8. Start a new container.
  9. Route traffic to the new container once verified.
  10. Stop the old container.
  11. Clean up unused images and containers.

Your app should now be deployed and passing health checks!

To verify, SSH into the server and check running containers with docker ps


Share this post on: