Kamal
Deploy web apps anywhere with zero-downtime deployments using Docker.
Source: Kamal Official Documentation | GitHub
Overview
Kamal = "Capistrano for Containers" by 37signals. Default deployment tool for Rails 8.
Key Features:
- •Zero-downtime deployments via Traefik proxy
- •Deploy anywhere (VPS, bare metal, cloud)
- •Remote builds, asset bridging
- •Accessory management (databases, Redis)
- •Automatic provisioning
- •Docker layer caching
Source: Kamal Documentation
Installation
Rails 8+ (Pre-installed)
bash
rails new myapp cd myapp # config/deploy.yml already exists
Manual Installation
bash
gem install kamal kamal init # Creates config/deploy.yml and .kamal/secrets
Configuration
Minimal deploy.yml
yaml
# config/deploy.yml
service: myapp
image: username/myapp
servers:
web:
hosts:
- 192.168.0.1
registry:
username: your-username
password:
- KAMAL_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
Production Configuration
yaml
service: myapp
image: username/myapp
servers:
web:
hosts:
- 192.168.0.1
- 192.168.0.2
labels:
traefik.http.routers.myapp.rule: Host(`myapp.com`)
workers:
hosts:
- 192.168.0.3
cmd: bundle exec sidekiq
registry:
server: ghcr.io
username: github-username
password:
- KAMAL_REGISTRY_PASSWORD
proxy:
ssl: true
host: myapp.com
env:
clear:
RAILS_ENV: production
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
asset_path: /app/public/assets
volumes:
- "storage:/app/storage"
retain_containers: 5
retain_images: 5
healthcheck:
path: /up
port: 3000
interval: 10s
ssh:
user: deploy
Source: Kamal Configuration
Accessories (Databases, Redis)
Important: Accessories do not have zero-downtime deployments. Managed separately from main app.
PostgreSQL
yaml
accessories:
db:
image: postgres:16
host: 192.168.0.10
port: "5432:5432"
env:
clear:
POSTGRES_USER: myapp
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
Redis
yaml
accessories:
redis:
image: redis:7-alpine
host: 192.168.0.11
port: "6379:6379"
cmd: redis-server --appendonly yes
directories:
- data:/data
MySQL
yaml
accessories:
mysql:
image: mysql:8.0
host: 192.168.0.12
port: "3306:3306"
env:
clear:
MYSQL_DATABASE: myapp_production
secret:
- MYSQL_ROOT_PASSWORD
directories:
- data:/var/lib/mysql
Source: Kamal Accessories
Commands
Deployment
bash
# Initial setup kamal setup # Deploy kamal deploy kamal deploy -d staging kamal deploy -d production # App management kamal app restart kamal app logs -f kamal app exec 'bin/rails console' kamal app exec 'bin/rails db:migrate' # Accessories kamal accessory boot all kamal accessory boot db kamal accessory reboot redis kamal accessory logs db # Cleanup kamal prune -y
Destinations (Staging/Production)
Base Config
yaml
# config/deploy.yml
service: myapp
image: username/myapp
registry:
username: username
password:
- KAMAL_REGISTRY_PASSWORD
Staging
yaml
# config/deploy.staging.yml
servers:
web:
hosts:
- staging.example.com
env:
clear:
RAILS_ENV: staging
proxy:
host: staging.myapp.com
Production
yaml
# config/deploy.production.yml
servers:
web:
hosts:
- prod1.example.com
- prod2.example.com
env:
clear:
RAILS_ENV: production
proxy:
ssl: true
host: myapp.com
require_destination: true # Prevent accidental deploys
Secrets Management
bash
# .kamal/secrets KAMAL_REGISTRY_PASSWORD=your-password RAILS_MASTER_KEY=your-key DATABASE_URL=postgresql://user:pass@host:5432/db REDIS_URL=redis://host:6379
yaml
# config/deploy.yml
registry:
password:
- KAMAL_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
Important: Add .kamal/secrets* to .gitignore!
Hooks
bash
.kamal/hooks/ ├── pre-deploy # Before deploy ├── post-deploy # After deploy └── pre-traefik-reboot # Before proxy restart
Example: Pre-deploy Hook
bash
#!/bin/bash # .kamal/hooks/pre-deploy kamal app exec 'bin/rails db:migrate'
Make executable: chmod +x .kamal/hooks/*
Zero-Downtime Flow
- •Health check old version (proxy routes traffic)
- •Build new image
- •Push to registry
- •Pull on servers
- •Start new container alongside old
- •Health check new version
- •Proxy gradually shifts traffic
- •Stop old container
- •Cleanup based on retention settings
Proxy (Traefik) is key to zero-downtime.
Rails Patterns
Database Migrations
bash
# Via hook (recommended) # .kamal/hooks/pre-deploy kamal app exec 'bin/rails db:migrate' # Or manually kamal app exec 'bin/rails db:migrate' kamal deploy
Sidekiq Workers
yaml
servers:
web:
hosts:
- web1.example.com
workers:
hosts:
- worker1.example.com
cmd: bundle exec sidekiq
Console Access
bash
kamal app exec 'bin/rails console' kamal app exec 'bin/rails dbconsole'
Best Practices
✅ DO
- •Use destinations for staging/production
yaml
require_destination: true
- •Configure health checks
yaml
healthcheck: path: /up interval: 10s
- •Set retention limits
yaml
retain_containers: 5 retain_images: 5
- •
Use secrets file, add to
.gitignore - •
Run accessories on separate hosts
- •
Use pre-deploy hooks for migrations
- •
Specific image tags in production
yaml
image: username/myapp:v1.2.3 # Not :latest
❌ DON'T
- •Don't commit secrets to git
- •Don't expect zero-downtime for accessories
- •Don't skip health checks
- •Don't use :latest tag in production
- •Don't deploy to production without testing staging first
Workflow
bash
# 1. Initial setup kamal setup kamal accessory boot all # 2. Development cycle git commit -am "Update" git push kamal deploy -d staging # Test staging kamal deploy -d production # 3. Monitoring kamal app logs -f kamal accessory logs db -f # 4. Troubleshooting kamal server containers kamal app logs --tail 100 kamal prune -y
Troubleshooting
bash
# Check status kamal server containers # View logs kamal app logs --tail 100 kamal accessory logs db # Health check curl https://myapp.com/up # Increase timeouts # In config/deploy.yml: deploy_timeout: 60 readiness_delay: 10 # Cleanup kamal prune -y
Summary
- •Kamal = Zero-downtime Docker deployments
- •Default in Rails 8
- •Zero-downtime via Traefik proxy
- •Accessories = No zero-downtime (plan maintenance)
- •Destinations = Staging/production separation
- •Secrets =
.kamal/secrets(never commit!)
References
Last updated: December 2025