Deploy Rails 8.1 with Kamal on a VM (Experimental)
หลังจาก Rails 8.1 ปล่อยออกมา ฟีเจอร์ที่รออยู่นานคือ Registry-Free Kamal Deployments
ปกติแล้ว Kamal ต้องการ Docker registry (Docker Hub, GHCR, registry.digitalocean.com, ฯลฯ) เป็นตัวกลาง — เครื่อง dev docker push ขึ้นไป แล้ว server docker pull ลงมารัน ปัญหาคือ Docker Hub แบบ free มี private repo ได้แค่ 1 ตัว ส่วน GHCR ก็ผูกอยู่กับ GitHub ถ้ามีหลายแอปต้องจ่ายเงินหรือเปลี่ยน provider
Kamal 2.7+ ที่มากับ Rails 8.1 แก้ปัญหานี้ด้วยการ run Docker registry container บน deploy server เลย (default ที่ localhost:5555) — Kamal build image บนเครื่อง dev แล้ว push ตรงไปที่ registry บน server ผ่าน SSH tunnel ไม่ต้องพึ่ง registry ภายนอกเลย
โพสต์นี้พา deploy Rails 8.1 บน VM ด้วยวิธีนี้ — ใช้ได้ทั้ง VM ที่บ้าน, DigitalOcean Droplet, Hetzner, หรือ VPS provider อื่นที่ให้เราเข้าถึง Ubuntu ผ่าน SSH
Prerequisites
- Rails 8.1+ บนเครื่อง dev
- Ruby 3.3+
- VM/Server (Ubuntu 22.04+ แนะนำ) ที่เข้าถึงด้วย SSH ได้ พร้อม sudo access
- Docker Desktop หรือ OrbStack บนเครื่อง dev (สำหรับ build image)
Getting Started
ในการทดลองนี้จะลอง deploy บน VM ก่อน ซึ่งน่าจะเป็นแนวทางในการ deploy บน server จริง ๆ ของเรา หรือบน cloud service ที่มีการจัดการ VPS ให้เรา อย่างเช่น DigitalOcean
Create a Server
ติดตั้ง SSH server และตั้ง firewall ให้เปิดเฉพาะ port ที่จำเป็น:
1
2
3
4
5
6
7
8
sudo apt install openssh-server ufw
sudo ufw status
sudo ufw enable
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo systemctl restart ssh
sudo systemctl status ssh
UFW (Uncomplicated Firewall) default จะ deny ทุก inbound port — ต้อง allow ssh (22), http (80), และ https (443) เพราะ Kamal-proxy ต้องการ 80/443 สำหรับ serve app และ Let’s Encrypt cert (ถ้าเปิดใช้ SSL)
Create Deploy User
ห้ามใช้ root deploy โดยตรง — สร้าง user แยกชื่อ deploy:
1
sudo adduser deploy
เพิ่ม deploy เข้า sudo group เพื่อให้รันคำสั่ง privileged ได้ (Kamal ต้องการตอน install Docker):
1
sudo adduser deploy sudo
login เข้า deploy user:
1
su deploy
เพิ่ม group docker และใส่ deploy เข้ากลุ่ม เพื่อให้รัน docker ได้โดยไม่ต้อง sudo:
1
2
sudo addgroup docker
sudo usermod -aG docker $USER
ต้อง logout แล้ว login ใหม่ (เพื่อให้สิทธิ์กลุ่ม docker มีผล)
สร้าง directory สำหรับ storage ของ Rails application (จะถูก bind-mount เข้า container ทีหลัง):
1
mkdir blog_storage
แก้ไฟล์ sudoers ให้ deploy รัน sudo ได้โดยไม่ต้องใส่ password:
1
sudo visudo
เพิ่ม deploy ALL=(ALL) NOPASSWD:ALL หลังบรรทัด %sudo ALL=(ALL:ALL) ALL:
1
2
3
4
# Allow members of group sudo to execute any command
%sudo ALL=(ALL:ALL) ALL
deploy ALL=(ALL) NOPASSWD:ALL
ทำไมต้อง NOPASSWD — Kamal ssh เข้าไปรัน command แบบ non-interactive ไม่มีทาง input password ระหว่าง deploy ถ้าไม่ตั้ง NOPASSWD ทุก task ที่ใช้ sudo จะค้าง
Add SSH Key for Deploy
Using ssh-copy-id
1
brew install ssh-copy-id
1
ssh-copy-id deploy@1.2.3.4
1
ssh deploy@1.2.3.4
Using authorized_keys
อ่าน public key บนเครื่อง dev:
1
2
# for RSA SSH key
cat ~/.ssh/id_rsa.pub
1
2
# for ED25519 SSH key
cat ~/.ssh/id_ed25519.pub
บน server สร้างไฟล์ authorized_keys ของ deploy user:
1
mkdir ~/.ssh && touch ~/.ssh/authorized_keys
1
sudo vi ~/.ssh/authorized_keys
paste public key ลงไป:
1
ssh-ed25519 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX user@computer
ทดสอบจากเครื่อง dev:
1
ssh deploy@1.2.3.4
Install Docker Desktop or OrbStack
บนเครื่อง dev เลือกอย่างใดอย่างหนึ่ง:
1
brew install --cask docker-desktop
หรือ (เร็วกว่า ใช้ resource น้อยกว่าบน macOS):
1
brew install --cask orbstack
Create Rails Application
1
2
3
rails new blog
cd blog
rails generate controller home index
ตั้ง root route:
1
root "home#index"
Configuring Kamal
แก้ config/deploy.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# Name of your application. Used to uniquely configure containers.
service: blog
# Name of the container image (use your-user/app-name on external registries).
image: blog
# Deploy to these servers.
servers:
web:
- - 192.168.0.1
+ - [ip_address ของ server]
# job:
# hosts:
# - 192.168.0.1
# cmd: bin/jobs
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# ...
# proxy:
# ssl: true
# host: app.example.com
# Where you keep your container images.
registry:
# Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
server: localhost:5555
# Needed for authenticated registries.
# username: your-user
# Always use an access token rather than real password when possible.
# password:
# - KAMAL_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
secret:
- RAILS_MASTER_KEY
clear:
SOLID_QUEUE_IN_PUMA: true
aliases:
console: app exec --interactive --reuse "bin/rails console"
shell: app exec --interactive --reuse "bash"
logs: app logs -f
dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"
# Use a persistent storage volume for sqlite database files and local Active Storage files.
volumes:
- - "blog_storage:/rails/storage"
+ - "./blog_storage:/rails/storage"
asset_path: /rails/public/assets
builder:
arch: amd64
-# ssh:
-# user: app
+ssh:
+ user: deploy
จุดสำคัญในไฟล์นี้:
registry: server: localhost:5555— บรรทัดที่เป็นหัวใจของ Registry-Free Deployment — บอก Kamal ให้ใช้ registry ที่รันอยู่บน deploy server (Kamal จะตั้ง containerkamal-docker-registryให้เองตอนkamal setup) Kamal push image ผ่าน SSH tunnel เข้า port 5555 บน server โดยตรง ไม่ผ่าน internetvolumes: ./blog_storage:/rails/storage— เปลี่ยนจาก Docker named volume (blog_storage:) เป็น bind mount โฟลเดอร์ host (./blog_storage) ที่สร้างไว้ใน home ของ deploy user — backup ง่ายกว่า เห็นไฟล์จริงบน host เลย และคุม permission ได้ตรงๆssh: user: deploy— สั่ง Kamal ssh ด้วย userdeployที่เพิ่งสร้าง ไม่ใช่ rootenv.secret: RAILS_MASTER_KEY— ดึงค่ามาจาก.kamal/secrets(ส่วนถัดไป) ไม่ใช่ฮาร์ดโค้ดใน yaml
ตั้ง .kamal/secrets
ไฟล์นี้คือ bash script ที่ Kamal source ก่อน deploy เพื่อโหลด secret ลง environment ค่าจะถูกส่งเข้า container ตอน start ไม่ได้ commit ลง repo
1
RAILS_MASTER_KEY=$(cat config/master.key)
ถ้าใช้ password manager (เช่น 1Password CLI) แทน:
1
RAILS_MASTER_KEY=$(op read "op://Vault/blog/RAILS_MASTER_KEY")
Setup และ Deploy
commit ทุกอย่างให้เรียบร้อย แล้วรัน:
1
kamal setup
kamal setup ทำงานหลายอย่างในคำสั่งเดียว:
- ssh เข้า server เป็น
deployuser - ติดตั้ง Docker บน server (ถ้ายังไม่มี)
- ตั้ง
kamal-proxycontainer (Traefik รุ่นเบาที่ Kamal เขียนเอง) ฟัง port 80/443 - ตั้ง local Docker registry container ที่ port 5555
- build image บนเครื่อง dev แล้ว push ผ่าน SSH tunnel ไปที่ local registry บน server
- pull image จาก local registry แล้ว start app container
- ผูก app container เข้ากับ kamal-proxy
หลัง setup เสร็จเปิด browser ไปที่ http://<ip ของ server> จะเห็นหน้า home
ครั้งต่อไปใช้:
1
kamal deploy
แค่ build + push + restart container ไม่ต้องตั้ง infra ใหม่
Verify
ดู log ของ app:
1
kamal app logs -f
เช็ค container ที่รันบน server:
1
docker ps
ควรเห็น kamal-proxy, kamal-docker-registry, และ blog-web-<version> รันอยู่
Troubleshooting
Permission denied (publickey) ตอน Kamal ssh เข้า server SSH key บนเครื่อง dev ยังไม่ได้ copy ไปที่ ~/.ssh/authorized_keys ของ deploy user ลอง ssh deploy@<ip> ด้วยมือก่อน ถ้าผ่านค่อย retry kamal setup
sudo: a terminal is required to read the password ลืม NOPASSWD ใน /etc/sudoers Kamal จะค้างรอ password ที่ไม่มีทาง input เปิด visudo เพิ่มบรรทัด deploy ALL=(ALL) NOPASSWD:ALL แล้ว retry
Browser เข้า http://<ip> แล้ว timeout UFW block — เช็คด้วย sudo ufw status ต้องมีทั้ง 80/tcp ALLOW และ 443/tcp ALLOW
kamal-proxy start ไม่ได้ — port already in use มี service อื่นบน host ใช้ port 80/443 อยู่ (Apache, Nginx ที่ติดมาจาก default) สั่งหยุดและ disable ก่อน:
1
2
sudo systemctl stop apache2 nginx
sudo systemctl disable apache2 nginx
Image push ช้ามาก Registry-Free Kamal push image ผ่าน SSH tunnel ซึ่งช้ากว่า push ตรงไป Docker Hub สำหรับ build ขนาดใหญ่ ลองใช้ builder.remote ตามตัวอย่างใน deploy.yml (build บน remote arm/amd64 server) เพื่อข้าม transfer image ข้าม internet
Conclusion
ได้ Rails 8.1 รันบน server ด้วย Kamal โดยไม่ต้องสมัคร registry ภายนอกเลย — ทุกอย่างอยู่บน server เดียว: app, kamal-proxy, local registry
Pattern นี้เหมาะกับ:
- Single-server deployment (1 VM/VPS รันแอปเดียว)
- Side project ที่ไม่อยากจ่ายค่า Docker Hub Pro
- Self-host บน home server หรือ Raspberry Pi
กลับไปใช้ external registry เมื่อ:
- มี server หลายตัวต้อง pull image ชุดเดียวกัน
- ต้องการ rollback ไป image version เก่าๆ ที่ไม่ได้เก็บบน server ปัจจุบัน
- ใช้ CI/CD pipeline ที่ build บน GitHub Actions/GitLab CI แล้ว push ขึ้น registry กลาง