This skill guides deploying Rails applications with Kamal. Use when configuring deploy.yml, setting up accessories, managing secrets, or preparing servers for container deployment.
Servers need Docker, SSH access, and ports 22/80/443 open. Provision with Ansible or cloud-init.
service: myapp
image: myapp
servers:
web:
- 203.0.113.10
proxy:
ssl: true
host: myapp.com
registry:
username: username
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: "true"
secret:
- RAILS_MASTER_KEY
service: myapp
image: myapp
servers:
web:
- 203.0.113.10
job:
hosts:
- 203.0.113.10
cmd: bin/jobs start
proxy:
ssl: true
host: myapp.com
registry:
username: username
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
RAILS_ENV: production
SOLID_QUEUE_IN_PUMA: false
secret:
- RAILS_MASTER_KEY
Job worker notes:
cmd: bin/jobs start runs Solid Queue in a separate containerSOLID_QUEUE_IN_PUMA: false to disable in-process queueEliminates Docker Hub dependency, rate limits, and external costs:
registry:
server: localhost:5555
username: ignored
password:
- KAMAL_REGISTRY_PASSWORD
Deploy the registry as an accessory:
accessories:
registry:
image: registry:2
host: 203.0.113.10
port: "5555:5000"
volumes:
- registry_data:/var/lib/registry
accessories:
db:
image: postgres:16
host: 203.0.113.10
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: 203.0.113.10
port: 6379
directories:
- data:/data
cmd: redis-server --appendonly yes
For SQLite + ActiveStorage apps, mount a named volume:
servers:
web:
hosts:
- 203.0.113.10
volumes:
- myapp_storage:/rails/storage
labels:
docker-volume-backup.stop-during-backup: "true"
job:
hosts:
- 203.0.113.10
cmd: bin/jobs start
volumes:
- myapp_storage:/rails/storage
Both web and job containers share the same volume for database access.
Kamal 2 uses kamal-proxy (not Traefik). It handles SSL termination, routing, and zero-downtime deploys.
proxy:
ssl: true
host: myapp.com
Automatic Let's Encrypt certificate provisioning ā no manual cert management.
proxy:
ssl: true
host: myapp.com
app_port: 3000
proxy:
ssl: true
hosts:
- myapp.com
- www.myapp.com
proxy:
ssl: true
host: myapp.com
healthcheck:
path: /up
interval: 3
timeout: 3
proxy:
ssl: true
host: myapp.com
response_timeout: 30
Kamal reads secrets from .kamal/secrets (git-ignored).
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")
KAMAL_REGISTRY_PASSWORD=$DOCKERHUB_TOKEN
RAILS_MASTER_KEY=$RAILS_MASTER_KEY
DATABASE_URL=$DATABASE_URL
# config/deploy.yml ā base config
service: myapp
# config/deploy.staging.yml ā overrides
service: myapp-staging
servers:
web:
- 203.0.113.20
proxy:
host: staging.myapp.com
# .kamal/secrets.staging
RAILS_MASTER_KEY=$(op read "op://MyApp/staging/master_key")
Deploy with: kamal deploy -d staging
# One-time: installs kamal-proxy, pushes image, deploys
kamal setup
# Subsequent: builds, pushes, rolling restart
kamal deploy
kamal deploy # Deploy latest
kamal deploy --version=abc123 # Deploy specific version
kamal deploy -d staging # Deploy to staging
kamal redeploy # Redeploy without building
kamal app containers # List available versions
kamal rollback <version> # Rollback to specific version
kamal app exec --interactive bash # Shell into container
kamal app logs -f # Tail logs
kamal app exec --interactive "bin/rails console" # Rails console
kamal app exec "bin/rails db:migrate" # Run migrations
kamal accessory boot all # Start all accessories
kamal accessory reboot db # Restart specific accessory
kamal accessory exec db --interactive "psql -U postgres"
kamal accessory logs litestream # View accessory logs
builder:
arch: amd64
builder:
multiarch: true
builder:
remote:
arch: amd64
host: ssh://builder@build-server
builder:
args:
RUBY_VERSION: "3.3.0"
# .kamal/hooks/pre-deploy
#!/bin/sh
echo "Running pre-deploy checks..."
# .kamal/hooks/post-deploy
#!/bin/sh
echo "Deploy complete: $(date)"
curl -s https://notify.example.com/deploy
# 1. Ansible: Configure server
ansible-playbook -i hosts.ini playbook.yml
# 2. Kamal: Bootstrap and deploy
kamal setup
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 |
| Unattended upgrades | Automatic security patches |
For SQLite apps, add Litestream as an accessory (see litestream-coder for full config):
accessories:
litestream:
image: litestream/litestream:0.3
host: 203.0.113.10
cmd: replicate
volumes:
- myapp_storage:/rails/storage:ro
files:
- config/litestream.yml:/etc/litestream.yml
env:
secret:
- LITESTREAM_ACCESS_KEY_ID
- LITESTREAM_SECRET_ACCESS_KEY
Mount storage as read-only (:ro) ā Litestream only reads WAL files.
myapp/
āāā config/
ā āāā deploy.yml # Main Kamal config
ā āāā deploy.staging.yml # Staging overrides
āāā .kamal/
ā āāā secrets # Production secrets (git-ignored)
ā āāā secrets.staging # Staging secrets (git-ignored)
ā āāā hooks/
ā āāā pre-deploy
ā āāā post-deploy
āāā Dockerfile # Application container
āāā docker-entrypoint.sh # Entrypoint script
| Issue | Cause | Fix |
|---|---|---|
| Connection refused | Docker not running | kamal setup or check Docker service |
| 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 |
| 502 Bad Gateway | Container not healthy | Increase healthcheck timeout |
| SSL cert not issued | DNS not pointing to server | Verify DNS A record |
| Asset 404 after deploy | Volume not mounted | Check volumes: in deploy.yml |