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.json의 production 환경에서 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를 사용하세요.
구조 다이어그램
핵심 포인트
gem install kamal — Kamal CLI 설치
kamal init — config/deploy.yml 생성
deploy.yml에 서버 IP, Docker 레지스트리, 환경변수 설정
builder.local 설정 — 로컬 빌드 + 레지스트리 Push (소규모 서버 필수)
kamal setup — 최초 서버 설정 (Docker, kamal-proxy 설치)
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·네트워크·프록시 등 여러 레이어에서 문제가 발생할 수 있어 디버깅 범위가 넓음