The recently introduced Kamal tool is very powerful and easy to use, you can achieve full deployment automation when it is integrated into the CI/CD pipelines. I'm going to walk you through how to run it automatically on GitHub Actions with multiple destinations (eg. staging and production).
Pipeline workflows:
Event | Action |
---|---|
Push to main (PR merged) |
Desploy to Staging |
Publish release (Tag created) | Deploy to Production |
Requirements
-
Setup your app secrets in your repo for both environments (staging and production) in this tutorial they are prefixed by
STG_
andPROD_
. - Make sure you have created
SSH_PRIVATE_KEY
,KAMAL_REGISTRY_USERNAME
andKAMAL_REGISTRY_PASSWORD
secrets.
GitHub Action files
.github/
├─ workflows/
├─ job-deploy-kamal.yml
├─ workflow-deploy-production.yml
├─ workflow-deploy-staging.yml
-
job-deploy-kamal.yml
is the shared job where Kamal runs -
workflow-deploy-production.yml
acts as a trigger for production -
workflow-deploy-staging.yml
acts as a trigger for staging
job-deploy-kamal.yml
name: Job - Deploy to Kamal
on:
workflow_call:
inputs:
kamal-destination:
required: true
type: string
secrets:
KAMAL_REGISTRY_USERNAME:
required: true
KAMAL_REGISTRY_PASSWORD:
required: true
SSH_PRIVATE_KEY:
required: true
DATABASE_URL:
required: true
QUEUE_DATABASE_URL:
required: true
SECRET_KEY_BASE:
required: true
env:
DOCKER_BUILDKIT: 1
KAMAL_REGISTRY_USERNAME: ${{ secrets.KAMAL_REGISTRY_USERNAME }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
# App secrets
DATABASE_URL: ${{ secrets.DATABASE_URL }}
QUEUE_DATABASE_URL: ${{ secrets.QUEUE_DATABASE_URL }}
SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}
jobs:
deploy:
name: kamal deploy
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.KAMAL_REGISTRY_USERNAME }}
password: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- run: gem install kamal
- run: kamal lock release -d ${{ inputs.kamal-destination }}
- run: kamal deploy -d ${{ inputs.kamal-destination }}
workflow-deploy-staging.yml
name: Workflow - Deployment (staging)
on:
push:
branches:
- main
concurrency:
group: deployment-staging
cancel-in-progress: false
jobs:
deploy:
uses: ./.github/workflows/job-deploy-kamal.yml
with:
kamal-destination: staging
secrets:
KAMAL_REGISTRY_USERNAME: ${{ secrets.KAMAL_REGISTRY_USERNAME }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
SSH_PRIVATE_KEY: ${{ secrets.STG_SSH_PRIVATE_KEY }}
DATABASE_URL: ${{ secrets.STG_DATABASE_URL }}
QUEUE_DATABASE_URL: ${{ secrets.STG_QUEUE_DATABASE_URL }}
SECRET_KEY_BASE: ${{ secrets.STG_SECRET_KEY_BASE }}
workflow-deploy-production.yml
name: Workflow - Deployment (production)
on:
release:
types: [published]
concurrency:
group: deployment-production
cancel-in-progress: false
jobs:
deploy:
uses: ./.github/workflows/job-deploy-kamal.yml
with:
kamal-destination: production
secrets:
KAMAL_REGISTRY_USERNAME: ${{ secrets.KAMAL_REGISTRY_USERNAME }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
QUEUE_DATABASE_URL: ${{ secrets.PROD_QUEUE_DATABASE_URL }}
SECRET_KEY_BASE: ${{ secrets.PROD_SECRET_KEY_BASE }}
Kamal files
.kamal/
├─ secrets-common
config/
├─ deploy.production.yml
├─ deploy.staging.yml
├─ deploy.yml
secrets-common
KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
DATABASE_URL=$DATABASE_URL
QUEUE_DATABASE_URL=$QUEUE_DATABASE_URL
SECRET_KEY_BASE=$SECRET_KEY_BASE
deploy.yml
service: myapp
image: myorg/myapp
registry:
username:
- KAMAL_REGISTRY_USERNAME
password:
- KAMAL_REGISTRY_PASSWORD
proxy:
ssl: true
app_port: 3000
builder:
arch: amd64
volumes:
- "app_storage:/app/storage"
deploy.production.yml
servers:
web:
- 192.168.0.1
job:
hosts:
- 192.168.0.1
cmd: bin/jobs
proxy:
host: api.myapp.com
env:
clear:
RAILS_LOG_TO_STDOUT: 1
WEB_CONCURRENCY: 2
JOB_CONCURRENCY: 1
SOLID_QUEUE_IN_PUMA: true
secret:
- DATABASE_URL
- QUEUE_DATABASE_URL
- SECRET_KEY_BASE
deploy.staging.yml
servers:
web:
- 192.168.0.2
job:
hosts:
- 192.168.0.2
cmd: bin/jobs
proxy:
host: api-staging.myapp.com
env:
clear:
RAILS_LOG_TO_STDOUT: 1
WEB_CONCURRENCY: 0
JOB_CONCURRENCY: 1
SOLID_QUEUE_IN_PUMA: true
secret:
- DATABASE_URL
- QUEUE_DATABASE_URL
- SECRET_KEY_BASE
Top comments (0)