📚

Kamal を使用したステージング環境へのデプロイ

に公開

Kamal を使用したステージング環境へのデプロイ

Rails アプリのデプロイに、 Kamal を初めて使ってみました。使うにあたっては、いきなり本番環境へデプロイするのではなく、まずは練習として LAN 内の別の PC にステージング環境を構築し、そこにデプロイをしました。その時のメモを兼ねた事例紹介の記事になります。

この記事は以下に述べます "構成" にあわせた内容であり、汎用的な内容ではありません。ただ、コンテナ・レジストリという外部サービスは必要になりますが、デプロイ先は手元の環境だけで済みますし、また用意しやすいものだと思いますので、これから Kamal を使ってみたい方や、ひとつのステージング環境の例として、参考となることを願っています。

追記@2025/11/5 コンテナ・レジストリについて、 Kamal 2.8 から外部サービスではなくローカルホスト上の Docker 環境に作られるコンテナ・レジストリを指定・利用できるようになりましたので、全て自前の環境で済ませられるようになりました!

見どころとしては、デプロイ先が Windows 上の WSL2 で起動した Linux である点と、 SSL/TLS カスタム証明書の利用といったトピックが特徴的かと思います。

デプロイ元 デプロイ先
ホストOS macOS Monterey Rocky Linux 9(Windows 11 の WSL2 上)
Dockerエンジン 28.1.1 (darwin/arm64) 28.3.1 (linux/amd64)
デプロイツール Kamal 2.7.0 -

なお Kamal は Rails 専用のツールではありませんが、事例の都合上、 Rails の設定内容が混ざっている箇所があります。できるだけ補足説明をつけていますが、ご了承ください。

0. 構成

これから紹介する例は、次のような構成です:

                              User
.....................................................................
   |        |                                      |
   |        | access                               |
   |        v                                      |
   |        443                                    |
   |    +-----------+                              |
   |    |kamal-proxy|                              |
   |    +-----------+                              |
   |        |                                      | access
   |        |forward                               |
   |        v                                      v
   |       3001                                   1080
   |   +----------------+                    +-------------+
   |   |Thruster + Rails|---["kamal"]--> 1025| MailCatcher |
   |   +----------------+    Network         +-------------+
   |        ^                                      ^
   |        | pull                                 | pull         LAN
~~~|~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~
   v push   |                                      |         Internet
 +------------------------+                 +--------------+
 |   Container Registry   |                 |  Docker Hub  |
 +------------------------+                 +--------------+
役割 説明
User ステージング環境をテストする開発者。ここからデプロイし、サービスを利用します
kamal-proxy kamal-proxy をエントリーポイント兼 SSL終端 として利用。ポート 443
Thruster + Rails Puma のラッパー Thruster を利用したアプリケーションサーバ。ポート 3001 。( Rails はポート 3000 )
MailCatcher SMTP MailCatcher を SMTP サーバとして利用。 Rails からのメールを処理し、通信は Docker 内部ネットワーク "kamal" を利用。これは自動的に作られるネットワークです
MailCatcher Web MailCatcher の Web インターフェース。 Proxy を通さず、そのままポート 1080 で公開します

プロキシ・サーバ kamal-proxy と、メールサーバの代わりになる MailCatcher は、ユーザは別途構築するなどの手間はありません。いずれも Kamal の設定に含まれており、 Kamal のコマンドによって構築(デプロイ)できます。

また各役割のサーバについて、ステージング環境ではすべて同じホストにデプロイします。上の図では別々に描かれていますが、実際は一つのホスト内にあります。

  proxy.ssl.host = servers.web.host = accessories.host
  +----------------------------------------------------+
  | +-----------+   +----------------+   +-----------+ |
  | |kamal-proxy|   |Thruster + Rails|   |MailCatcher| |
  | +-----------+   +----------------+   +-----------+ |
  +----------------------------------------------------+

そのデプロイ先のホストは、 LAN 内にすぐに利用できる Windows PC があったので、それを使うことにしました。しかし Windows は使わず、この Windows の WSL2 環境で動く Rocky Linux 9 を使います。その Linux 上のユーザは "rocky" とします。

手元の開発マシンは macOS で、ユーザは "developer" とします。ここからデプロイを実行します。またデプロイ後のアプリのサービスの利用もここから行います。

1. 事前準備

この事例では、あらかじめデプロイ先のホストのセットアップが必要です:

  • SSH 公開鍵認証によるログインの設定
  • Docker エンジン
  • Windows ファイアウォールと、ポート転送の設定

また関連事項として必要とするものもあります:

  • コンテナ・レジストリの用意
  • SSL/TLS 証明書と hosts ファイル

1-a. SSH 公開鍵認証によるログインの設定

Kamal は、デプロイ先のホストに SSH でログインして、デプロイ先のホストをコントロールします。そのため事前に、指定のユーザが SSH でログインできる環境を整えておく必要があります。

この事例では、デプロイ元の macOS のユーザ developer が、 デプロイ先のホスト上のユーザ rocky として公開鍵認証によって SSH でデプロイ先にログインできるように設定しておきます。

1-b. Docker エンジン

Kamal は Docker エンジンそのもののインストール、セットアップも行う機能を持っていますが、それは今回は使いません。

デプロイ先では、公式のインストールガイドに従って Docker エンジンをインストール、セットアップしました。従ってこの事例では、デプロイ先のホスト上には Docker エンジンがすでに稼働している状態を前提としてます。

Install Docker Engine on CentOS

要約すると、次のようにインストールしました:

$ sudo dnf install -y dnf-plugins-core
$ sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
$ sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
$ sudo systemctl enable --now docker

また今回の事例ではデプロイ先へ SSH 接続するユーザは、一般ユーザ( rocky )を想定しています。そのためにも、( Kamal によるセットアップではなく)ユーザ自身で Docker エンジンをインストール、セットアップする必要があります。

注意点として、一般ユーザが Docker コンテナを作成するには、そのユーザーがグループ docker に属している必要がありますので、その状況を確認してください。

$ id 
uid=1000(rocky) gid=1000(rocky) groups=1000(rocky),10(wheel),998(docker)
$ 

もし属していなければ、次のようにしてグループに追加します(その後、ログインし直します):

$ sudo usermod -aG docker rocky

1-c. Windows のファイアウォール設定、ポート解放

この事例では Windows 11 の WSL2 環境で Rocky Linux 9 を動かしますが、その仮想マシンはあくまでも Windows の内部にあるために、 Windows の外部からのアクセスは Windows のファイアウォールに阻まれます。

従って Windows ファイアウォールで、使用するポートの受け入れルールを追加してください。

ポート番号 用途
22 デプロイ元から SSH でアクセスします。
443 デプロイしたアプリのサービスの利用は https でアクセスします。
1080 同様に MailCatcher の Web インタフェースはこのポートで動作します。

また同時に、それらのポートを WSL2 上の Linux ホストへ転送するようにしておく必要があります。 Windows のことは詳しくないので仔細は省きますが(すみません)、大まかに述べますと管理者権限の PowerShell で次のようなことを行います:

# WSL2 の IPアドレス 確認(複数出ますが、おそらく最初のもの)
PS> wsl hostname -I
172.30.144.223 172.17.0.1 172.18.0.1
PS> 

# ポート転送設定
PS> netsh interface portproxy add v4tov4 listenport=22 listenaddress=0.0.0.0 connectport=22 connectaddress=172.30.144.223
PS> netsh interface portproxy add v4tov4 listenport=443 listenaddress=0.0.0.0 connectport=443 connectaddress=172.30.144.223
PS> netsh interface portproxy add v4tov4 listenport=1080 listenaddress=0.0.0.0 connectport=1080 connectaddress=172.30.144.223
PS>

# 一覧表示
PS> netsh interface portproxy show all

ipv4 をリッスンする:         ipv4 に接続する:

Address         Port        Address         Port
--------------- ----------  --------------- ----------
0.0.0.0         22          172.30.144.223  22
0.0.0.0         1080        172.30.144.223  1080
0.0.0.0         443         172.30.144.223  443

PS>

なお WSL2 の IP アドレスは Windows を再起動すると変わることがありますので、その際は再設定してください。(余談ですが、私は設定を行うバッチ・スクリプトを ChatGPT に作ってもらいました。)

1-d. コンテナ・レジストリの用意

Kamal はデプロイ時にビルドしたイメージを、コンテナ・レジストリに push します。デプロイ先では、そこからイメージを pull します。

つまりそのために、コンテナ・レジストリにアカウントが必要です。代表的なものには Docker や GitHub といったプロバイダーがあります。

ここでは詳細を省きますが、注意点として、利用するアカウントに適切な権限が設定されていることを確認してください。たとえば GitHub Container Registry を利用するのであれば、 write:packages read:packages delete:packages といった権限が必要です。

追記@2025/11/5 前述の追記のとおり、Kamal 2.8 から外部サービスではなくローカルホスト上の Docker 環境に作られるコンテナ・レジストリを指定・利用できるようになりました。したがって外部サービスのコンテナ・レジストリは必須ではありません。

1-e. SSL/TLS 証明書と hosts ファイル

本番環境のアプリが https で動くので、ステージング環境でも https で動かそうと思います。

SSL/TLS のカスタム証明書については、 Kamal では Let's Encrypt を利用した証明書の作成(更新)を自動で行う機能がありますが、この事例では LAN 内に構築するステージング環境という都合から、 mkcert を利用した自前のルート認証局をあらかじめ作成し、そのルート認証局証明書を、サービスを利用する環境(ユーザの環境: macOS )およびアプリのデプロイ先の環境( Rocky Linux )にインストールしておくことになります。

ルート認証局証明書

$ mkcert -CAROOT
$ ls -l "`mkcert -CAROOT`"
total 16
-r--------  1 developer  staff  2484  7  4 13:38 rootCA-key.pem
-rw-r--r--  1 developer  staff  1761  7  4 13:38 rootCA.pem
$

作成した rootCA.pem を、ステージング環境のアプリにアクセスする各所のマシンのシステムにインストールします。

macOS では "キーチェーンアクセス" に登録します。

Windows の場合は "ユーザー証明書の管理"(プログラム名 "certmgr" で検索)を実行し、「信頼されたルート証明機関」に rootCA.pem を追加してください。

Rocky Linux 9 では、ルート認証局証明書のファイルを /etc/pki/ca-trust/source/anchors/ の中に配置し、コマンド update-ca-trust を実行します(いずれも管理者権限で実行します):

$ sudo cp -ip rootCA.pem /etc/pki/ca-trust/source/anchors/
$ sudo update-ca-trust
$

コマンドの出力は特にありません。 /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem の内容を直接見てみると、冒頭に rootCA.pem の内容が挿入されていることがわかります。

カスタム証明書と hosts ファイル

次に、 mkcert でカスタム証明書を作成する際には、 Common Name となるホスト名が解決できなければいけないため、ステージング環境にアクセスする関係各所の /etc/hosts ファイルにホスト名と、その IPアドレス のペアを登録しておきます。(もし LAN 内のアドレスを解決できる DNS が利用できるのであれば、それを利用すれば十分です)。

この記事では説明のために、ステージング環境のホスト名を "staging.example.com" とします。このホスト名に対するカスタム証明書を作成します:

$ mkcert staging.example.com
$ ls -l *.pem
-rw-------  1 developer  staff  1704  7  4 13:40 staging.example.com-key.pem
-rw-r--r--  1 developer  staff  1570  7  4 13:40 staging.example.com.pem
$ 

これらファイルの内容を Kamal に設定します(後述)。

2. Dockerfile

(ここは Rails の話になりますが、ほかの環境に於いては、 kamal-proxy はデプロイするアプリケーション・サーバのポート番号を知る必要がある、という点に注意してください。)

Dockerfile のうちデプロイに関係するところを示します:

...
...
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
ENV THRUSTER_HTTP_PORT="3001" \
    THRUSTER_TARGET_PORT="3000" \
    THRUSTER_DEBUG=1
CMD ["bundle", "exec", "thrust", "bin/rails", "server"]

Rails アプリはサーバに Thruster を使うようにしています。コマンド名は thrust です。

Thruster の設定は環境変数で行うことになっていますので、それを設定しています。 Rails アプリが使うポートはデフォルトが 3000 なのでそれを尊重し、 Thruster では 3001 を使用するようにしました。 kamal-proxy はこの 3001 とやりとりすることになります。

3. Kamal の設定

ここまでの内容を踏まえ、 Kamal の設定ファイルを作ります。

3-a. 設定 config/deploy.yml

全体を示します:

require_destination: true
service: myapp

image: myapp-staging

servers:
  web:
    hosts:
      - staging.example.com

proxy:
  host: staging.example.com
  ssl:
    certificate_pem: DEPLOY_CERTIFICATE_PEM
    private_key_pem: DEPLOY_PRIVATE_KEY_PEM
  app_port: 3001

registry:
  server: ghcr.io
  username: (アカウント名)
  password:
    - DEPLOY_REGISTRY_PASSWORD

builder:
  arch:
    - amd64
    - arm64
  context: .
  dockerfile: Dockerfile

env:
  clear:
    RAILS_ENV: staging
    SMTP_ADDRESS: smtp
    SMTP_PORT: 1025
  secret:
    - SECRET_KEY_BASE

ssh:
  port: 22
  user: rocky
  keys_only: true
  keys:
    - /Users/developer/.ssh/id_rsa

volumes:
  - "/home/rocky/myapp-storage:/rails/storage"

accessories:
  mailcatcher:
    service: smtp
    image: sj26/mailcatcher:v0.10.0
    host: staging.example.com
    options:
      publish:
        - "1080:1080"

このうち、特徴的な箇所について説明します。

proxy.host と proxy.ssl

proxy:
  host: staging.example.com
  ssl:
    certificate_pem: DEPLOY_CERTIFICATE_PEM
    private_key_pem: DEPLOY_PRIVATE_KEY_PEM

SSL/TLS カスタム証明書の Common Name に相当するホスト名を host に設定する必要があります。(余談:最初、これはデプロイ先のホストかと考えて IP アドレスで記述していたため、うまくいかずにハマりました。)

また proxy.ssl の方ですが、 certificate_pemprivate_key_pem の値はシークレットから読み取るように設定します。

registry

registry:
  server: ghcr.io
  username: (アカウント名)
  password:
    - DEPLOY_REGISTRY_PASSWORD

コンテナ・レジストリの設定です。 GitHub を利用する場合は server が "ghcr.io" です。 username はアカウント名、 password は Personal Access Token アクセストークンと呼ばれています。これもシークレットから読み取るように設定します。

追記@2025/11/5 コンテナ・レジストリには、 Kamal 2.8 からローカルホストを指定することができるようになりました。 registry.serverlocalhost:5555 (任意のポート番号)を指定するだけです。これによりデプロイ時に、ローカルホスト上の Docker にレジストリとなるコンテナが追加され、そこを介して処理が進むようになります。

builder.arch

builder:
  arch:
    - amd64
    - arm64

今回の事例では、デプロイ元となる開発環境は M1 チップの macOS です。これはアーキテクチャ "arm64" と示されます。一方、デプロイ先は Windows マシンなので、 "amd64" です。つまり Dockerfile のビルドに際しては "amd64" 向けのイメージを作成しなければいけません。

この際なので、両方の環境に対応できるよう、両方ともビルドするようにします。

いずれにしろ、デプロイ元の Docker で buildx が利用できなければいけません。私の macOS の環境では Docker Desktop をインストールしましたが、その中に Docker Buildx は含まれています。次のように確認してみてください:

$ docker buildx version 
github.com/docker/buildx v0.23.0-desktop.1 503f948aadbddb6de3ec5581f766e1d27f6975a1
$

なお、ビルダーについては Kamal がデプロイ時に(ビルド時に)、必要に応じてビルダーのコンテナを作るようで、あらかじめ用意する必要はない模様です。

env

env:
  clear:
    RAILS_ENV: staging
    SMTP_ADDRESS: smtp
    SMTP_PORT: 1025
  secret:
    - SECRET_KEY_BASE

これらは Rails アプリ用の動作設定に利用している環境変数の設定であり、 Kamal 自体には関係ありません。

ただし SMTP_ADDRESS として指定している "smtp" 値は、 Docker の内部ネットワーク内で参照できるホスト名を示しています。 "smtp" は accessories.mailcatcher.service の値として設定しているサービス名です。

もし accessories.mailcatcher.service が未設定の場合は、トップレベルの service の値とアクセサリ名を組み合わせた myapp-mailcatcher というホスト名になります。

volumes

volumes:
  - "/home/rocky/myapp-storage:/rails/storage"

これもデプロイするアプリの都合のものであり、 Kamal 自体には関係ありません。 Docker のボリュームの設定を、このように記述しますという例です。

このディレクトリは、最初のデプロイの前に作成しておきます。もし作成が後になってしまった場合は、デプロイ後のサーバの起動時に "書き込み権限がない" といった問題に遭遇するかもしれません。

$ mkdir -p /home/rocky/myapp-storage

accessories.mailcatcher.options

accessories:
  mailcatcher:
    service: smtp
    image: sj26/mailcatcher:v0.10.0
    host: staging.example.com
    options:
      publish:
        - "1080:1080"

MailCatcher をアクセサリとして追加しますが、 MailCatcher は SMTP と HTTP と、二つのサービスを使用します。

SMTP は Docker の内部ネットワーク "kamal" を使用するので特に指定していませんが、 HTTP はポート 1080 を公開するため、それを accessories.mailcatcher.options.publish にて設定しています。

3-b. シークレットの設定

将来は本番環境のための設定を作ることになるので、あらかじめファイルを secrets.stagingsecrets.production とに分けておきました。

余談ですが、デプロイに関する項目は、接頭語として "DEPLOY_" をつけるようにしています( Kamal の制約ではありません)。実際はたくさんの設定項目があるので、グループ分けのためにそうしています。

.kamal/secrets.staging

DEPLOY_REGISTRY_PASSWORD=$DEPLOY_REGISTRY_PASSWORD

デプロイするイメージを push する先となる、コンテナ・レジストリのアカウントのパスワード(トークン)を設定します。

この記述は、「具体的な値は環境変数 $DEPLOY_REGISTRY_PASSWORD からセットする」という記述であって、その値を直接ここに書いてはいけません。

DEPLOY_CERTIFICATE_PEM=$DEPLOY_CERTIFICATE_PEM
DEPLOY_PRIVATE_KEY_PEM=$DEPLOY_PRIVATE_KEY_PEM

ステージング環境では SSL/TLS のための設定としてカスタム証明書とその秘密鍵を使うため、そのためのシークレットを設定します。さきほどと同様に、「具体的な値は環境変数の $DEPLOY_CERTIFICATE_PEM$DEPLOY_PRIVATE_KEY_PEM からセットする」としています。

SECRET_KEY_BASE=$SECRET_KEY_BASE

SECRET_KEY_BASE は Rails のための設定で、 Kamal は関係ありませんが、アプリの機密情報もシークレットに記述しています、という例示です。

3-c. 環境変数

これまでの説明の中にあるように、設定情報の一部は環境変数を通して設定するようにしています。

これらは秘匿情報を含むので、外部に漏れ出さないように注意が必要です。 dotenv を利用する場合は、少なくとも .gitignore.dockerignore ファイルなどで .env* が無視リストに含まれているか、確認してください。

.env.staging ファイルを利用すると、次のような記述になります:

DEPLOY_REGISTRY_PASSWORD="abcdefg......"

SECRET_KEY_BASE="0123456789abc......"

DEPLOY_CERTIFICATE_PEM="-----BEGIN CERTIFICATE-----
NIIEWTCCAsGgAwIBAgIQYwiE1ETlNvxeqWT5cDDvnDANBgkqhkiG9w0BAQsFADCB
izEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTAwLgYDVQQLDCdoaXJv
(途中省略)
BGJkgl3TOxkzk6jlYeplHDbyFyRVcaXsV9Xf2D/jgZCKygUxinv8rfhUUZ3Njwo0
KrelGkjiRlvVyYSWlYkHyFIyzmADFxljsU6eXNJnldPsQ7onwhQ5+212mXwTiH0w
wJKsjhJf4F8pZBbQJg==
-----END CERTIFICATE-----
"

DEPLOY_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----
NIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDlWmEM3dIncagc
Z8F+xLr0UNsmwyWCbCTGpkzdRX16zGxlLXi5ChDAhyrBQBtFdjHlfvuPELKa6b+r
(途中省略)
CL+EMm2OYIueHt1OrZ0q/YlNB6PzNpFWDDo9DCZ9gGMqPC4cFYQrFCvOjSyzdVVY
gLvKiU5SfZ0IVnWtSB1u0ZACUWys8ctikQ1mMaWqBq+V3mKASa7p1QXqzhaT19Mu
0JMdXiUTYFhYrTl3AK+AEA==
-----END PRIVATE KEY-----
"

(秘密鍵を露にしてしまっていますが、この鍵はでたらめなものです)

このうち、 DEPLOY_CERTIFICATE_PEM には事前準備で作成したカスタム証明書 staging.example.com.pem の内容を、そして DEPLOY_PRIVATE_KEY_PEM にはカスタム証明書の秘密鍵 staging.example.com-key.pem の内容を、それぞれ貼り付けます。

このように PEM データを環境変数に設定するのは、いささか躊躇われるようなことかもしれません。代わりの手段として、シークレット内で PEM データを環境変数から読み取る代わりに、 PEM ファイルの内容を cat した出力から読み取る、というやり方でもよいと思います。

しかしながら、秘匿情報の入った保護するべきファイルの数を増やしたくないという意図があり、すべての秘匿情報は環境変数(実際は、それを設定した .env ファイル)だけにするという選択をしています。このあたりの事情は運用の方針に従えばよいでしょう。

4. デプロイの実行

環境変数をセットしたら、デプロイを実行します。

最初のデプロイ時には、まず先にアクセサリをデプロイします:

$ dotenv -f .env.staging kamal accessory boot mailcatcher --destination=staging

それから本体のアプリケーションのデプロイを実行します。これにより kamal-proxy もデプロイされます:

$ dotenv -f .env.staging kamal deploy --destination=staging

基本的なデプロイ作業は以上になります。

事前作業がけっこうな手間がかかりましたが、ステージング環境という特殊な事情からのことなので(特に Windows PC を使っていることや、サーバ証明書に関する諸々)、記述したことの多くは Kamal 自体の手間ではありません。

Kamal に限りませんが、公式のドキュメントや、第三者による Web 記事などで得た知識をもとに、いざそれを動かしてみるとつまずくことも多いですが、つまずくことも良い経験になるので、まず手を動かすことは大事なことと思います。しかしいきなり本番環境でやることではありませんから、さしあたっては手近な PC に安全なステージング環境を構築し、そこで練習をしてみるのがよいでしょう。


最後になりましたが、とても参考になった記事とサイトを紹介します。ありがとうございます。

  • Real World Kamal - タケユー・ウェブ株式会社
    Kamal の公式ドキュメントだけではイメージを掴みづらいのですが、まさに Real な事例を紹介くださっていることで、理解の助けになりました。

  • Kamal README: 37signalsの多機能コンテナデプロイツール(翻訳) - TechRacho by BPS株式会社
    現在のバージョンに対して少し古くなってしまった情報になるのですが、ほかの Web 記事にはないトピックがあったり内容も丁寧なので、 Kamal の世界観を把握しやすくなりました。

  • Kamal discussions
    Kamal のリポジトリに付随するディスカッションです。さまざまな視点が見られ、設定ファイルの書き方などで気づくこともよくありました。また Issue よりも気軽に投稿できそうな雰囲気があり、思い悩んだ時に頼りになりそうです。

Discussion