| name | kamal-coder |
| description | This skill guides deploying Rails applications with Kamal. Use when configuring deploy.yml, setting up accessories, managing secrets, or preparing servers for container deployment. |
| allowed-tools | Read, Write, Edit, Grep, Glob, Bash |
Kamal Coder
Overview
Kamal deploys containerized applications to bare metal or VMs using Docker. It handles zero-downtime deployments with Traefik as reverse proxy.
Server Requirements
Before Kamal can deploy, servers need:
| Requirement | Purpose |
|---|---|
| Docker | Container runtime |
| SSH access | Kamal connects via SSH |
| Ports 80, 443 open | HTTP/HTTPS traffic |
| Port 22 open | SSH for deployments |
Provision with: Ansible (infra/bin/provision --config) or cloud-init at boot time.
Configuration: config/deploy.yml
Minimal Setup
service: myapp
image: username/myapp
servers:
web:
hosts:
- 192.168.1.1
labels:
traefik.http.routers.myapp.rule: Host(`myapp.com`)
registry:
username: username
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: "true"
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
Multi-Role Setup
service: myapp
image: username/myapp
servers:
web:
hosts:
- 192.168.1.1
- 192.168.1.2
labels:
traefik.http.routers.myapp.rule: Host(`myapp.com`)
worker:
hosts:
- 192.168.1.3
cmd: bundle exec sidekiq
traefik: false # No HTTP traffic
registry:
username: username
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
RAILS_ENV: production
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
- REDIS_URL
With Accessories (Databases, Redis)
service: myapp
image: username/myapp
servers:
web:
hosts:
- 192.168.1.1
accessories:
db:
image: postgres:16
host: 192.168.1.1
port: 5432
env:
clear:
POSTGRES_DB: myapp_production
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
options:
shm-size: 256m
redis:
image: redis:7-alpine
host: 192.168.1.1
port: 6379
directories:
- data:/data
cmd: redis-server --appendonly yes
Secrets: .kamal/secrets
Kamal reads secrets from .kamal/secrets (git-ignored).
With 1Password CLI
# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$(op read "op://Infrastructure/DockerHub/password")
RAILS_MASTER_KEY=$(op read "op://MyApp/production/master_key")
DATABASE_URL=$(op read "op://MyApp/production/database_url")
POSTGRES_PASSWORD=$(op read "op://MyApp/production-db/password")
With Environment Variables
# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$DOCKERHUB_TOKEN
RAILS_MASTER_KEY=$RAILS_MASTER_KEY
DATABASE_URL=$DATABASE_URL
Multi-Environment
# config/deploy.yml
<% if ENV["KAMAL_DESTINATION"] == "staging" %>
service: myapp-staging
<% else %>
service: myapp
<% end %>
# .kamal/secrets.staging
RAILS_MASTER_KEY=$(op read "op://MyApp/staging/master_key")
Traefik Configuration
SSL with Let's Encrypt
traefik:
options:
publish:
- "443:443"
volume:
- /letsencrypt:/letsencrypt
args:
entryPoints.web.address: ":80"
entryPoints.websecure.address: ":443"
entryPoints.web.http.redirections.entryPoint.to: websecure
entryPoints.web.http.redirections.entryPoint.scheme: https
certificatesResolvers.letsencrypt.acme.email: admin@myapp.com
certificatesResolvers.letsencrypt.acme.storage: /letsencrypt/acme.json
certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web
servers:
web:
hosts:
- 192.168.1.1
labels:
traefik.http.routers.myapp.rule: Host(`myapp.com`)
traefik.http.routers.myapp.entrypoints: websecure
traefik.http.routers.myapp.tls.certresolver: letsencrypt
Health Checks
healthcheck:
path: /up
port: 3000
interval: 10s
max_attempts: 30
Common Commands
First Deployment
# Bootstrap server (installs Docker, creates directories)
kamal server bootstrap
# Full setup (push config, start traefik, deploy app)
kamal setup
Regular Deployment
# Deploy latest
kamal deploy
# Deploy specific version
kamal deploy --version=abc123
# Deploy to staging
kamal deploy -d staging
Rollback
# List available versions
kamal app containers
# Rollback to previous
kamal rollback
Debugging
# SSH into container
kamal app exec --interactive bash
# View logs
kamal app logs -f
# Rails console
kamal app exec --interactive "bin/rails console"
Accessories
# Start all accessories
kamal accessory boot all
# Restart specific accessory
kamal accessory reboot db
# Exec into accessory
kamal accessory exec db --interactive psql -U postgres
Provisioning Workflow
Terraform + Ansible + Kamal Pipeline
# infra/bin/provision
#!/usr/bin/env bash
set -euo pipefail
# 1. Terraform: Create infrastructure
cd infra && tofu apply
# 2. Ansible: Configure server
SERVER_IP=$(tofu output -raw server_ip)
cd ansible
echo "[web]\n$SERVER_IP ansible_user=root" > hosts.ini
ansible-playbook -i hosts.ini playbook.yml
# 3. Kamal: Bootstrap containers
cd ../..
bundle exec kamal server bootstrap
What Ansible Should Configure
Based on kamal-ansible-manager:
| Task | Purpose |
|---|---|
| Install Docker | Container runtime |
| Configure fail2ban | SSH intrusion prevention |
| Setup UFW | Firewall (22, 80, 443) |
| Enable NTP | Time synchronization |
| Create swap | Memory overflow protection |
| Harden SSH | Disable password auth, root login |
| Kernel tuning | swappiness, somaxconn |
Builder Configuration
Native ARM64 Builds (Hetzner CAX)
builder:
arch: arm64
# OR for multi-arch:
# multiarch: true
Remote Builder
builder:
remote:
arch: amd64
host: ssh://builder@build-server
Hooks
Pre-Deploy
# .kamal/hooks/pre-deploy
#!/bin/sh
echo "Running pre-deploy tasks..."
bundle exec rails assets:precompile
Post-Deploy
# .kamal/hooks/post-deploy
#!/bin/sh
echo "Running migrations..."
kamal app exec "bin/rails db:migrate"
Directory Structure
myapp/
├── config/
│ └── deploy.yml # Main Kamal config
├── .kamal/
│ ├── secrets # Secret values (git-ignored)
│ ├── secrets.staging # Staging secrets (git-ignored)
│ └── hooks/
│ ├── pre-deploy
│ └── post-deploy
└── Dockerfile # Application container
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Connection refused | Docker not running | kamal server bootstrap |
| Permission denied | SSH key not authorized | Check server's authorized_keys |
| Health check failing | App not starting | Check kamal app logs |
| Registry auth failed | Wrong credentials | Verify .kamal/secrets |
| Traefik 502 | Container not healthy | Increase max_attempts |