Overview

Kamal is 37signals’ deployment tool (powers Basecamp & HEY) designed for zero-downtime deployments to any VPS.
Best for: Teams needing zero-downtime deploys and preferring CLI over web UI.

Why Kamal?

  • ✅ Zero-downtime deployments
  • ✅ Simple YAML configuration
  • ✅ Git-based workflow
  • ✅ Built-in secret management
  • ✅ Battle-tested by 37signals

Prerequisites

Local machine:
  • Ruby 3.0+ (ruby --version)
  • Docker installed
  • SSH access to VPS
VPS:
  • Ubuntu 24.04 LTS
  • Docker installed
  • SSH key configured

Installation

1. Install Kamal

gem install kamal

2. Initialize Configuration

In your ripplecore-forge repository:
kamal init
This creates config/deploy.yml.

3. Configure deploy.yml

Edit config/deploy.yml:
service: ripplecore
image: your-username/ripplecore

servers:
  web:
    - 46.224.2.100

registry:
  username: your-dockerhub-username
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  clear:
    NODE_ENV: production
  secret:
    - DATABASE_URL
    - REDIS_URL
    - BETTER_AUTH_SECRET

ssh:
  user: root

healthcheck:
  path: /api/health
  port: 3000

accessories:
  postgres:
    image: postgres:18-alpine
    host: 46.224.2.100
    port: 5432
    env:
      clear:
        POSTGRES_DB: ripplecore
        POSTGRES_USER: ripplecore
      secret:
        - POSTGRES_PASSWORD
    directories:
      - data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    host: 46.224.2.100
    port: 6379
    cmd: --requirepass ${REDIS_PASSWORD}
    directories:
      - data:/data

traefik:
  options:
    publish:
      - 443:443
    volume:
      - /letsencrypt:/letsencrypt
  args:
    entryPoints.web.address: ":80"
    entryPoints.websecure.address: ":443"
    certificatesResolvers.letsencrypt.acme.email: "your@email.com"
    certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
    certificatesResolvers.letsencrypt.acme.httpchallenge: true
    certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web

4. Set Environment Variables

Create .env:
KAMAL_REGISTRY_PASSWORD=your_dockerhub_token
DATABASE_URL=postgresql://ripplecore:PASSWORD@postgres:5432/ripplecore
REDIS_URL=redis://:PASSWORD@redis:6379
BETTER_AUTH_SECRET=your_secret
POSTGRES_PASSWORD=your_postgres_password
REDIS_PASSWORD=your_redis_password
Add .env to .gitignore!

Deployment

Initial Setup

# Set up accessories (PostgreSQL, Redis)
kamal accessory boot all

# Run migrations
kamal app exec -i "pnpm db:push"

# Deploy application
kamal deploy

Subsequent Deploys

# Zero-downtime deploy
kamal deploy
Kamal automatically:
  1. Builds new Docker image
  2. Pushes to registry
  3. Pulls on VPS
  4. Starts new container
  5. Health checks new container
  6. Stops old container (zero downtime!)

Commands

# Deploy latest code
kamal deploy

# View logs
kamal app logs --follow

# SSH into server
kamal app exec -i bash

# Restart app
kamal app restart

# Roll back to previous version
kamal app rollback

# View status
kamal details

# Stop application
kamal app stop

# Start PostgreSQL
kamal accessory boot postgres

# Backup database
kamal accessory exec postgres -- pg_dump -U ripplecore ripplecore

Configuration Details

Multiple Apps

For multiple services (app, api, web):
servers:
  web:
    hosts:
      - 46.224.2.100
    labels:
      traefik.http.routers.app.rule: "Host(`app.yourdomain.com`)"

  api:
    hosts:
      - 46.224.2.100
    labels:
      traefik.http.routers.api.rule: "Host(`api.yourdomain.com`)"
    cmd: node apps/api/server.js

Custom Dockerfile

Specify in deploy.yml:
builder:
  dockerfile: apps/app/Dockerfile
  context: .

Migration from Dokploy

1. Export Dokploy Data

# Backup database
docker exec ripplecore-postgres pg_dump -U ripplecore > backup.sql

2. Deploy with Kamal

# Initialize and configure Kamal
kamal init
# Edit deploy.yml (see above)

# Boot accessories
kamal accessory boot all

# Deploy app
kamal deploy

3. Restore Data

cat backup.sql | kamal accessory exec postgres -i -- psql -U ripplecore ripplecore

4. Update DNS

Point to new VPS if different. Migration time: 2-3 hours

Pros & Cons

Pros:
  • ✅ Zero-downtime deploys
  • ✅ Simple YAML config
  • ✅ Git-based workflow
  • ✅ Built-in secrets
  • ✅ Battle-tested
Cons:
  • ❌ Requires Ruby locally
  • ❌ CLI-only (no GUI)
  • ❌ Manual monitoring setup
  • ❌ Fewer docs than Dokploy

Best Practices

  1. Use CI/CD: Run kamal deploy from GitHub Actions
  2. Health Checks: Always configure health checks
  3. Backup: Automate database backups
  4. Test Locally: Use kamal envify to test configs

Resources

Kamal is perfect for teams comfortable with CLI tools who need zero-downtime deployments.