跳转至

Kamal 2 部署工具完全指南

为什么要学 Kamal

  1. 37signals 出品,Rails 之父背书:Kamal(原名 MRSK)由 DHH(Ruby on Rails 创建者)和 37signals 团队开发,用于部署 HEY.com 和 Basecamp。它代表了"回归简单"的部署哲学——不需要 Kubernetes,不需要 PaaS,直接 SSH 到服务器部署 Docker 容器。

  2. 零停机部署,无需 K8s:Kamal 使用 Traefik 反向代理实现滚动部署(Rolling Deploy)。新容器启动并通过健康检查后,流量才切换过去,旧容器优雅退出。你获得了 K8s 级别的零停机部署,但复杂度只有它的 1/10。

  3. 任何语言、任何框架:虽然 Kamal 出自 Ruby 社区,但它本质上就是一个 Docker 部署工具。只要你的应用能打包成 Docker 镜像,不管是 Node.js、Python、Go、Rust 还是 PHP,都能用 Kamal 部署。

  4. 直接 SSH,无中间层:Kamal 通过 SSH 直接操作目标服务器。没有 Agent、没有 Daemon、不需要在服务器上安装额外软件(除了 Docker)。部署的可审计性和可调试性极强。

  5. Accessories 管理附属服务:数据库、Redis、搜索引擎等附属服务可以在 Kamal 配置中声明和管理,与主应用一起部署和维护。


核心概念详解

Kamal 是什么(白话解释)

假设你租了几台服务器,想把你的 Web 应用部署上去。传统方式:

  1. SSH 到每台服务器
  2. 拉取最新代码
  3. 构建 Docker 镜像
  4. 停掉旧容器
  5. 启动新容器
  6. 配置 Nginx
  7. 祈祷不出问题

Kamal 把这个过程自动化了。你写一个配置文件(config/deploy.yml),然后运行 kamal deploy。Kamal 自动:

  1. 在 CI 或本地构建 Docker 镜像
  2. 推送到 Registry
  3. SSH 到每台服务器
  4. 拉取新镜像
  5. 启动新容器
  6. 等健康检查通过
  7. 将流量切到新容器
  8. 停掉旧容器

核心架构

开发者本地 / CI
    ├── kamal build → Docker Image → Push to Registry
    └── kamal deploy (via SSH)
    ┌───────────────────────────────────────────┐
    │ 服务器 1                                   │
    │  ┌──────────┐  ┌──────────┐               │
    │  │ Traefik  │→│ App v2   │ (新容器)       │
    │  │ (代理)   │  │ (健康✓) │               │
    │  └──────────┘  └──────────┘               │
    │                 ┌──────────┐               │
    │                 │ App v1   │ (旧容器,待退) │
    │                 └──────────┘               │
    └───────────────────────────────────────────┘
    ┌───────────────────────────────────────────┐
    │ 服务器 2(同样的滚动部署)                  │
    └───────────────────────────────────────────┘
    ┌───────────────────────────────────────────┐
    │ 服务器 3(Accessories: DB, Redis)          │
    └───────────────────────────────────────────┘

关键概念

概念说明
Service你的应用(Web、Worker 等)
Destination部署目标环境(staging/production)
RegistryDocker 镜像仓库
ProxyKamal Proxy(基于 Traefik 的轻量代理)
Accessories附属服务(数据库、Redis 等)
Builder构建配置(本地/远程/多架构)
Healthcheck健康检查(确认新容器正常才切流量)
Hooks部署生命周期钩子

Kamal vs 其他部署方案对比

特性KamalKubernetesDocker ComposeCoolify
复杂度极高
零停机原生支持原生支持需手动支持
自动扩缩手动自动手动手动
学习曲线极高极低
多服务器支持核心特性不支持支持
配置方式YAMLYAML+大量资源YAMLWeb UI
健康检查内置内置基础基础
SSLTraefik 自动Ingress手动自动
回滚一键自动手动手动
适合规模中小型大型开发环境中小型
运维成本最低
服务发现TraefikCoreDNSDocker网络Traefik

安装与配置

安装 Kamal

# Ruby gem 安装
gem install kamal

# 或通过 Docker(不需要 Ruby)
alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock" -e SSH_AUTH_SOCK="/run/host-services/ssh-auth.sock" -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/basecamp/kamal:latest'

# 验证
kamal version

初始化项目

cd my-app
kamal init

这会创建以下文件:

config/
└── deploy.yml       # 主配置文件
.kamal/
├── hooks/           # 部署钩子
│   ├── docker-setup
│   ├── pre-connect
│   ├── pre-build
│   ├── pre-deploy
│   └── post-deploy
└── secrets          # 密钥文件

配置文件详解

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

servers:
  web:
    hosts:
      - 192.168.1.100
      - 192.168.1.101
    labels:
      traefik.http.routers.my-app.rule: Host(`app.example.com`)
    options:
      memory: 512m
      cpus: "0.5"
  worker:
    hosts:
      - 192.168.1.102
    cmd: bin/jobs

proxy:
  ssl: true
  host: app.example.com
  app_port: 3000
  healthcheck:
    path: /up
    interval: 3
    timeout: 5

registry:
  server: ghcr.io
  username: myuser
  password:
    - KAMAL_REGISTRY_PASSWORD

builder:
  arch: amd64
  # 或多架构
  # multiarch: true
  # 或远程构建
  # remote:
  #   arch: amd64
  #   host: ssh://builder@build-server

env:
  clear:
    RAILS_ENV: production
    NODE_ENV: production
    DB_HOST: 192.168.1.200
  secret:
    - RAILS_MASTER_KEY
    - DATABASE_URL
    - REDIS_URL
    - SECRET_KEY_BASE

accessories:
  db:
    image: postgres:16
    host: 192.168.1.200
    port: "127.0.0.1:5432:5432"
    env:
      clear:
        POSTGRES_DB: my_app_production
      secret:
        - POSTGRES_PASSWORD
    directories:
      - data:/var/lib/postgresql/data
    options:
      memory: 1g

  redis:
    image: redis:7-alpine
    host: 192.168.1.200
    port: "127.0.0.1:6379:6379"
    directories:
      - data:/data

# 多环境支持
# config/deploy.staging.yml 可以覆盖上面的配置

密钥管理

# .kamal/secrets 文件(不要提交到 Git)
KAMAL_REGISTRY_PASSWORD=ghp_xxxxxxxxxxxx
RAILS_MASTER_KEY=xxxxxxxxxxxx
DATABASE_URL=postgresql://user:pass@db/app
REDIS_URL=redis://redis:6379/0
SECRET_KEY_BASE=xxxxxxxxxxxx
POSTGRES_PASSWORD=xxxxxxxxxxxx
# 或使用环境变量 / 1Password / Vault
# .kamal/secrets 支持 ERB 模板
KAMAL_REGISTRY_PASSWORD=<%= `op read "op://Vault/Registry/password"`.strip %>

快速上手:5 分钟最小示例

准备工作

  1. 一台 Linux 服务器(Ubuntu 22.04),SSH 可访问
  2. Docker Hub 或 GitHub Container Registry 账号
  3. 应用有 Dockerfile

Dockerfile

FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production=false
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .
EXPOSE 3000
HEALTHCHECK CMD curl -f http://localhost:3000/up || exit 1
CMD ["node", "dist/index.js"]

配置

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

servers:
  web:
    hosts:
      - your-server-ip

proxy:
  host: app.yourdomain.com
  ssl: true
  app_port: 3000

registry:
  username: myuser
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  clear:
    NODE_ENV: production

部署

# 首次设置(安装 Docker、配置 Traefik)
kamal setup

# 后续部署
kamal deploy

# 查看状态
kamal details

# 查看日志
kamal app logs

# 回滚
kamal rollback

进阶用法

场景一:多服务器 Rolling Deploy

servers:
  web:
    hosts:
      - 10.0.0.1
      - 10.0.0.2
      - 10.0.0.3
    options:
      memory: 1g
      cpus: "1"

# Kamal 逐台服务器部署:
# 1. 在 10.0.0.1 启动新容器
# 2. 健康检查通过后切换流量
# 3. 停掉旧容器
# 4. 在 10.0.0.2 重复...
# 5. 在 10.0.0.3 重复...
# 全程零停机

场景二:Web + Worker + 定时任务

servers:
  web:
    hosts:
      - 10.0.0.1
      - 10.0.0.2
  worker:
    hosts:
      - 10.0.0.3
    cmd: node dist/worker.js
  scheduler:
    hosts:
      - 10.0.0.3
    cmd: node dist/scheduler.js

场景三:多环境部署

# config/deploy.yml(生产环境)
service: my-app
image: myuser/my-app
servers:
  web:
    hosts: [10.0.1.1, 10.0.1.2]
proxy:
  host: app.example.com
# config/deploy.staging.yml(覆盖配置)
servers:
  web:
    hosts: [10.0.2.1]
proxy:
  host: staging.example.com
env:
  clear:
    NODE_ENV: staging
# 部署到 staging
kamal deploy -d staging

# 部署到 production
kamal deploy

场景四:部署钩子

# .kamal/hooks/pre-deploy
#!/bin/sh
echo "Running pre-deploy checks..."
npm test
if [ $? -ne 0 ]; then
  echo "Tests failed! Aborting deployment."
  exit 1
fi
# .kamal/hooks/post-deploy
#!/bin/sh
echo "Post-deploy: running migrations..."
kamal app exec "npx prisma migrate deploy"

echo "Notifying team..."
curl -X POST "$SLACK_WEBHOOK" \
  -H "Content-Type: application/json" \
  -d "{\"text\": \"Deployed ${KAMAL_VERSION} to production\"}"

场景五:Accessories 管理

accessories:
  db:
    image: postgres:16
    host: 10.0.0.10
    port: "127.0.0.1:5432:5432"
    env:
      clear:
        POSTGRES_DB: app_production
        POSTGRES_USER: app
      secret:
        - POSTGRES_PASSWORD
    directories:
      - data:/var/lib/postgresql/data
    options:
      memory: 2g

  redis:
    image: redis:7-alpine
    host: 10.0.0.10
    port: "127.0.0.1:6379:6379"
    directories:
      - data:/data
    cmd: redis-server --appendonly yes

  search:
    image: getmeili/meilisearch:latest
    host: 10.0.0.10
    port: "127.0.0.1:7700:7700"
    env:
      secret:
        - MEILI_MASTER_KEY
    directories:
      - data:/meili_data
# 管理 Accessories
kamal accessory boot db        # 启动数据库
kamal accessory reboot redis   # 重启 Redis
kamal accessory logs search    # 查看搜索服务日志
kamal accessory remove db      # 移除数据库

场景六:远程构建(CI 集成)

# GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'

      - run: gem install kamal

      - uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - run: kamal deploy
        env:
          KAMAL_REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
          RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

场景七:资源限制与健康检查

servers:
  web:
    hosts: [10.0.0.1]
    options:
      memory: 512m
      cpus: "0.5"
      log-opt: max-size=50m

proxy:
  host: app.example.com
  ssl: true
  app_port: 3000
  healthcheck:
    path: /up
    interval: 3      # 每3秒检查一次
    timeout: 5        # 超时5秒
    # 默认:7次成功后才切流量

场景八:常用操作命令

# 部署
kamal deploy                  # 完整部署流程
kamal redeploy                # 快速重新部署(跳过构建)

# 应用管理
kamal app start               # 启动应用
kamal app stop                # 停止应用
kamal app restart              # 重启应用
kamal app details              # 查看详情
kamal app logs                 # 查看日志
kamal app logs -f              # 跟踪日志
kamal app exec "command"       # 在容器中执行命令

# 代理管理
kamal proxy reboot             # 重启 Traefik

# 回滚
kamal rollback                 # 回滚到上一个版本
kamal rollback [version]       # 回滚到指定版本

# 环境变量
kamal env push                 # 推送更新的环境变量

# 服务器管理
kamal server bootstrap         # 初始化服务器(安装 Docker)

# Audit
kamal audit                    # 查看部署审计日志

常见问题与排错

问题一:SSH 连接失败

# 确保 SSH key 已配置
ssh-add -l

# 测试 SSH 连接
ssh root@your-server-ip

# Kamal 默认使用 root 用户
# 如需其他用户,在 deploy.yml 中设置
# ssh:
#   user: deploy

问题二:Docker 构建失败

# 本地测试构建
docker build -t test .

# 检查 Dockerfile 是否有 HEALTHCHECK
# Kamal 依赖健康检查来判断部署是否成功

问题三:健康检查不通过

# 确保应用有健康检查端点
proxy:
  healthcheck:
    path: /up        # 应用必须在此路径返回 200
    interval: 3
    timeout: 10       # 增加超时
// 健康检查端点示例
app.get('/up', (req, res) => {
  res.status(200).send('OK');
});

问题四:部署回滚

# 查看历史版本
kamal app containers

# 回滚
kamal rollback

# 回滚到特定版本
kamal rollback abc123def

问题五:磁盘空间不足

# 清理旧镜像
kamal prune all

# 或在服务器上手动清理
ssh root@server "docker system prune -af"

问题六:如何查看部署日志

# 实时日志
kamal app logs -f

# 指定服务器
kamal app logs --hosts 10.0.0.1

# 指定行数
kamal app logs -n 200

参考资源

  • 官方文档:https://kamal-deploy.org/
  • GitHub:https://github.com/basecamp/kamal
  • DHH 介绍 Kamal:https://world.hey.com/dhh (搜索 "Kamal")
  • Kamal Handbook:https://kamal-deploy.org/docs/installation
  • 37signals 部署实践:https://dev.37signals.com/
  • Ruby on Rails Guides(Kamal 章节):https://guides.rubyonrails.org/
  • Kamal Discord:https://discord.gg/kamal