コンテンツにスキップ

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

CommandDescription
kamal initGenerate config files
kamal setupFirst-time server setup and deploy
kamal deployDeploy latest version
kamal redeployDeploy without setup
kamal rollback <version>Rollback to previous version
kamal detailsShow running containers
kamal app logsView application logs
kamal app exec <cmd>Execute command in container
kamal traefik rebootRestart Traefik proxy
kamal env pushPush environment variables
kamal lock releaseRelease 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

IssueSolution
Deploy stuck/lockedRun kamal lock release
Container not startingCheck kamal app logs; verify healthcheck path
Registry auth failedVerify KAMAL_REGISTRY_PASSWORD in secrets
SSH connection refusedCheck SSH keys and server firewall
Traefik 502 errorsCheck healthcheck; run kamal traefik reboot
Assets not servingVerify 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'