Kamal Deployment

This guide covers deploying Lean CMS to a production server using Kamal 2.

Prerequisites

  • Ruby 3.4+
  • Docker (local, for building images)
  • A Linux server (Ubuntu 24.04 LTS recommended)
  • A domain name pointed to your server
  • A container registry (GitHub Container Registry recommended)

Initial setup

1. Server requirements

Minimum specs:

  • 1GB RAM (512MB works with swap, but not recommended)
  • 25GB disk
  • Ubuntu 24.04 LTS

If using a small droplet (512MB), add swap:

ssh root@your-server-ip "fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile && echo '/swapfile none swap sw 0 0' >> /etc/fstab"

2. SSH configuration with 1Password

Lean CMS recommends using 1Password for SSH key management:

  1. Enable 1Password SSH Agent in 1Password settings (Developer → SSH Agent)
  2. Store your SSH key in 1Password
  3. Configure ~/.ssh/config:
Host *
  IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"

Host your-server
  HostName your-server-ip
  User root

3. Container registry setup

Create a GitHub Personal Access Token with write:packages and read:packages scopes.

Create .env file (gitignored):

KAMAL_REGISTRY_PASSWORD=ghp_your_token_here

4. Configuration

Edit config/deploy.yml:

service: your-app-name
image: your-username/your-app-name

servers:
  web:
    - your-server-ip

proxy:
  ssl: true
  host: your-domain.com
  healthcheck:
    path: /up
    interval: 3
    timeout: 10

registry:
  server: ghcr.io
  username: your-username
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - RAILS_MASTER_KEY

volumes:
  - "your-app_storage:/rails/storage"

5. Secrets configuration

Edit .kamal/secrets:

KAMAL_REGISTRY_PASSWORD=$(grep KAMAL_REGISTRY_PASSWORD .env | cut -d '=' -f2)
RAILS_MASTER_KEY=$(cat config/master.key)

Deployment

First deploy

kamal setup

This will:

  1. Install Docker on the server
  2. Set up the kamal network
  3. Start kamal-proxy
  4. Build and push your image
  5. Deploy your application

Subsequent deploys

kamal deploy

Useful commands

# View logs
kamal logs

# Rails console
kamal console

# Shell access
kamal shell

# App status
kamal app details

SQLite database management

Lean CMS uses SQLite for simplicity and file-based backups.

Manual database sync

Copy local DB to production:

# Stop the container
ssh root@your-server "docker stop $(docker ps -q --filter name=your-app-web)"

# Copy database
scp storage/development.sqlite3 root@your-server:/var/lib/docker/volumes/your-app_storage/_data/production.sqlite3

# Clean WAL files and restart
ssh root@your-server "rm -f /var/lib/docker/volumes/your-app_storage/_data/production.sqlite3-{shm,wal} && docker start $(docker ps -aq --filter name=your-app-web)"

Pull production DB locally:

scp root@your-server:/var/lib/docker/volumes/your-app_storage/_data/production.sqlite3 storage/production_local.sqlite3

Automated content sync

Lean CMS includes Rake tasks for safe content synchronization. See Content Sync for the full workflow.

New project / first deploy

# 1. Develop normally in development DB
bin/rails server

# 2. Stage your development DB as production_local
bin/rails lean_cms:sync:stage

# 3. Boot Rails in production mode against the staged DB
bin/stage

# 4. When happy, push the staged DB to the real server
bin/rails lean_cms:sync:push

Ongoing sync workflow

# Lock production so editors can't change anything while you work
bin/kamal cms-lock

# Pull production DB to local
bin/rails lean_cms:sync:pull

# Make local changes, then push and unlock in one step
bin/rails lean_cms:sync:finish

Troubleshooting

Health check failures

If deploys fail with “target failed to become healthy”:

  1. Check container logs: kamal app logs
  2. Verify /up endpoint works locally
  3. Check for OOM issues (add swap if needed)
  4. Increase timeouts in deploy.yml

Database issues

SQLite uses WAL mode in production. If you see lock errors:

  1. Stop the container before any manual DB operations
  2. Always remove .sqlite3-shm and .sqlite3-wal files when replacing the DB
  3. Use the content sync Rake tasks for safe operations

SSH connection issues

  1. Verify 1Password SSH agent is running: ssh-add -l
  2. Check ~/.ssh/config has the IdentityAgent configured
  3. Ensure your SSH key is stored in 1Password

Production checklist

  • Server has adequate RAM (1GB+ recommended)
  • Swap configured if using small instance
  • SSL certificate provisioning works (DNS pointing to server)
  • Health check endpoint (/up) accessible
  • Database volume is persistent
  • Secrets configured in .kamal/secrets
  • 1Password SSH integration set up