Deploy Rails apps to Ubuntu servers with Kamal
Kamal เป็นเครื่องมือที่ใช้สำหรับ deploy web app ด้วย Docker — เทียบกับ Capistrano ที่ ssh เข้าไป pull code แล้ว restart Puma/Nginx โดยตรง Kamal จะ build image บนเครื่อง dev push ขึ้น registry แล้วให้ server pull ลงมารันใน Docker container ทั้งหมด ทำให้ environment เหมือนกันทุกที่และ rollback ง่าย
ในที่นี้เราจะลองใช้เครื่องมือนี้ deploy ลง server ของเราเอง
โพสต์นี้เขียนสมัย Kamal 1.x ใช้
.envเก็บ secret และ Traefik เป็น reverse proxy ถ้าใช้ Kamal 2 (default ของ Rails 8+) ที่ใช้.kamal/secretsกับkamal-proxyดูที่ Deploy Rails 8.1 with Kamal on a VM ซึ่งครอบ Registry-Free Deployment ด้วย
Prerequisites
สำหรับเครื่อง local:
- Ruby 3.3.4
- Rails 7.2.0.rc1
- Kamal 1.8.1
- Docker Desktop (หรือ OrbStack บน macOS)
- Docker Hub account (สำหรับใช้เป็น registry)
ฝั่ง server:
- Ubuntu 22.04+
- SSH access พร้อม sudo
Preparing Server
เริ่มจากที่เราติดตั้ง Ubuntu server หลังจากติดตั้งเสร็จให้ทำการตั้งค่า firewall1 ให้เรียบร้อยเสียก่อน
จากนั้นตรวจดูว่าเราสามารถ ssh เข้าไปได้:
1
2
ssh ubuntu@SERVER_ADDRESS
exit
แล้วก็ทำการอนุญาตให้ login ได้โดยไม่ต้องใช้ password (Kamal ต้องการ key-based auth สำหรับ deploy แบบ non-interactive):
1
2
ssh-copy-id ubuntu@SERVER_ADDRESS
ssh ubuntu@SERVER_ADDRESS
อัพเดทและอัพเกรด server ให้เรียบร้อย:
1
2
sudo apt update
sudo apt upgrade -y
Install Docker2
- ตั้งค่า Docker’s
aptrepository:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Add Docker's official GPG key:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
- ติดตั้ง Docker packages ตัวล่าสุด:
1
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
- อนุญาตให้รัน docker ได้โดยไม่ต้องใช้ sudo:
1
2
3
sudo usermod -a -G docker ubuntu
sudo apt install -y curl git
exit
ตรวจว่า docker รันอยู่มั้ยโดยการ login เข้าอีกครั้ง:
1
2
3
$ ssh ubuntu@SERVER_ADDRESS
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ใน Rails application ที่เราจะสร้าง เราจะใช้ SQLite ในการทดลองนี้ เพราะฉะนั้นเราจะต้องสร้าง directory เพื่อให้ Docker มา mount ไฟล์ไว้ที่นี่ (จะถูก bind-mount เข้า container ตอน deploy):
1
2
3
mkdir hello-storage
sudo chmod 777 hello-storage
exit
chmod 777เปิดสิทธิ์ให้ทุก user บน server อ่าน/เขียน/รันได้ — สะดวกตอนทดลองแต่ไม่ปลอดภัยบน production จริง ทางที่ดีกว่าคือใช้chmod 770แล้วตั้ง group ให้ตรงกับ user ที่รันใน container (UID 1000 ของ Rails image default)
Preparing Rails
Create new rails application
1
2
3
4
5
rails new hello
cd hello
bundle lock --add-platform aarch64-linux
bin/rails g scaffold Post body:text
bin/setup
bundle lock --add-platform aarch64-linux เพิ่ม Linux ARM64 ลง Gemfile.lock — ถ้าเครื่อง dev เป็น Apple Silicon (M1/M2/M3) bundler default จะ resolve gem แค่สำหรับ arm64-darwin ตอน build image สำหรับ Linux server (ที่อาจเป็น amd64 หรือ arm64) จะ error gem ไม่ตรง platform บรรทัดนี้บอก bundler ให้เพิ่ม platform เผื่อไว้
แก้ไข routes.rb:
1
2
3
4
Rails.application.routes.draw do
resources :posts
root "posts#index"
end
ปิด force SSL ก่อน เพราะยังไม่ได้ตั้ง SSL certificate ให้ Traefik — ถ้าเปิดไว้จะ redirect HTTP → HTTPS แล้ว browser โหลดไม่ขึ้น:
1
config.force_ssl = false
เสร็จแล้วลองรันดู:
1
2
bin/rails s
open http://127.0.0.1:3000/
Install Kamal
1
2
gem install kamal
kamal init --bundle
kamal init --bundle generate ไฟล์ที่จำเป็น: config/deploy.yml, .kamal/hooks/, bin/kamal wrapper script และเพิ่ม kamal ลง Gemfile ของ project (--bundle flag) เพื่อให้ทีมใช้ Kamal version เดียวกัน
ต่อไปก็แก้ไฟล์ deploy.yml ดังตัวอย่างนี้:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
service: hello
image: phuwanart/hello
servers:
- SERVER_ADDRESS
registry:
username: phuwanart
password:
- KAMAL_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
ssh:
user: ubuntu
volumes:
- "./hello-storage:/rails/storage"
อธิบายแต่ละ field:
service— ชื่อ app ใช้เป็น prefix ของ container (hello-web-<version>)image— ชื่อ image บน registry (<docker-hub-user>/<image-name>)servers— รายการ IP/hostname ของ server ที่จะ deployregistry— credential สำหรับ push/pull image —passwordอ่านจาก env var ชื่อKAMAL_REGISTRY_PASSWORD(ค่ามาจาก.env)env.secret— รายการ env var ที่จะ inject เข้า container — Kamal อ่านจาก.envของเครื่อง dev ตอน deployssh.user— user ที่ Kamal ssh เข้า server (default คือroot)volumes— bind-mount จาก host เข้า container./hello-storageคือ path relative ของ deploy user บน server (/home/ubuntu/hello-storage)
ในที่นี้เราจะใช้ registry ที่ Docker Hub
จากนั้นไปดูไฟล์ .env:
1
2
KAMAL_REGISTRY_PASSWORD=change-this
RAILS_MASTER_KEY=another-env
เราจะต้องสร้าง access token เพื่อเอามากำหนดใน KAMAL_REGISTRY_PASSWORD
สำหรับ permissions จะให้เป็น Read & Write:
เมื่อ Generate แล้วจะได้ access token มาแล้วก็ copy ไปใส่ .env ได้เลย:
อีกค่าที่ต้องกำหนด RAILS_MASTER_KEY ให้ copy แล้วเอาไปใช้ได้ดังนี้:
1
cat config/master.key | pbcopy
ต้องไม่ลืมแก้ไข database.yml เสียก่อน — Rails default จะวาง SQLite ที่ db/production.sqlite3 แต่เราต้องการให้อยู่ใน storage/ ที่ bind-mount ออกมา ไม่งั้น database จะหายทุกครั้งที่ deploy ใหม่ (เพราะ container ใหม่ไม่มี database จาก container เก่า):
1
2
3
production:
<<: *default
database: storage/production.sqlite3
เสร็จแล้วก็ให้ git add และ git commit เสียก่อน
ต่อไปให้รัน Docker Desktop จากนั้นรันคำสั่งนี้:
1
kamal setup
kamal setup ทำทุกอย่างในคำสั่งเดียว: install Docker บน server (ถ้ายังไม่มี), login เข้า Docker Hub, build image บนเครื่อง dev, push ขึ้น registry, ssh เข้า server pull image ลงมา, start Traefik (reverse proxy), start app container, ผูก app เข้ากับ Traefik
รอจนเสร็จ แล้วตรวจดูผล:
1
2
open http://SERVER_ADDRESS/up
open http://SERVER_ADDRESS/posts
เสร็จ!!!
ครั้งต่อไปใช้ kamal deploy พอ — แค่ build + push + rolling update container
Troubleshooting
unauthorized: incorrect username or password ตอน push image ยังไม่ได้ generate Docker Hub access token หรือใส่ผิดใน .env — ตรวจค่า KAMAL_REGISTRY_PASSWORD ตรงกับ token ที่สร้างไว้
permission denied ตอนเขียน SQLite ใน production hello-storage บน server permission ไม่พอ — ลอง ls -la hello-storage ถ้าไม่ใช่ drwxrwxrwx ให้ chmod 777 (หรือ 770 + ตั้ง group ตามที่ note ไว้ข้างบน)
Browser blocks HTTP — redirect to HTTPS ตอนเปิด /up ลืมตั้ง config.force_ssl = false หรือยังไม่ได้ commit แก้แล้ว kamal deploy ใหม่
Conclusion
ยังต้องมีอะไรต้องทำอีกเยอะหลังจากนี้ ไม่ว่าจะเป็นการทำ SSL, เปลี่ยน database ไปใช้ตัวอื่น, ใช้ Nginx อะไรเหล่านี้ และที่ต้องหาเพิ่มคือจะใช้ registry ที่ไหนได้บ้างนอกจาก Docker Hub เพราะว่ามันได้แค่ 1 private repository เท่านั้นเอง
ปัญหา “registry ฟรีได้แค่ 1 private repo” ได้รับการแก้ไขใน Kamal 2 ด้วย Registry-Free Deployment ที่รัน Docker registry บน deploy server เลย ดูวิธีใช้ที่ Deploy Rails 8.1 with Kamal on a VM

