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.
- Ansible - Infrastructure as code software for configuring, and managing servers.
- Docker - Packages applications into containers that run consistently across different environments.
- Kamal - Tool that utilizes Docker to deploy and manage containerized apps on remote servers.
Steps
- Setting Up the VM with Ansible
- Configuring Docker on the VM
- Installing and Setting Up Kamal on the Host
- Configuring the Phoenix App
- 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:
- Create a Docker group.
- Add your user to the Docker group.
- Log in to the new Docker group.
- Test if Docker can be run without root by executing the Hello World container.
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:
config/deploy.yml(main configuration).kamal/secrets(for environment variables).kamal/hooks(Sample hooks)
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
- Connect to servers over SSH.
- Install Docker on any missing servers.
- Log into the registry locally and remotely.
- Build the Docker image.
- Push the image to the registry.
- Pull the image onto the servers.
- Ensure
kamal-proxyis running.- Start a new container.
- Route traffic to the new container once verified.
- Stop the old container.
- 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