Bối cảnh: Tại sao không phải Kubernetes, và tại sao lại là Kamal?
Kubernetes mạnh, nhưng setup một cluster K8s cho một app Rails hay Node.js chạy trên VPS $5/tháng là overkill hoàn toàn. Mình đã trải qua giai đoạn đó — viết YAML cho Deployment, Service, Ingress, cert-manager… chỉ để deploy một app có vài trăm users. Không đáng.
Còn cách cũ hơn? SSH vào server, kéo image mới, restart container thủ công. Đơn giản, nhưng không có zero-downtime, không rollback tự động, và khi cần deploy lên nhiều server thì script Bash bắt đầu trở thành nỗi ác mộng bảo trì.
Kamal (trước đây gọi là MRSK, do team Basecamp/37signals phát triển) lấp vừa khoảng giữa đó. Những gì nó mang lại:
- Deploy qua SSH trực tiếp — không cần cài agent hay daemon trên server
- Zero-downtime deployment bằng container swap, dùng Traefik làm reverse proxy
- Tự động quản lý SSL qua Let’s Encrypt (thông qua Traefik)
- Rollback về version trước chỉ với một lệnh:
kamal rollback - Multi-server support — deploy lên 5 server cùng lúc cũng được
Mình đã chuyển từ docker-compose v1 sang v2 cho toàn bộ stack khi rebuild lại hạ tầng, và quá trình đó khá smooth. Nhưng vẫn còn pain point: mỗi lần deploy vẫn phải SSH tay, copy-paste lệnh, và cầu mong không có downtime. Kamal giải quyết đúng cái đó.
Cài đặt Kamal
Kamal là Ruby gem, nhưng bạn không cần biết Ruby để dùng. Ruby 3.1+ cần có trên máy local — không phải server.
# macOS — dùng rbenv
brew install rbenv
rbenv install 3.2.2
rbenv global 3.2.2
# Ubuntu/Debian (máy local)
sudo apt install ruby ruby-dev build-essential
# Cài Kamal
gem install kamal
# Kiểm tra
kamal version
Yêu cầu phía VPS (server):
- Ubuntu 20.04+ hoặc Debian 11+
- Docker chưa cần cài sẵn — Kamal tự cài khi setup
- SSH key đã cấu hình (không dùng password)
- User có quyền sudo
Kiểm tra SSH access trước khi làm gì cả:
ssh user@your-vps-ip "echo OK"
Cấu hình chi tiết với deploy.yml
Vào thư mục app, init project Kamal:
kamal init
Lệnh này tạo file config/deploy.yml — file quan trọng nhất. Dưới đây là ví dụ cấu hình thực tế cho một app web thông thường:
# config/deploy.yml
service: myapp
image: ghcr.io/your-username/myapp
servers:
web:
hosts:
- 103.x.x.x
labels:
traefik.http.routers.myapp-secure.rule: Host(`myapp.com`)
traefik.http.routers.myapp-secure.tls: true
traefik.http.routers.myapp-secure.tls.certresolver: letsencrypt
registry:
server: ghcr.io
username: your-github-username
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
APP_ENV: production
PORT: 3000
secret:
- DATABASE_URL
- SECRET_KEY_BASE
healthcheck:
path: /up
port: 3000
interval: 3s
timeout: 10s
retries: 5
traefik:
options:
publish:
- "443:443"
volume:
- "/letsencrypt/acme.json:/letsencrypt/acme.json"
args:
entryPoints.web.address: ":80"
entryPoints.websecure.address: ":443"
certificatesResolvers.letsencrypt.acme.email: "[email protected]"
certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
certificatesResolvers.letsencrypt.acme.tlschallenge: true
accessories:
db:
image: postgres:16
host: 103.x.x.x
port: 5432
env:
clear:
POSTGRES_DB: myapp_production
secret:
- POSTGRES_PASSWORD
volumes:
- /data/postgres:/var/lib/postgresql/data
redis:
image: redis:7
host: 103.x.x.x
port: 6379
volumes:
- /data/redis:/data
Tạo file .env ở thư mục gốc project (không commit vào git):
# .env
KAMAL_REGISTRY_PASSWORD=ghp_your_github_token
DATABASE_URL=postgresql://user:pass@localhost/myapp_production
SECRET_KEY_BASE=your_long_random_secret
POSTGRES_PASSWORD=strong_db_password
# Thêm vào .gitignore ngay
echo ".env" >> .gitignore
Mẹo hay khi cấu hình
Healthcheck là bắt buộc nếu muốn zero-downtime thật sự. Kamal giữ container cũ chạy cho đến khi container mới pass healthcheck. Không có endpoint /up? Tự tạo một route trả về HTTP 200 là đủ.
Dùng volumes cho persistent data — uploaded files, storage, cache. Ví dụ với Rails:
volumes:
- /data/myapp/storage:/rails/storage
Multi-server deploy — Kamal xử lý song song, rolling update mặc định:
servers:
web:
hosts:
- 103.x.x.1
- 103.x.x.2
workers:
hosts:
- 103.x.x.3
cmd: bundle exec sidekiq
Deploy lần đầu
# Setup server lần đầu — cài Docker, Traefik, accessories
kamal setup
# Mỗi lần deploy sau đó
kamal deploy
Lần đầu chạy kamal setup, Kamal SSH vào server và tự lo phần còn lại: cài Docker, khởi Traefik, start Postgres/Redis, pull image rồi chạy app. Mất khoảng 2-3 phút. Từ lần thứ hai trở đi, kamal deploy thường xong trong 30-60 giây.
Kiểm tra & Monitoring sau khi deploy
App đã chạy trên production — nhưng công việc không dừng ở đó. Mấy lệnh sau sẽ cần dùng thường xuyên:
# Xem logs realtime
kamal app logs -f
# Trạng thái containers trên từng server
kamal app details
# SSH vào container đang chạy để debug
kamal app exec --interactive "bash"
# Chạy lệnh một lần trong container (ví dụ: migrate DB)
kamal app exec "rails db:migrate"
# Xem logs Traefik
kamal traefik logs
# Rollback về version trước (rất nhanh, dùng image đã có sẵn)
kamal rollback
Kiểm tra SSL và routing
Muốn xác nhận SSL hoạt động đúng, chạy nhanh:
curl -I https://myapp.com
Thấy HTTP/2 200 kèm certificate từ Let’s Encrypt là ngon lành. Lần đầu Let’s Encrypt cấp cert có thể mất từ vài giây đến 2-3 phút — trong lúc đó Traefik dùng self-signed cert tạm thời, không cần lo.
Lỗi thường gặp và cách xử lý
Port 80/443 bị chiếm: Nếu Nginx hoặc Apache đang chạy trên server, Traefik sẽ không bind được port. Dừng service cũ trước:
ssh user@vps "sudo systemctl stop nginx && sudo systemctl disable nginx"
acme.json permission lỗi: File lưu Let’s Encrypt certificate cần permission 600, không thì Traefik từ chối đọc:
ssh user@vps "sudo touch /letsencrypt/acme.json && sudo chmod 600 /letsencrypt/acme.json"
Image pull chậm: Dùng GitHub Container Registry (ghcr.io) thay Docker Hub. Pull image ~500MB từ ghcr.io về server ở châu Á thường mất 20-40 giây; cùng image đó qua Docker Hub có thể lên 2-3 phút.
Rõ ràng Kamal không dành cho bài toán 100 microservices hay cần horizontal autoscaling. Nhưng nếu bạn có 1-5 server, app dưới vài nghìn concurrent users, và không muốn mất một tuần setup K8s — đây là lựa chọn đúng. Mình dùng cho một SaaS khoảng 800 users, chạy ổn 6 tháng nay, chưa cần đụng vào infrastructure lần nào.

