🚢

Kamal 배포

Rails 공식 배포 도구 — Docker 기반 제로 다운타임 배포

Kamal 2は Rails 8의 공식 배포 도구로, Docker 컨테이너 기반의 제로 다운타임 배포를 제공합니다. Heroku 같은 PaaS 없이도 어떤 VPS(DigitalOcean, Hetzner, AWS EC2 등)에든 배포할 수 있습니다.

Kamal이란?

Kamal(구 MRSK)은 37시그널스가 Basecamp와 HEY를 배포하기 위해 만든 도구입니다. Rails 8부터 rails new로 프로젝트를 생성하면 Kamal 설정이 기본으로 포함됩니다.

핵심 특징:

  • Docker 컨테이너 기반 배포

  • 제로 다운타임: 새 컨테이너가 헬스체크를 통과한 후에야 트래픽 전환

  • kamal-proxy: Traefik을 대체하는 경량 리버스 프록시 (Kamal 2부터)

  • SSL 자동 발급 (Let's Encrypt)

  • 롤백: kamal rollback 한 줄로 이전 버전 복원

  • 멀티 서버 배포 지원


deploy.yml 설정

# config/deploy.yml
service: my-app
image: my-dockerhub-user/my-app

servers:
  web:
    hosts:

      - 123.456.789.10
    options:
      network: "private"

proxy:
  ssl: true
  host: my-app.com

registry:
  username: my-dockerhub-user
  password:

    - KAMAL_REGISTRY_PASSWORD

env:
  secret:

    - RAILS_MASTER_KEY

    - DATABASE_URL
  clear:
    RAILS_ENV: production

builder:
  local:
    arch: amd64       # 서버 아키텍처에 맞게
    host: unix:///var/run/docker.sock


배포 플로우

kamal deploy 실행
  ↓
1. 로컬에서 Docker 이미지 빌드
  ↓
2. Docker Hub (또는 GHCR)에 이미지 Push
  ↓
3. 서버에서 이미지 Pull
  ↓
4. 새 컨테이너 시작 + 헬스체크
  ↓
5. kamal-proxy가 트래픽을 새 컨테이너로 전환
  ↓
6. 이전 컨테이너 제거


⚠️ DigitalOcean + Kamal: 로컬 빌드 권장

DigitalOcean의 소규모 Droplet(1~2GB RAM)에서 서버에서 직접 Docker 빌드하면 CPU/메모리 부하가 극심합니다. Rails 앱의 Docker 빌드는 gem install, asset precompile 등으로 많은 리소스를 소모하기 때문입니다.

권장 패턴: 로컬 빌드 → 레지스트리 Push → 서버 Pull

# config/deploy.yml
builder:
  local:
    arch: amd64
    host: unix:///var/run/docker.sock

builder.local 설정으로 로컬 머신에서 이미지를 빌드하고, Docker Hub나 GitHub Container Registry에 push한 뒤, 서버에서는 완성된 이미지를 pull만 합니다.

왜 서버 빌드가 문제인가?

  • bundle install: 수십 개 gem 컴파일 → CPU 100% + 메모리 1GB 이상

  • rails assets:precompile: Tailwind CSS + Vite 빌드 → 추가 메모리 소모

  • 1GB Droplet: OOM Killer가 프로세스를 죽이거나, 서비스 중인 앱까지 영향

  • 2GB Droplet: 빌드 중 응답 시간 10배 이상 느려짐

💡 실전 경험: Solid Queue의 메모리 문제

Solid Queue는 기본적으로 워커가 3개 뜹니다. 각 워커가 독립 프로세스로 약 178MB씩 점유하므로, Rails 본체보다 메모리를 더 많이 먹습니다.

프로세스                         메모리
─────────────────────────────────────────
ruby (Rails/Puma)                357MB
bundle (SolidQueue worker x3)    178MB x 3 = 534MB
supervisord                      18MB
─────────────────────────────────────────
합계                             약 910MB

1GB 서버에서 OOM이 나는 것이 당연합니다. 2GB Droplet에서도 OS + Docker 오버헤드를 고려하면 평소 운영 자체가 메모리 한계에 가깝습니다. 특히 배포 시점에 새 컨테이너 시작 + 기존 컨테이너 drain 과정에서 메모리 부족이 부각되어 OOM이 발생하는 경우가 많았습니다.

production 설정에서 processes: 1, threads: 1로 해도 bundle 프로세스가 3개 뜨는 점에 주의가 필요합니다. 워커 수를 줄이면 메모리를 절약할 수 있지만, 근본적으로 소규모 VPS에서는 빠듯합니다.

메모리 절약 방법:
1. YJIT 끄기 (RUBY_YJIT_ENABLE=0) — 프로세스당 30~50MB 절약 (성능은 조금 떨어짐)
2. MALLOC_ARENA_MAX=2 — Ruby의 메모리 단편화 줄이기 (프로세스당 50~100MB 절약, 효과 큼)
3. Sidekiq으로 교체 — 스레드 기반이라 프로세스 1개로 동작, 메모리 대폭 절약. 단 Redis 필요
4. Solid Queue를 별도 머신으로 분리 — SQLite 사용 시 불가능 (파일 공유 문제)

필자는 가장 간단한 환경변수 추가를 선택했습니다:

MALLOC_ARENA_MAX = "2"    # Ruby 메모리 단편화 방지
RUBY_YJIT_ENABLE = "0"   # YJIT 비활성화로 메모리 절약

이것만으로 프로세스당 80~150MB 정도 절약이 가능하지만, 근본적인 해결책은 아니었습니다.

필자는 결국 2GB Droplet에서의 불안정함을 해결하지 못하고 Fly.io로 이전했습니다. 더 큰 Droplet(4GB+)으로 올리면 해결될 수 있지만, 비용 대비 Fly.io가 더 나은 선택이었습니다.

로컬 빌드의 장점:

  • 개발 머신의 풍부한 CPU/RAM 활용 (M1/M2 Mac의 빠른 빌드)

  • 서버는 pull + 컨테이너 시작만 → 거의 부하 없음

  • 서비스 중인 앱에 영향 없이 배포 가능


주요 명령어

kamal setup          # 최초 서버 설정 (Docker, kamal-proxy 설치)
kamal deploy         # 빌드 → Push → 배포
kamal redeploy       # 기존 이미지로 재배포 (설정 변경 시)
kamal rollback       # 이전 버전으로 롤백
kamal app logs       # 앱 로그 확인
kamal app exec 'bin/rails console'  # 원격 Rails 콘솔
kamal app exec 'bin/rails db:migrate'  # 마이그레이션
kamal envify         # .env를 .kamal/secrets에 동기화


⚠️ Vite autoBuild: 프로덕션에서 반드시 false

Vite를 사용하는 Rails 앱을 배포할 때, config/vite.jsonproduction 환경에서 autoBuild: true로 설정하면 안 됩니다. autoBuild: true이면 사용자가 접속할 때마다 서버에서 vite build가 실행되어 엄청난 CPU/메모리 부하가 발생합니다.

필자의 경우, 이 설정을 놓쳐서 접속할 때마다 서버 리소스가 고갈되는 문제를 겪었습니다.

// config/vite.json
{
  "development": {
    "autoBuild": true   // ✅ 개발 환경만 true
  },
  "test": {
    "autoBuild": false
  },
  "production": {
    "autoBuild": false  // ⚠️ 반드시 false!
  }
}

프로덕션에서는 배포 시 vite build를 미리 실행 (Dockerfile의 RUN bin/rails assets:precompile)하고, 런타임에는 빌드된 에셋만 서빙해야 합니다.


Kamal vs 다른 배포 방식

항목 Kamal + VPS Heroku Fly.io Capistrano
월 비용 $5~12 (VPS) $25+ (Eco) $5+ VPS 비용만
Docker 필수 ❌ (Buildpack) ❌ (서버 직접)
제로 다운타임 ✅ (kamal-proxy) ✅ (유료) △ (설정 필요)
SSL 자동 ✅ (Let's Encrypt) 수동
SQLite 사용 ✅ (Volume) ✅ (Litestream)
서버 제어 완전 제어 제한적 제한적 완전 제어
학습 곡선 중간 낮음 중간 높음

SQLite + Kamal

Kamal로 SQLite 기반 앱을 배포하려면 Docker Volume을 사용하여 데이터베이스 파일을 컨테이너 외부에 저장해야 합니다.

# config/deploy.yml
servers:
  web:
    hosts:

      - 123.456.789.10
    options:
      network: "private"
    volumes:

      - data:/rails/storage  # SQLite DB 파일 영속화

주의: SQLite는 단일 서버에서만 사용 가능합니다. 멀티 서버 배포 시에는 PostgreSQL 등 클라이언트-서버 DB를 사용하세요.

구조 다이어그램

Kamal 배포 플로우
💻
1. 로컬에서 Docker 이미지 빌드
docker build → my-app:latest
📦
2. Docker Hub / GHCR에 Push
docker push registry/my-app
🖥️
3. 서버에서 이미지 Pull + 새 컨테이너 시작
docker pull → docker run
4. 헬스체크 통과 → kamal-proxy가 트래픽 전환
⚡ 제로 다운타임 — 이전 컨테이너 제거
❌ 서버에서 빌드 (소규모 VPS)
bundle install → CPU 100%, RAM 1GB+
assets:precompile → 추가 메모리 소모
🔥 OOM Killer 발동 / 서비스 중단
1~2GB Droplet에서 빌드 = 재앙
✅ 로컬에서 빌드 (권장)
개발 머신 → 풍부한 CPU/RAM
서버는 Pull만 → 부하 거의 없음
⚡ 서비스 무중단 배포
builder.local.arch: amd64
핵심 설정 (config/deploy.yml):
# config/deploy.yml
service: my-app
image: user/my-app
servers:
  web:
    hosts: [123.456.789.10]
builder:
  local:
    arch: amd64
핵심: <strong>Kamal + VPS</strong>로 월 $5~12에 프로덕션 운영 — 소규모 서버는 반드시 <strong>로컬 빌드</strong>

핵심 포인트

1

gem install kamal — Kamal CLI 설치

2

kamal init — config/deploy.yml 생성

3

deploy.yml에 서버 IP, Docker 레지스트리, 환경변수 설정

4

builder.local 설정 — 로컬 빌드 + 레지스트리 Push (소규모 서버 필수)

5

kamal setup — 최초 서버 설정 (Docker, kamal-proxy 설치)

6

kamal deploy — 빌드 → Push → 제로 다운타임 배포

장점

  • Rails 8 공식 — rails new로 바로 사용 가능
  • 월 $5~12로 프로덕션 운영 가능 (PaaS 대비 저렴)
  • 제로 다운타임 배포 기본 제공
  • SSL 자동 발급 (Let's Encrypt)
  • 서버 완전 제어 — 벤더 락인 없음
  • kamal rollback으로 즉시 롤백
  • SQLite 운영 가능 — Render/Heroku는 영속 볼륨 미지원으로 SQLite 불가
  • DigitalOcean 등 VPS는 お名前.com VPS보다 전반적으로 저렴하고, 서드파티 연동도 충실하며 UI가 심플함

단점

  • 서버 관리 직접 해야 함 (보안 패치, 모니터링)
  • Docker 기본 지식 필요
  • 초기 설정이 PaaS보다 복잡
  • 소규모 서버에서 서버 빌드 시 부하 문제 (로컬 빌드로 해결)
  • SSH 접근 가능한 서버 필요 (서버리스 불가)
  • VPS 운영 부담 — Docker/앱 로그가 쌓여 디스크 100% 문제, 정기적 로그 정리·용량·리소스 모니터링 등 PaaS에선 신경 쓸 필요 없는 작업이 늘어남
  • Docker 이미지 레지스트리 관리 필요 — Render/Fly.io는 코드만 push하면 자동 빌드되지만, Kamal은 Docker Hub 등에 이미지를 직접 저장해야 함. Private 레포 사용 시 추가 비용 발생 가능, 오래된 이미지가 쌓여 용량 관리도 필요
  • SQLite 운영 시 백업을 자체 구현해야 함 — Kamal 자체 문제보다 SQLite + VPS 조합의 문제. 매니지드 DB(RDS 등)는 자동 백업이 제공되지만, SQLite 파일은 cron + Litestream 등으로 직접 백업 파이프라인을 구축해야 함
  • 배포 시 트러블이 잦음 — 인프라 지식이 있는 필자도 배포할 때마다 크고 작은 문제가 발생했음. 인프라 경험이 없는 경우 문제 해결이 상당히 어려울 수 있음. Render/Fly.io는 git push만으로 배포가 완료되는 반면, Kamal은 Docker·SSH·네트워크·프록시 등 여러 레이어에서 문제가 발생할 수 있어 디버깅 범위가 넓음

사용 사례

DigitalOcean Droplet에 Rails 앱 배포 ($5~12/월) Hetzner, Vultr 등 저비용 VPS에 배포 SQLite + Volume으로 DB 운영 비용 제로 Heroku/Render 대비 월 비용 70%+ 절감 사이드 프로젝트/MVP 빠른 배포