This skill guides deploying Rails applications with Kamal. Use when configuring deploy.yml, setting up accessories, managing secrets, or preparing servers for container deployment.
This skill is limited to using the following tools:
Kamal deploys containerized applications to bare metal or VMs using Docker. It handles zero-downtime deployments with Traefik as reverse proxy.
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.
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
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
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
Kamal reads secrets from .kamal/secrets (git-ignored).
# .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")
# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$DOCKERHUB_TOKEN
RAILS_MASTER_KEY=$RAILS_MASTER_KEY
DATABASE_URL=$DATABASE_URL
# 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:
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
healthcheck:
path: /up
port: 3000
interval: 10s
max_attempts: 30
# Bootstrap server (installs Docker, creates directories)
kamal server bootstrap
# Full setup (push config, start traefik, deploy app)
kamal setup
# Deploy latest
kamal deploy
# Deploy specific version
kamal deploy --version=abc123
# Deploy to staging
kamal deploy -d staging
# List available versions
kamal app containers
# Rollback to previous
kamal rollback
# SSH into container
kamal app exec --interactive bash
# View logs
kamal app logs -f
# Rails console
kamal app exec --interactive "bin/rails console"
# 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
# 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
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:
arch: arm64
# OR for multi-arch:
# multiarch: true
builder:
remote:
arch: amd64
host: ssh://builder@build-server
# .kamal/hooks/pre-deploy
#!/bin/sh
echo "Running pre-deploy tasks..."
bundle exec rails assets:precompile
# .kamal/hooks/post-deploy
#!/bin/sh
echo "Running migrations..."
kamal app exec "bin/rails db:migrate"
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
| 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 |