Kamal Cheat Sheet
Overview
Kamal (formerly MRSK) is a deployment tool from 37signals (the creators of Basecamp and HEY) that deploys Docker containers to bare metal servers or cloud VMs with zero downtime. It uses Traefik as a reverse proxy, SSH for server access, and Docker for containerization. Kamal provides a simple alternative to Kubernetes for teams that want container deployment without cluster overhead.
Kamal manages rolling deployments, health checks, asset bridging, and multi-server configurations. It supports deploying web servers, background workers, and cron jobs across multiple hosts with a single command, using an SSH-based approach that works with any Linux server.
Installation
# Install via RubyGems
gem install kamal
# Or via Docker (no Ruby needed)
alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "${SSH_AUTH_SOCK}:/ssh-agent" -v /var/run/docker.sock:/var/run/docker.sock -e "SSH_AUTH_SOCK=/ssh-agent" ghcr.io/basecamp/kamal:latest'
# Verify
kamal version
Core Commands
| Command | Description |
|---|---|
kamal init | Generate config files |
kamal setup | First-time server setup and deploy |
kamal deploy | Deploy latest version |
kamal redeploy | Deploy without setup |
kamal rollback <version> | Rollback to previous version |
kamal details | Show running containers |
kamal app logs | View application logs |
kamal app exec <cmd> | Execute command in container |
kamal traefik reboot | Restart Traefik proxy |
kamal env push | Push environment variables |
kamal lock release | Release deployment lock |
Configuration
config/deploy.yml
service: my-app
image: myregistry/my-app
servers:
web:
hosts:
- 192.168.1.10
- 192.168.1.11
labels:
traefik.http.routers.my-app.rule: Host(`myapp.com`)
traefik.http.routers.my-app.tls.certresolver: letsencrypt
worker:
hosts:
- 192.168.1.12
cmd: bin/jobs
registry:
server: ghcr.io
username: myuser
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
RAILS_ENV: production
DB_HOST: db.example.com
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
- REDIS_URL
traefik:
options:
publish:
- "443:443"
volume:
- "/letsencrypt:/letsencrypt"
args:
entryPoints.web.address: ":80"
entryPoints.websecure.address: ":443"
certificatesResolvers.letsencrypt.acme.email: admin@myapp.com
certificatesResolvers.letsencrypt.acme.storage: /letsencrypt/acme.json
certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint: web
healthcheck:
path: /up
port: 3000
interval: 10s
volumes:
- "/storage:/rails/storage"
asset_path: /rails/public/assets
.kamal/secrets
# .kamal/secrets (auto-loaded)
KAMAL_REGISTRY_PASSWORD=$(op read "op://Vault/Registry/password")
RAILS_MASTER_KEY=$(cat config/master.key)
DATABASE_URL=postgres://user:pass@db.example.com/myapp
REDIS_URL=redis://redis.example.com:6379
Accessories (Databases, Redis)
accessories:
db:
image: postgres:16
host: 192.168.1.20
port: 5432
env:
clear:
POSTGRES_DB: myapp_production
secret:
- POSTGRES_PASSWORD
volumes:
- /var/lib/postgresql/data:/var/lib/postgresql/data
options:
shm-size: 256m
redis:
image: redis:7-alpine
host: 192.168.1.20
port: 6379
volumes:
- /var/lib/redis:/data
cmd: redis-server --appendonly yes
Deployment Workflow
# First-time setup (installs Docker, Traefik, deploys app)
kamal setup
# Regular deployments
kamal deploy
# Deploy with skip of push (already pushed)
kamal deploy --skip-push
# Rollback to previous version
kamal rollback [version]
# Check what's running
kamal details
# View logs
kamal app logs
kamal app logs -f # Follow logs
kamal app logs --since 1h # Last hour
kamal app logs -r worker # Worker role logs
Environment Management
# Push secrets to servers
kamal env push
# Delete env on servers
kamal env delete
# Edit secrets
vim .kamal/secrets
kamal env push
kamal deploy
Advanced Usage
Multiple Destinations
# config/deploy.yml (base)
service: my-app
image: myregistry/my-app
# config/deploy.staging.yml
servers:
web:
hosts:
- staging.example.com
# config/deploy.production.yml
servers:
web:
hosts:
- prod1.example.com
- prod2.example.com
# Deploy to staging
kamal deploy -d staging
# Deploy to production
kamal deploy -d production
Builder Configuration
builder:
multiarch: false
local:
arch: amd64
cache:
type: registry
options: mode=max
args:
RUBY_VERSION: "3.3"
secrets:
- BUNDLE_ENTERPRISE_TOKEN
Hook Scripts
# .kamal/hooks/pre-deploy
#!/bin/bash
echo "Running pre-deploy checks..."
bundle exec rake test
if [ $? -ne 0 ]; then
echo "Tests failed! Aborting deploy."
exit 1
fi
# .kamal/hooks/post-deploy
#!/bin/bash
echo "Deploy complete! Notifying team..."
curl -X POST "$SLACK_WEBHOOK" -d '{"text":"Deployed my-app to production"}'
Running Commands
# Execute command in running container
kamal app exec 'bin/rails console'
kamal app exec 'bin/rails db:migrate'
# Execute on specific host
kamal app exec --hosts 192.168.1.10 'bin/rails runner "puts User.count"'
# Interactive shell
kamal app exec -i 'bash'
# Execute in a fresh container (not the running one)
kamal app exec --reuse 'bin/rails db:seed'
Proxy Configuration
proxy:
ssl: true
host: myapp.com
healthcheck:
interval: 3
path: /up
timeout: 3
buffering:
requests: true
responses: true
memory: 512
response_timeout: 30
forward_headers: true
Troubleshooting
| Issue | Solution |
|---|---|
| Deploy stuck/locked | Run kamal lock release |
| Container not starting | Check kamal app logs; verify healthcheck path |
| Registry auth failed | Verify KAMAL_REGISTRY_PASSWORD in secrets |
| SSH connection refused | Check SSH keys and server firewall |
| Traefik 502 errors | Check healthcheck; run kamal traefik reboot |
| Assets not serving | Verify asset_path config; check volume mounts |
# Check running state
kamal details
# View Traefik logs
kamal traefik logs
# Reboot Traefik
kamal traefik reboot
# Remove old containers
kamal prune all
# Force unlock
kamal lock release
# Debug SSH connection
kamal app exec -i 'whoami'