Agent Skills: Kamal 2 Coder

This skill guides deploying Rails applications with Kamal 2. Use when configuring deploy.yml, setting up kamal-proxy, managing secrets, accessories, or preparing servers for container deployment.

UncategorizedID: majesticlabs-dev/majestic-marketplace/kamal-coder

Install this agent skill to your local

pnpm dlx add-skill https://github.com/majesticlabs-dev/majestic-marketplace/tree/HEAD/plugins/majestic-rails/skills/kamal-coder

Skill Files

Browse the full folder contents for kamal-coder.

Download Skill

Loading file tree…

plugins/majestic-rails/skills/kamal-coder/SKILL.md

Skill Metadata

Name
kamal-coder
Description
This skill guides deploying Rails applications with Kamal 2. Use when configuring deploy.yml, setting up kamal-proxy, managing secrets, accessories, or preparing servers for container deployment.

Kamal 2 Coder

Servers need Docker, SSH access, and ports 22/80/443 open. Provision with Ansible or cloud-init.

Configuration: config/deploy.yml

Minimal Setup

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

Multi-Role Setup (Web + Job Worker)

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 container
  • Set SOLID_QUEUE_IN_PUMA: false to disable in-process queue
  • Job role has no proxy — only web role serves HTTP traffic

With Local Registry

Eliminates 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

With Accessories

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

Docker Volumes for Persistence

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.

Proxy Configuration (kamal-proxy)

Kamal 2 uses kamal-proxy (not Traefik). It handles SSL termination, routing, and zero-downtime deploys.

Basic SSL

proxy:
  ssl: true
  host: myapp.com

Automatic Let's Encrypt certificate provisioning — no manual cert management.

Custom Port

proxy:
  ssl: true
  host: myapp.com
  app_port: 3000

Multiple Hosts

proxy:
  ssl: true
  hosts:
    - myapp.com
    - www.myapp.com

Health Check

proxy:
  ssl: true
  host: myapp.com
  healthcheck:
    path: /up
    interval: 3
    timeout: 3

Response Timeout

proxy:
  ssl: true
  host: myapp.com
  response_timeout: 30

Secrets: .kamal/secrets

Kamal reads secrets from .kamal/secrets (git-ignored).

With 1Password CLI

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")

With Environment Variables

KAMAL_REGISTRY_PASSWORD=$DOCKERHUB_TOKEN
RAILS_MASTER_KEY=$RAILS_MASTER_KEY
DATABASE_URL=$DATABASE_URL

Multi-Environment

# 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

Common Commands

First Deployment

# One-time: installs kamal-proxy, pushes image, deploys
kamal setup

# Subsequent: builds, pushes, rolling restart
kamal deploy

Regular Operations

kamal deploy                    # Deploy latest
kamal deploy --version=abc123   # Deploy specific version
kamal deploy -d staging         # Deploy to staging
kamal redeploy                  # Redeploy without building

Rollback

kamal app containers            # List available versions
kamal rollback <version>        # Rollback to specific version

Debugging

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

Accessories

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 Configuration

Native Builds

builder:
  arch: amd64

Multi-Architecture

builder:
  multiarch: true

Remote Builder

builder:
  remote:
    arch: amd64
    host: ssh://builder@build-server

Build Arguments

builder:
  args:
    RUBY_VERSION: "3.3.0"

Hooks

Pre-Deploy

# .kamal/hooks/pre-deploy
#!/bin/sh
echo "Running pre-deploy checks..."

Post-Deploy

# .kamal/hooks/post-deploy
#!/bin/sh
echo "Deploy complete: $(date)"
curl -s https://notify.example.com/deploy

Provisioning Workflow

Ansible + Kamal Pipeline

# 1. Ansible: Configure server
ansible-playbook -i hosts.ini playbook.yml

# 2. Kamal: Bootstrap and deploy
kamal setup

What Ansible Should Configure

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 |

Litestream Backup Accessory

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.

Directory Structure

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

Troubleshooting

| 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 |

References